diff --git a/go.mod b/go.mod index 2da3664..e2c8233 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/owenrumney/go-sarif v1.1.1 github.com/secure-systems-lab/go-securesystemslib v0.7.0 github.com/sigstore/cosign/v2 v2.2.2 + github.com/sigstore/rekor v1.3.4 github.com/sigstore/sigstore v1.7.6 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 @@ -145,7 +146,6 @@ require ( github.com/sassoftware/relic v7.2.1+incompatible // indirect github.com/segmentio/ksuid v1.0.4 // indirect github.com/sigstore/fulcio v1.4.3 // indirect - github.com/sigstore/rekor v1.3.4 // indirect github.com/sigstore/timestamp-authority v1.2.0 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/sourcegraph/conc v0.3.0 // indirect diff --git a/pkg/attestation/attestation.go b/pkg/attestation/attestation.go index 7437c7f..52a7b38 100644 --- a/pkg/attestation/attestation.go +++ b/pkg/attestation/attestation.go @@ -11,22 +11,45 @@ import ( "errors" "fmt" "io" + "os" "strings" - "time" "github.com/google/go-containerregistry/pkg/crane" intoto "github.com/in-toto/in-toto-golang/in_toto" ovattest "github.com/openvex/go-vex/pkg/attestation" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v2/cmd/cosign/cli/sign" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/sigstore/pkg/signature/dsse" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" ) type Attestation struct { - signedData []byte `json:"-"` ovattest.Attestation + + // Sign is boolean that signals if the attestation has been signed Signed bool `json:"-"` + + // signatureData embeds the signed attestaion, the certificate used to sign + // it and the transparency log inclusion proof + SignatureData *SignatureData `json:"-"` +} + +type SignatureData struct { + // CertData of the cert used to sign the attestation encodeded in PEM + CertData []byte `json:"-"` + + // Chain contains the intermediate certificate chain of the attestation's cert + Chain []byte `json:"-"` + + // Entry contains the proof of inclusion to the transparency log + Entry *models.LogEntryAnon `json:"-"` + + // signedPayload contains the resulting blob after the attestation was + // signed. + signedPayload []byte } func New() *Attestation { @@ -38,51 +61,18 @@ func New() *Attestation { // Sign the attestation func (att *Attestation) Sign() error { - ctx := context.Background() - var timeout time.Duration /// TODO move to options - var certPath, certChainPath string - ko := options.KeyOpts{ - // KeyRef: s.options.PrivateKeyPath, - // IDToken: identityToken, - FulcioURL: options.DefaultFulcioURL, - RekorURL: options.DefaultRekorURL, - OIDCIssuer: options.DefaultOIDCIssuerURL, - OIDCClientID: "sigstore", - - InsecureSkipFulcioVerify: false, - SkipConfirmation: true, - // FulcioAuthFlow: "", - } - - if timeout != 0 { - var cancelFn context.CancelFunc - ctx, cancelFn = context.WithTimeout(ctx, timeout) - defer cancelFn() - } - - sv, err := sign.SignerFromKeyOpts(ctx, certPath, certChainPath, ko) - if err != nil { - return fmt.Errorf("getting signer: %w", err) - } - defer sv.Close() + ctx, ko := initSigning() - // Wrap the attestation in the DSSE envelope - wrapped := dsse.WrapSigner(sv, "application/vnd.in-toto+json") - - var b bytes.Buffer - if err := att.ToJSON(&b); err != nil { - return fmt.Errorf("serializing attestation to json: %w", err) + // Sign the attestaion. + if err := signAttestation(ctx, &ko, att); err != nil { + return fmt.Errorf("signing attestation: %w", err) } - signedPayload, err := wrapped.SignMessage( - bytes.NewReader(b.Bytes()), signatureoptions.WithContext(ctx), - ) - if err != nil { - return fmt.Errorf("signing attestation: %w", err) + // Register the signature in rekor + if err := appendSignatureDataToTLog(ctx, &ko, att); err != nil { + return fmt.Errorf("recording signature data to transparency log: %w", err) } - att.Signed = true - att.signedData = signedPayload return nil } @@ -114,12 +104,100 @@ func (att *Attestation) ToJSON(w io.Writer) error { if !att.Signed { return att.Attestation.ToJSON(w) } - if len(att.signedData) == 0 { + if att.SignatureData == nil || len(att.SignatureData.signedPayload) == 0 { return errors.New("consistency error: attestation is signed but data is empty") } - if _, err := w.Write(att.signedData); err != nil { + if _, err := w.Write(att.SignatureData.signedPayload); err != nil { return fmt.Errorf("writing signed attestation: %w", err) } return nil } + +// initSigning initializes the options and context needed to sign. Right now +// it only sets up some default options and a backgrous context but we +// should wire the options set from the CLI to this function +func initSigning() (context.Context, options.KeyOpts) { + ko := options.KeyOpts{ + FulcioURL: options.DefaultFulcioURL, + RekorURL: options.DefaultRekorURL, + OIDCIssuer: options.DefaultOIDCIssuerURL, + OIDCClientID: "sigstore", + InsecureSkipFulcioVerify: false, + SkipConfirmation: true, + } + + ctx := context.Background() + // TODO(puerco): Support context.WithTimeout(ctx, timeout) + + return ctx, ko +} + +// signAttestation creates a signer and signs the attestation. The attestation's +// SignatureData field will be populated with the certificate, chain and the +// attestaion data wrapped in its DSSE envelope. +func signAttestation(ctx context.Context, ko *options.KeyOpts, att *Attestation) error { + // TODO(puerco): Investigate supporting certificates preloaded in the + // attestation. We would need to dump them to disk and load them into + // the args here and if we're reusing the bundle, set it in ko.BundlePath + // Note that in this call we hardocde the pats empty, but we should get them + // from somewhere. + sv, err := sign.SignerFromKeyOpts(ctx, "", "", *ko) + if err != nil { + return fmt.Errorf("getting signer: %w", err) + } + defer sv.Close() + + // Wrap the attestation in the DSSE envelope + wrapped := dsse.WrapSigner(sv, "application/vnd.in-toto+json") + + var b bytes.Buffer + if err := att.ToJSON(&b); err != nil { + return fmt.Errorf("serializing attestation to json: %w", err) + } + + // SIGN! + signedPayload, err := wrapped.SignMessage( + bytes.NewReader(b.Bytes()), signatureoptions.WithContext(ctx), + ) + if err != nil { + return fmt.Errorf("signing attestation: %w", err) + } + + // Assign the new data to the attestation + att.SignatureData = &SignatureData{ + CertData: sv.Cert, + Chain: sv.Chain, + signedPayload: signedPayload, + } + att.Signed = true + + return nil +} + +// appendSignatureDataToTLog records the signature data to the transparency log +// (rekor). The proof of inclusion will be added to the attestation's SignatureData +// struct. +// If uploading fails, the signature data will be destroyed to guarantee an atomic +// operation of attesation.Sign() +func appendSignatureDataToTLog(ctx context.Context, ko *options.KeyOpts, att *Attestation) error { + tlogClient, err := rekor.NewClient(ko.RekorURL) + if err != nil { + att.SignatureData = nil + return fmt.Errorf("creating rekor client: %w", err) + } + + // ...and upload the signature data + entry, err := cosign.TLogUploadDSSEEnvelope( + ctx, tlogClient, att.SignatureData.signedPayload, att.SignatureData.CertData, + ) + if err != nil { + att.SignatureData = nil + return fmt.Errorf("uploading to transparency log: %w", err) + } + + att.SignatureData.Entry = entry + fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) + + return nil +} diff --git a/pkg/ctl/implementation.go b/pkg/ctl/implementation.go index 39771f8..f55dcdc 100644 --- a/pkg/ctl/implementation.go +++ b/pkg/ctl/implementation.go @@ -23,6 +23,7 @@ import ( ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" "github.com/sigstore/cosign/v2/pkg/cosign" + cbundle "github.com/sigstore/cosign/v2/pkg/cosign/bundle" "github.com/sigstore/cosign/v2/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" "github.com/sigstore/cosign/v2/pkg/oci/static" @@ -185,7 +186,7 @@ func (impl *defaultVexCtlImplementation) Attach(ctx context.Context, att *attest } for _, ref := range refs { - if err := attachAttestation(ctx, payload, ref); err != nil { + if err := attachAttestation(ctx, att, payload, ref); err != nil { return fmt.Errorf("attaching attestation to %s: %w", ref, err) } } @@ -196,7 +197,7 @@ func (impl *defaultVexCtlImplementation) Attach(ctx context.Context, att *attest // attachAttestation is a utility function to do the actual attachment of // the signed attestation -func attachAttestation(ctx context.Context, payload []byte, imageRef string) error { +func attachAttestation(ctx context.Context, original *attestation.Attestation, payload []byte, imageRef string) error { regOpts := options.RegistryOptions{} remoteOpts, err := regOpts.ClientOpts(ctx) if err != nil { @@ -216,6 +217,22 @@ func attachAttestation(ctx context.Context, payload []byte, imageRef string) err ref = digest //nolint:ineffassign opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)} + + // Add the attestation certificate: + opts = append(opts, static.WithCertChain(original.SignatureData.CertData, original.SignatureData.Chain)) + + // Add the tlog entry to the annotations + if original.SignatureData.Entry != nil { + opts = append(opts, static.WithBundle( + cbundle.EntryToBundle(original.SignatureData.Entry), + )) + } + + // Add predicateType as manifest annotation + opts = append(opts, static.WithAnnotations(map[string]string{ + "predicateType": vex.Context, + })) + att, err := static.NewAttestation(payload, opts...) if err != nil { return err