diff --git a/internal/cmd/attest.go b/internal/cmd/attest.go index f46d73a..e71c4c8 100644 --- a/internal/cmd/attest.go +++ b/internal/cmd/attest.go @@ -9,34 +9,86 @@ 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 customary in the sigstore tooling. For example: @@ -48,11 +100,12 @@ 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 be included in the resulting attestation. +valid image references will not be included in the resulting attestation unless +the product has hashes associated with it. Signing Attestations -------------------- @@ -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, @@ -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 { @@ -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) } diff --git a/pkg/ctl/ctl.go b/pkg/ctl/ctl.go index bfb0114..fa8003d 100644 --- a/pkg/ctl/ctl.go +++ b/pkg/ctl/ctl.go @@ -149,8 +149,8 @@ func (vexctl *VexCtl) Attest(vexDataPath string, subjectStrings []string) (*atte } // 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) }