Skip to content

Commit

Permalink
Merge pull request #126 from puerco/reuse-digests
Browse files Browse the repository at this point in the history
Update vexctl attest
  • Loading branch information
cpanato authored Oct 10, 2023
2 parents b3aa228 + 0c730fc commit e50faaf
Show file tree
Hide file tree
Showing 4 changed files with 358 additions and 176 deletions.
135 changes: 91 additions & 44 deletions internal/cmd/attest.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,50 +9,103 @@ import (
"context"
"errors"
"fmt"
"io"
"os"

"github.com/google/go-containerregistry/pkg/name"
"github.com/openvex/vexctl/pkg/ctl"
"github.com/spf13/cobra"
)

type attestOptions struct {
outFileOption
attach bool
sign bool
refs []string
}

func (o *attestOptions) AddFlags(cmd *cobra.Command) {
o.outFileOption.AddFlags(cmd)
cmd.PersistentFlags().BoolVarP(
&o.attach,
"attach",
"a",
false,
"attach the generated attestation to an image (implies --sign)",
)

cmd.PersistentFlags().BoolVarP(
&o.sign,
"sign",
"s",
false,
"sign the attestation with sigstore",
)

cmd.PersistentFlags().StringArrayVarP(
&o.refs,
"refs",
"r",
[]string{},
"list of image references to attach the attestation to",
)
}

// Validate checks if the options are sane
func (o *attestOptions) Validate() error {
var sErr error
for _, ref := range o.refs {
if _, err := name.ParseReference(ref); err != nil {
sErr = fmt.Errorf("parsing reference: %w", err)
break
}
}

if o.attach {
o.sign = true
}

return errors.Join(
sErr, o.outFileOption.Validate(),
)
}

