Skip to content

Commit 5387e73

Browse files
committed
Refactor rekor entries creation in acceptance tests
This commit refactors the functions 'CreateAndPushImageSignature' and 'createAndPushAttestationWithPatches' so that they now replicate the transparency log entry creation on the rekor stub. The signature and the attestation are now created using the cosign.TLogUpload function, and stubbing the rekor endpoints that get called during the tlog entry creation process. The result is a signature that can be successfully verified using the 'cosign verify' command, and an attestation that has a corresponding entry in rekor. This refactor also removed the need to explicitly create rekor entries in the acceptance tests, since this is now part of the cosign flow. The acceptance tests using this new rekor flow now reflect more accuratly the real-world scenario. Assisted by: Claude Code Ref: https://issues.redhat.com/browse/EC-1210
1 parent ac60d64 commit 5387e73

File tree

9 files changed

+545
-235
lines changed

9 files changed

+545
-235
lines changed

acceptance/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,11 @@ require (
134134
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
135135
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b // indirect
136136
github.com/hashicorp/errwrap v1.1.0 // indirect
137+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
137138
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
138139
github.com/hashicorp/go-memdb v1.3.4 // indirect
139140
github.com/hashicorp/go-multierror v1.1.1 // indirect
141+
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
140142
github.com/hashicorp/golang-lru v1.0.2 // indirect
141143
github.com/in-toto/attestation v1.1.0 // indirect
142144
github.com/inconshreveable/mousetrap v1.1.0 // indirect

acceptance/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,8 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
541541
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
542542
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
543543
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
544+
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
545+
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
544546
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
545547
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
546548
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=

acceptance/image/image.go

Lines changed: 208 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"archive/tar"
2323
"bytes"
2424
"context"
25+
"crypto/sha256"
2526
"crypto/x509"
2627
"encoding/base64"
2728
"encoding/hex"
@@ -46,17 +47,21 @@ import (
4647
"github.com/google/go-containerregistry/pkg/v1/types"
4748
"github.com/in-toto/in-toto-golang/in_toto"
4849
"github.com/sigstore/cosign/v2/pkg/cosign"
50+
"github.com/sigstore/cosign/v2/pkg/cosign/bundle"
4951
"github.com/sigstore/cosign/v2/pkg/oci"
5052
"github.com/sigstore/cosign/v2/pkg/oci/layout"
5153
cosignRemote "github.com/sigstore/cosign/v2/pkg/oci/remote"
5254
"github.com/sigstore/cosign/v2/pkg/oci/static"
5355
cosigntypes "github.com/sigstore/cosign/v2/pkg/types"
56+
rc "github.com/sigstore/rekor/pkg/client"
57+
"github.com/sigstore/sigstore/pkg/cryptoutils"
5458
"github.com/sigstore/sigstore/pkg/signature"
5559
"gopkg.in/go-jose/go-jose.v2/json"
5660

5761
"github.com/conforma/cli/acceptance/attestation"
5862
"github.com/conforma/cli/acceptance/crypto"
5963
"github.com/conforma/cli/acceptance/registry"
64+
"github.com/conforma/cli/acceptance/rekor"
6065
"github.com/conforma/cli/acceptance/testenv"
6166
)
6267

@@ -135,7 +140,7 @@ func imageFrom(ctx context.Context, imageName string) (v1.Image, error) {
135140
// CreateAndPushImageSignature for a named image in the Context creates a signature
136141
// image, same as `cosign sign` or Tekton Chains would, of that named image and pushes it
137142
// to the stub registry as a new tag for that image akin to how cosign and Tekton Chains
138-
// do it
143+
// do it. This implementation includes transparency log upload to generate bundle information.
139144
func CreateAndPushImageSignature(ctx context.Context, imageName string, keyName string) (context.Context, error) {
140145
var state *imageState
141146
ctx, err := testenv.SetupState(ctx, &state)
@@ -169,29 +174,101 @@ func CreateAndPushImageSignature(ctx context.Context, imageName string, keyName
169174
return ctx, err
170175
}
171176

172-
// creates a cosign signature payload signs it and provides the raw signature
173-
payload, signature, err := signature.SignImage(signer, digestImage, map[string]interface{}{})
177+
// Create the cosign signature payload and sign it
178+
payload, rawSignature, err := signature.SignImage(signer, digestImage, map[string]interface{}{})
174179
if err != nil {
175180
return ctx, err
176181
}
177182

178-
signatureBase64 := base64.StdEncoding.EncodeToString(signature)
183+
signatureBase64 := base64.StdEncoding.EncodeToString(rawSignature)
179184

180-
// creates the layer with the image signature
181-
signatureLayer, err := static.NewSignature(payload, signatureBase64)
185+
// Create the signature structure for the stub rekor entry
186+
signature := Signature{
187+
KeyID: "",
188+
Signature: signatureBase64,
189+
}
190+
191+
signatureJSON, err := json.Marshal(signature)
192+
if err != nil {
193+
return ctx, fmt.Errorf("failed to marshal signature structure: %w", err)
194+
}
195+
196+
// Get the public key from the signer for hashedrekord validation
197+
publicKey, err := signer.PublicKey()
198+
if err != nil {
199+
return ctx, fmt.Errorf("failed to get public key: %w", err)
200+
}
201+
202+
publicKeyBytes, err := cryptoutils.MarshalPublicKeyToPEM(publicKey)
203+
if err != nil {
204+
return ctx, fmt.Errorf("failed to marshal public key: %w", err)
205+
}
206+
207+
// Create stubs for both Rekor entry signature creation and retrieval endpoints
208+
err = rekor.StubRekorEntryCreationForSignature(ctx, payload, rawSignature, signatureJSON, publicKeyBytes)
209+
if err != nil {
210+
return ctx, fmt.Errorf("error stubbing rekor endpoints: %w", err)
211+
}
212+
213+
// Upload to transparency log to get bundle information like Tekton Chains does
214+
rekorURL, err := rekor.StubRekor(ctx)
215+
if err != nil {
216+
return ctx, fmt.Errorf("failed to get stub rekor URL: %w", err)
217+
}
218+
219+
rekorClient, err := rc.GetRekorClient(rekorURL)
220+
if err != nil {
221+
return ctx, fmt.Errorf("failed to get rekor client: %w", err)
222+
}
223+
224+
// Get public key or cert for transparency log upload
225+
pkoc, err := getPublicKeyOrCert(signer)
226+
if err != nil {
227+
return ctx, fmt.Errorf("failed to get public key or cert: %w", err)
228+
}
229+
230+
// Compute payload checksum
231+
checksum := sha256.New()
232+
if _, err := checksum.Write(payload); err != nil {
233+
return ctx, fmt.Errorf("error checksuming payload: %w", err)
234+
}
235+
236+
tlogEntry, err := cosign.TLogUpload(ctx, rekorClient, rawSignature, checksum, pkoc)
237+
if err != nil {
238+
return ctx, fmt.Errorf("failed to upload to transparency log: %w", err)
239+
}
240+
241+
// Create bundle from the actual transparency log entry
242+
rekorBundle := bundle.EntryToBundle(tlogEntry)
243+
if rekorBundle == nil {
244+
return ctx, fmt.Errorf("rekorBundle is nil after EntryToBundle")
245+
}
246+
247+
// Create the signature layer with bundle information using static.WithBundle
248+
signatureLayer, err := static.NewSignature(payload, signatureBase64, static.WithBundle(rekorBundle))
182249
if err != nil {
183250
return ctx, err
184251
}
185252

253+
// Extract bundle information from signatureLayer to include in annotations
254+
annotations := map[string]string{
255+
static.SignatureAnnotationKey: signatureBase64,
256+
}
257+
258+
// Add bundle annotation if bundle information exists
259+
bundleJSON, err := json.Marshal(rekorBundle)
260+
if err != nil {
261+
return ctx, fmt.Errorf("failed to marshal bundle for annotation: %w", err)
262+
}
263+
annotations[static.BundleAnnotationKey] = string(bundleJSON)
264+
186265
// creates the signature image with the correct media type and config and appends
187266
// the signature layer to it
188-
singnatureImage := mutate.MediaType(empty.Image, types.OCIManifestSchema1)
189-
singnatureImage = mutate.ConfigMediaType(singnatureImage, types.OCIConfigJSON)
190-
singnatureImage, err = mutate.Append(singnatureImage, mutate.Addendum{
191-
Layer: signatureLayer,
192-
Annotations: map[string]string{
193-
static.SignatureAnnotationKey: signatureBase64,
194-
},
267+
signatureImage := mutate.MediaType(empty.Image, types.OCIManifestSchema1)
268+
signatureImage = mutate.ConfigMediaType(signatureImage, types.OCIConfigJSON)
269+
signatureImage, err = mutate.Append(signatureImage, mutate.Addendum{
270+
Layer: signatureLayer,
271+
Annotations: annotations,
195272
})
196273
if err != nil {
197274
return ctx, err
@@ -204,16 +281,13 @@ func CreateAndPushImageSignature(ctx context.Context, imageName string, keyName
204281
}
205282

206283
// push to the registry
207-
err = remote.Write(ref, singnatureImage)
284+
err = remote.Write(ref, signatureImage)
208285
if err != nil {
209286
return ctx, err
210287
}
211288

212289
state.Signatures[imageName] = ref.String()
213-
state.ImageSignatures[imageName] = Signature{
214-
KeyID: "",
215-
Signature: signatureBase64,
216-
}
290+
state.ImageSignatures[imageName] = signature
217291

218292
return ctx, nil
219293
}
@@ -229,7 +303,8 @@ func CreateAndPushAttestation(ctx context.Context, imageName, keyName string) (c
229303
// image, same as `cosign attest` or Tekton Chains would, and pushes it to the stub
230304
// registry as a new tag for that image akin to how cosign and Tekton Chains do
231305
// it; this variant applies additional JSON Patch patches to the SLSA provenance
232-
// statement as required by the tests
306+
// statement as required by the tests. This implementation now includes transparency
307+
// log upload to generate bundle information like Tekton Chains does for attestations.
233308
func createAndPushAttestationWithPatches(ctx context.Context, imageName, keyName string, patches *godog.Table) (context.Context, error) {
234309
var state *imageState
235310
ctx, err := testenv.SetupState(ctx, &state)
@@ -267,34 +342,117 @@ func createAndPushAttestationWithPatches(ctx context.Context, imageName, keyName
267342
return ctx, err
268343
}
269344

270-
if sig, err := unmarshallSignatures(signedAttestation); err != nil {
345+
// Extract signature information from the signed attestation
346+
var sig *cosign.Signatures
347+
sig, err = unmarshallSignatures(signedAttestation)
348+
if err != nil {
271349
return ctx, err
272-
} else {
273-
state.AttestationSignatures[imageName] = Signature{
274-
KeyID: sig.KeyID,
275-
Signature: sig.Sig,
350+
}
351+
if sig == nil {
352+
return ctx, fmt.Errorf("failed to extract signature from attestation: no signatures found")
353+
}
354+
355+
state.AttestationSignatures[imageName] = Signature{
356+
KeyID: sig.KeyID,
357+
Signature: sig.Sig,
358+
}
359+
360+
// Extract raw signature from the signed attestation for transparency log upload
361+
var rawSignature []byte
362+
if sig.Sig != "" {
363+
rawSignature, err = base64.StdEncoding.DecodeString(sig.Sig)
364+
if err != nil {
365+
return ctx, fmt.Errorf("failed to decode signature: %w", err)
276366
}
277367
}
278368

279-
attestationLayer, err := static.NewAttestation(signedAttestation)
369+
// Get the signer for transparency log operations
370+
signer, err := crypto.SignerWithKey(ctx, keyName)
280371
if err != nil {
281372
return ctx, err
282373
}
283374

375+
// Get the public key from the signer for intoto validation
376+
publicKey, err := signer.PublicKey()
377+
if err != nil {
378+
return ctx, fmt.Errorf("failed to get public key: %w", err)
379+
}
380+
381+
publicKeyBytes, err := cryptoutils.MarshalPublicKeyToPEM(publicKey)
382+
if err != nil {
383+
return ctx, fmt.Errorf("failed to marshal public key: %w", err)
384+
}
385+
386+
// Create stubs for both Rekor entry creation and retrieval endpoints for attestations
387+
err = rekor.StubRekorEntryCreationForAttestation(ctx, signedAttestation, publicKeyBytes)
388+
if err != nil {
389+
return ctx, fmt.Errorf("error stubbing rekor endpoints for attestation: %w", err)
390+
}
391+
392+
// Upload to transparency log to get bundle information like Tekton Chains does
393+
rekorURL, err := rekor.StubRekor(ctx)
394+
if err != nil {
395+
return ctx, fmt.Errorf("failed to get stub rekor URL: %w", err)
396+
}
397+
398+
rekorClient, err := rc.GetRekorClient(rekorURL)
399+
if err != nil {
400+
return ctx, fmt.Errorf("failed to get rekor client: %w", err)
401+
}
402+
403+
// Get public key or cert for transparency log upload
404+
pkoc, err := getPublicKeyOrCert(signer)
405+
if err != nil {
406+
return ctx, fmt.Errorf("failed to get public key or cert: %w", err)
407+
}
408+
409+
// Compute payload checksum
410+
checksum := sha256.New()
411+
if _, err := checksum.Write(signedAttestation); err != nil {
412+
return ctx, fmt.Errorf("error checksuming attestation: %w", err)
413+
}
414+
415+
tlogEntry, err := cosign.TLogUpload(ctx, rekorClient, rawSignature, checksum, pkoc)
416+
if err != nil {
417+
return ctx, fmt.Errorf("failed to upload attestation to transparency log: %w", err)
418+
}
419+
420+
// Create bundle from the actual transparency log entry
421+
rekorBundle := bundle.EntryToBundle(tlogEntry)
422+
if rekorBundle == nil {
423+
return ctx, fmt.Errorf("rekorBundle is nil after EntryToBundle")
424+
}
425+
426+
// Create the attestation layer with bundle information using static.WithBundle
427+
attestationLayer, err := static.NewAttestation(signedAttestation, static.WithBundle(rekorBundle))
428+
if err != nil {
429+
return ctx, err
430+
}
431+
432+
// Extract bundle information from attestationLayer to include in annotations
433+
annotations := map[string]string{
434+
// When cosign creates an attestation, it sets this annotation to an empty
435+
// string, as seen here:
436+
// https://github.com/sigstore/cosign/blob/34afd5240ce8490a4fa427c3f46523246643047c/pkg/oci/static/signature.go#L52-L55
437+
// We choose to mimic the cosign behavior to avoid inconsistencies in the tests.
438+
static.SignatureAnnotationKey: "",
439+
}
440+
441+
// Add bundle annotation if bundle information exists
442+
bundleJSON, err := json.Marshal(rekorBundle)
443+
if err != nil {
444+
return ctx, fmt.Errorf("failed to marshal bundle for annotation: %w", err)
445+
}
446+
annotations[static.BundleAnnotationKey] = string(bundleJSON)
447+
284448
// creates the attestation image with the correct media type and config and appends
285449
// the attestation layer to it
286450
attestationImage := mutate.MediaType(empty.Image, types.OCIManifestSchema1)
287451
attestationImage = mutate.ConfigMediaType(attestationImage, types.OCIConfigJSON)
288452
attestationImage, err = mutate.Append(attestationImage, mutate.Addendum{
289-
MediaType: cosigntypes.DssePayloadType,
290-
Layer: attestationLayer,
291-
Annotations: map[string]string{
292-
// When cosign creates an attestation, it sets this annotation to an empty
293-
// string, as seen here:
294-
// https://github.com/sigstore/cosign/blob/34afd5240ce8490a4fa427c3f46523246643047c/pkg/oci/static/signature.go#L52-L55
295-
// We choose to mimic the cosign behavior to avoid inconsistencies in the tests.
296-
static.SignatureAnnotationKey: "",
297-
},
453+
MediaType: cosigntypes.DssePayloadType,
454+
Layer: attestationLayer,
455+
Annotations: annotations,
298456
})
299457
if err != nil {
300458
return ctx, err
@@ -862,6 +1020,22 @@ func RawImageSignaturesFrom(ctx context.Context) map[string]string {
8621020
return ret
8631021
}
8641022

1023+
// getPublicKeyOrCert returns the cert if we have it, otherwise return public key
1024+
// This mimics the same logic used in Tekton Chains
1025+
func getPublicKeyOrCert(signer signature.SignerVerifier) ([]byte, error) {
1026+
// For now, we'll always use the public key since we're using test keys
1027+
// In a real scenario with certificates, we'd check for cert first
1028+
pub, err := signer.PublicKey()
1029+
if err != nil {
1030+
return nil, fmt.Errorf("getting public key: %w", err)
1031+
}
1032+
pem, err := cryptoutils.MarshalPublicKeyToPEM(pub)
1033+
if err != nil {
1034+
return nil, fmt.Errorf("key to pem: %w", err)
1035+
}
1036+
return pem, nil
1037+
}
1038+
8651039
func applyPatches(statement *in_toto.ProvenanceStatementSLSA02, patches *godog.Table) (*in_toto.ProvenanceStatementSLSA02, error) {
8661040
if statement == nil || patches == nil || len(patches.Rows) == 0 {
8671041
return statement, nil

acceptance/kubernetes/kubernetes.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import (
3636
"github.com/conforma/cli/acceptance/kubernetes/stub"
3737
"github.com/conforma/cli/acceptance/kubernetes/types"
3838
"github.com/conforma/cli/acceptance/registry"
39-
"github.com/conforma/cli/acceptance/rekor"
4039
"github.com/conforma/cli/acceptance/snaps"
4140
"github.com/conforma/cli/acceptance/testenv"
4241
)
@@ -196,21 +195,11 @@ func createNamedSnapshotWithManyComponents(ctx context.Context, name string, amo
196195
return ctx, err
197196
}
198197

199-
err = rekor.RekorEntryForImageSignature(ctx, imageRef)
200-
if err != nil {
201-
return ctx, err
202-
}
203-
204198
ctx, err = image.CreateAndPushAttestation(ctx, imageRef, key)
205199
if err != nil {
206200
return ctx, err
207201
}
208202

209-
err = rekor.RekorEntryForAttestation(ctx, imageRef)
210-
if err != nil {
211-
return ctx, err
212-
}
213-
214203
components = append(components, fmt.Sprintf(`{"name": "component%d", "containerImage": "${REGISTRY}/%s"}`, i, imageRef))
215204
}
216205

0 commit comments

Comments
 (0)