From 43933130d2cae41d333e5148c54fc2fb7e77e712 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Wed, 18 Sep 2024 16:47:43 -0400 Subject: [PATCH] Fix bug in attest-blob when using a timestamp authority with new bundles (#3877) * Fix bug in #3752 When adding bundles support to `attest-blob`, we sent the wrong data to the timestamp authority to sign. Signed-off-by: Zach Steindler * Only change timestamp authority signature behavior for new bundles Also add TODO when we get to updating `cosign attest` Signed-off-by: Zach Steindler * Add happy path e2e test Signed-off-by: Zach Steindler --------- Signed-off-by: Zach Steindler --- cmd/cosign/cli/attest/attest.go | 9 ++- cmd/cosign/cli/attest/attest_blob.go | 32 +++++++- test/e2e_test.go | 109 +++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 4 deletions(-) diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index abfb25e8324..b23d587fc33 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -175,7 +175,14 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) } if c.KeyOpts.TSAServerURL != "" { - // Here we get the response from the timestamped authority server + // TODO - change this when we implement protobuf / new bundle support + // + // Historically, cosign sent the entire JSON DSSE Envelope to the + // timestamp authority. However, when sigstore clients are verifying a + // bundle they will use the DSSE Sig field, so we choose what signature + // to send to the timestamp authority based on our output format. + // + // See cmd/cosign/cli/attest/attest_blob.go responseBytes, err := tsa.GetTimestampedSignature(signedPayload, tsaclient.NewTSAClient(c.KeyOpts.TSAServerURL)) if err != nil { return err diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go index 50795215e66..beb8ecf0bf9 100644 --- a/cmd/cosign/cli/attest/attest_blob.go +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -165,9 +165,35 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error var rekorEntry *models.LogEntryAnon if c.TSAServerURL != "" { - timestampBytes, err = tsa.GetTimestampedSignature(sig, client.NewTSAClient(c.TSAServerURL)) - if err != nil { - return err + // We need to decide what signature to send to the timestamp authority. + // + // Historically, cosign sent `sig`, which is the entire JSON DSSE + // Envelope. However, when sigstore clients are verifying a bundle they + // will use the DSSE Sig field, so we choose what signature to send to + // the timestamp authority based on our output format. + if c.NewBundleFormat { + var envelope dsse.Envelope + err = json.Unmarshal(sig, &envelope) + if err != nil { + return err + } + if len(envelope.Signatures) == 0 { + return fmt.Errorf("envelope has no signatures") + } + envelopeSigBytes, err := base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig) + if err != nil { + return err + } + + timestampBytes, err = tsa.GetTimestampedSignature(envelopeSigBytes, client.NewTSAClient(c.TSAServerURL)) + if err != nil { + return err + } + } else { + timestampBytes, err = tsa.GetTimestampedSignature(sig, client.NewTSAClient(c.TSAServerURL)) + if err != nil { + return err + } } rfc3161Timestamp = cbundle.TimestampToRFC3161Timestamp(timestampBytes) // TODO: Consider uploading RFC3161 TS to Rekor diff --git a/test/e2e_test.go b/test/e2e_test.go index f0e36328b6f..598d7faa5c0 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -68,6 +68,7 @@ import ( "github.com/sigstore/cosign/v2/pkg/cosign/kubernetes" "github.com/sigstore/cosign/v2/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/v2/pkg/oci/remote" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/signature/payload" tsaclient "github.com/sigstore/timestamp-authority/pkg/client" "github.com/sigstore/timestamp-authority/pkg/server" @@ -859,6 +860,114 @@ func TestAttestationRFC3161Timestamp(t *testing.T) { must(verifyAttestation.Exec(ctx, []string{imgName}), t) } +func TestAttestationBlobRFC3161Timestamp(t *testing.T) { + // TSA server needed to create timestamp + viper.Set("timestamp-signer", "memory") + viper.Set("timestamp-signer-hash", "sha256") + apiServer := server.NewRestAPIServer("localhost", 0, []string{"http"}, false, 10*time.Second, 10*time.Second) + server := httptest.NewServer(apiServer.GetHandler()) + t.Cleanup(server.Close) + + blob := "someblob" + predicate := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }` + predicateType := "slsaprovenance" + + td := t.TempDir() + t.Cleanup(func() { + os.RemoveAll(td) + }) + + bp := filepath.Join(td, blob) + if err := os.WriteFile(bp, []byte(blob), 0600); err != nil { + t.Fatal(err) + } + + predicatePath := filepath.Join(td, "predicate") + if err := os.WriteFile(predicatePath, []byte(predicate), 0600); err != nil { + t.Fatal(err) + } + + bundlePath := filepath.Join(td, "bundle.sigstore.json") + _, privKeyPath, pubKeyPath := keypair(t, td) + + ctx := context.Background() + ko := options.KeyOpts{ + KeyRef: privKeyPath, + BundlePath: bundlePath, + NewBundleFormat: true, + TSAServerURL: server.URL + "/api/v1/timestamp", + PassFunc: passFunc, + } + + attestBlobCmd := attest.AttestBlobCommand{ + KeyOpts: ko, + PredicatePath: predicatePath, + PredicateType: predicateType, + Timeout: 30 * time.Second, + TlogUpload: false, + RekorEntryType: "dsse", + } + must(attestBlobCmd.Exec(ctx, bp), t) + + client, err := tsaclient.GetTimestampClient(server.URL) + if err != nil { + t.Error(err) + } + + chain, err := client.Timestamp.GetTimestampCertChain(nil) + if err != nil { + t.Fatalf("unexpected error getting timestamp chain: %v", err) + } + + var certs []*x509.Certificate + for block, contents := pem.Decode([]byte(chain.Payload)); ; block, contents = pem.Decode(contents) { + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Error(err) + } + certs = append(certs, cert) + + if len(contents) == 0 { + break + } + } + + tsaCA := root.CertificateAuthority{ + Root: certs[len(certs)-1], + Intermediates: certs[:len(certs)-1], + } + + trustedRoot, err := root.NewTrustedRoot(root.TrustedRootMediaType01, nil, nil, []root.CertificateAuthority{tsaCA}, nil) + if err != nil { + t.Error(err) + } + + trustedRootPath := filepath.Join(td, "trustedroot.json") + trustedRootBytes, err := trustedRoot.MarshalJSON() + if err != nil { + t.Error(err) + } + if err := os.WriteFile(trustedRootPath, trustedRootBytes, 0600); err != nil { + t.Fatal(err) + } + + ko = options.KeyOpts{ + KeyRef: pubKeyPath, + BundlePath: bundlePath, + NewBundleFormat: true, + } + + verifyBlobAttestation := cliverify.VerifyBlobAttestationCommand{ + KeyOpts: ko, + PredicateType: predicateType, + IgnoreTlog: true, + CheckClaims: true, + TrustedRootPath: trustedRootPath, + } + + must(verifyBlobAttestation.Exec(ctx, bp), t) +} + func TestVerifyWithCARoots(t *testing.T) { ctx := context.Background() // TSA server needed to create timestamp