func addAttest(parentCmd *cobra.Command) {
opts := attestOptions{}
generateCmd := &cobra.Command{
attestCmd := &cobra.Command{
Short: fmt.Sprintf("%s attest: generate a VEX attestation", appname),
Long: fmt.Sprintf(`%s attest: generate an VEX attestation
The attach subcommand lets users wrap OpenVEX documents in in-toto attestations.
Attestations generated by %s can be signed with sigstore and attached to container
images stored in an OCI registry.
Attestations generated by %s can be signed with sigstore and attached to
container images stored in an OCI registry.
In its simplest form, %s will create an attestation from an OpenVEX file and
write it to stdout:
%s attest data.vex.json
Without any more arguments, images defined as products in the VEX statements,
will be read by %s and transferred to the attestation's subjects when required.
Without any more arguments, container images and other products that have hashes
defined in their VEX statements, will be read by %s and transferred to
the attestation's subjects when required.
If the products are purls of type oci:, they will be converted to image
references as is customery in the sigstore tooling. For example:
references as is customary in the sigstore tooling. For example:
If a product identified by the following purl is found in a statement:
If a product identified by the following package URL is found in a statement:
pkg:oci/kube-apiserver?repository_url=registry.k8s.io&tag=v1.26.0
It will be transferred to the attestation subjects as:
registry.k8s.io/kube-apiserver:v1.26.0
Any purls and image references not specifying a digest will trigger a network
lookup to read the image digest from the registry.
Any oci purls and image references not specifying a digest will trigger a
network lookup to read the image digest from the registry.
Please note that purls of types other than oci: and other strings which are not
valid image references will not included in the attestation.
valid image references will not be included in the resulting attestation unless
the product has hashes associated with it.
Signing Attestations
--------------------
Expand All @@ -62,35 +115,38 @@ credentials from the user or trying to get them from the environment:
%s attest --sign data.vex.json
When signing an attestation, the standard sigstore signing flow will be defined
When signing an attestation, the standard sigstore signing flow will be triggered
if credentials are not found in the environment. Refer to the sigstore
documentation for details.
Attaching Attestations
----------------------
The --attach flag will attach the attestation to the OCI registry of all attested
images. It will use the credentials in the user's environment to authenticate,
this means that if you can write to the registry, attaching should work.
The --attach flag will attach the resulting attestation to the OCI registry of
all subjects that parse as image references. If this behavior fails, try defining
--refs to specify which images to attach the attestation to.
--attach always implies --sign as sigstore does not support attaching unsigned
images.
%s will use the credentials from the user's environment to authenticate to the
registry, this means that if you can write to the registry, attaching should work.
Note: --attach always implies --sign as sigstore does not support attaching
unsigned attestations.
Specifying Images to Attest
---------------------------
If any further positional arguments are defined, they will be interpreted as
products/image references. %s will generate and attach (if applicable) the
attestation only for those images, skpping any other products found in the
VEX document.
attestation only for those images, not transferring other products in the
VEX document as in-toto subjects.
For example, the following invocation will only attest and attach vex.json
to user/test, even if the OpenVEX document has entries for other images:
to user/test, even if the OpenVEX document has product entries for other images:
%s attest --attach vex.json user/test
`, appname, appname, appname, appname, appname, appname, appname, appname),
`, appname, appname, appname, appname, appname, appname, appname, appname, appname),
Use: "attest",
SilenceUsage: false,
SilenceErrors: false,
Expand All @@ -99,16 +155,16 @@ to user/test, even if the OpenVEX document has entries for other images:
if len(args) == 0 {
return errors.New("not enough arguments")
}
cmd.SilenceUsage = true

if err := opts.Validate(); err != nil {
return fmt.Errorf("validating options: %w", err)
}

cmd.SilenceUsage = true
ctx := context.Background()

vexctl := ctl.New()
vexctl.Options.Sign = opts.sign
// Attaching always means signing
if opts.attach {
vexctl.Options.Sign = true
}

attestation, err := vexctl.Attest(args[0], args[1:])
if err != nil {
Expand All @@ -121,29 +177,20 @@ to user/test, even if the OpenVEX document has entries for other images:
}
}

if err := attestation.ToJSON(os.Stdout); err != nil {
var out io.Writer = os.Stdout
if opts.outFileOption.outFilePath != "" {
out, err = os.Create(opts.outFilePath)
if err != nil {
return fmt.Errorf("opening attestation file: %w", err)
}
}
if err := attestation.ToJSON(out); err != nil {
return fmt.Errorf("marshaling attestation to json")
}

return nil
},
}

generateCmd.PersistentFlags().BoolVarP(
&opts.attach,
"attach",
"a",
false,
"attach the generated attestation to an image (implies --sign)",
)

generateCmd.PersistentFlags().BoolVarP(
&opts.sign,
"sign",
"s",
false,
"sign the attestation with sigstore",
)

parentCmd.AddCommand(generateCmd)
opts.AddFlags(attestCmd)
parentCmd.AddCommand(attestCmd)
}
65 changes: 49 additions & 16 deletions pkg/ctl/ctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ import (
"github.com/openvex/go-vex/pkg/vex"
"github.com/sirupsen/logrus"

intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/openvex/vexctl/pkg/attestation"
)

const errNoImage = "some entries are not container images: %v"
const errNotAttestable = "some entries are not attestable as they don't have a hash: %v"

type VexCtl struct {
impl Implementation
Expand All @@ -29,6 +30,13 @@ type Options struct {
Sign bool // When true, attestations will be signed before attaching
}

// ProductRefs is a struct that captures a resolved component reference string
// and any hashes associated with it.
type productRef struct {
Name string
Hashes map[vex.Algorithm]vex.Hash
}

func New() *VexCtl {
return &VexCtl{
impl: &defaultVexCtlImplementation{},
Expand Down Expand Up @@ -61,8 +69,8 @@ func (vexctl *VexCtl) Apply(r *sarif.Report, vexDocs []*vex.VEX) (finalReport *s
return finalReport, nil
}

// Generate an attestation from a VEX
func (vexctl *VexCtl) Attest(vexDataPath string, manSubjects []string) (*attestation.Attestation, error) {
// Attest generates an attestation from a list of identifiers
func (vexctl *VexCtl) Attest(vexDataPath string, subjectStrings []string) (*attestation.Attestation, error) {
doc, err := vexctl.impl.OpenVexData(vexctl.Options, []string{vexDataPath})
if err != nil {
return nil, fmt.Errorf("opening vex data: %w", err)
Expand All @@ -71,32 +79,57 @@ func (vexctl *VexCtl) Attest(vexDataPath string, manSubjects []string) (*attesta
// Generate the attestation
att := attestation.New()
att.Predicate = *doc[0]
subjects := manSubjects
subjects := []productRef{}
for _, s := range subjectStrings {
subjects = append(subjects, productRef{Name: s})
}

// If we did not get a specific list of subjects to attest, we default
// to the products of the VEX document.
if len(manSubjects) == 0 {
if len(subjects) == 0 {
subjects, err = vexctl.impl.ListDocumentProducts(doc[0])
if err != nil {
return nil, fmt.Errorf("listing document products")
return nil, fmt.Errorf("listing document products: %w", err)
}
}

imageSubjects, otherSubjects, err := vexctl.impl.NormalizeImageRefs(subjects)
imageSubjects, otherSubjects, unattestableSubjects, err := vexctl.impl.NormalizeProducts(subjects)
if err != nil {
return nil, fmt.Errorf("normalizing VEX products to attest: %w", err)
}

if len(otherSubjects) != 0 {
// if subject are manual, fail
if len(manSubjects) > 0 {
return nil, fmt.Errorf(errNoImage, otherSubjects)
if len(unattestableSubjects) != 0 {
// If subjects are manual, fail
if len(subjectStrings) > 0 {
return nil, fmt.Errorf(errNotAttestable, unattestableSubjects)
}
// If we are just checking an existing document, we dont err. We skip
// any unattestable subjects.
logrus.Warnf(errNotAttestable, unattestableSubjects)
}

allSubjects := []productRef{}
allSubjects = append(allSubjects, imageSubjects...)
allSubjects = append(allSubjects, otherSubjects...)
subs := []intoto.Subject{}
for _, sub := range allSubjects {
d := map[string]string{}
// TODO(puerco): Move this logic to the go-vex hash structs
for a, h := range sub.Hashes {
switch a {
case vex.SHA256:
d["sha256"] = string(h)
case vex.SHA512:
d["sha512"] = string(h)
}
}
// if from a doc, we ignore and skip
logrus.Warnf(errNoImage, otherSubjects)
subs = append(subs, intoto.Subject{
Name: sub.Name,
Digest: d,
})
}

if err := att.AddImageSubjects(imageSubjects); err != nil {
if err := att.AddSubjects(subs); err != nil {
return nil, fmt.Errorf("adding image references to attestation: %w", err)
}

Expand All @@ -116,8 +149,8 @@ func (vexctl *VexCtl) Attest(vexDataPath string, manSubjects []string) (*attesta
}

// Attach attaches an attestation to a list of images
func (vexctl *VexCtl) Attach(ctx context.Context, att *attestation.Attestation) (err error) {
if err := vexctl.impl.Attach(ctx, att); err != nil {
func (vexctl *VexCtl) Attach(ctx context.Context, att *attestation.Attestation, refs ...string) (err error) {
if err := vexctl.impl.Attach(ctx, att, refs...); err != nil {
return fmt.Errorf("attaching attestation: %w", err)
}

Expand Down
Loading

0 comments on commit e50faaf

Please sign in to comment.