From 4e44f2164899c1a7e38880c79d185ec79eb236d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Tue, 5 Dec 2023 21:33:01 -0600 Subject: [PATCH 1/3] Sign: Upload to tlog and capture sig data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a large commit that refactors the Sign() method of the attestation. The main goal is to add two missing features: 1. Register the signature data to Rekor After signing, we now register the signature in the sigstore transparency log. This is essentail to allow for keyless verification. 2. New SignatureData Field The attestation now has a new SignatureData field that captures the results of the signing operation. This is required to make data like the cert and the proof of inlclusion available externally (eg to record them in oci annotations). The attestation.Sign() method has been heavily refactored but should be simppler as the work it does is now broken into three internal functions: initSigning: creates context and options signAttestation: Performs the actual signing appendSignatureDataToTLog: Uploads data to rekor Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/attestation/attestation.go | 166 ++++++++++++++++++++++++--------- 1 file changed, 122 insertions(+), 44 deletions(-) diff --git a/pkg/attestation/attestation.go b/pkg/attestation/attestation.go index 7437c7f..b2e190a 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 +} From 5aa0d4485f6e8c38e88456fa35e3b08aca626e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Tue, 5 Dec 2023 21:42:35 -0600 Subject: [PATCH 2/3] Attach: Add OCI annotations for keyless verification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit modifies the attachAttestation function of the vexctl implementation to add the OCI annotations required to keylessly verify OpenVEX attestations. Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/attestation/attestation.go | 10 +++++----- pkg/ctl/implementation.go | 21 +++++++++++++++++++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/pkg/attestation/attestation.go b/pkg/attestation/attestation.go index b2e190a..52a7b38 100644 --- a/pkg/attestation/attestation.go +++ b/pkg/attestation/attestation.go @@ -64,12 +64,12 @@ func (att *Attestation) Sign() error { ctx, ko := initSigning() // Sign the attestaion. - if err := signAttestation(ctx, ko, att); err != nil { + if err := signAttestation(ctx, &ko, att); err != nil { return fmt.Errorf("signing attestation: %w", err) } // Register the signature in rekor - if err := appendSignatureDataToTLog(ctx, ko, att); err != nil { + if err := appendSignatureDataToTLog(ctx, &ko, att); err != nil { return fmt.Errorf("recording signature data to transparency log: %w", err) } @@ -136,13 +136,13 @@ func initSigning() (context.Context, options.KeyOpts) { // 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 { +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) + sv, err := sign.SignerFromKeyOpts(ctx, "", "", *ko) if err != nil { return fmt.Errorf("getting signer: %w", err) } @@ -180,7 +180,7 @@ func signAttestation(ctx context.Context, ko options.KeyOpts, att *Attestation) // 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 { +func appendSignatureDataToTLog(ctx context.Context, ko *options.KeyOpts, att *Attestation) error { tlogClient, err := rekor.NewClient(ko.RekorURL) if err != nil { att.SignatureData = 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 From d10bee436004d28f88b24bdc4066bca883098ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Tue, 5 Dec 2023 22:05:00 -0600 Subject: [PATCH 3/3] go mod tidy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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