Skip to content

Commit 6498df4

Browse files
authored
Merge pull request #2948 from st3penta/refactor-acceptance-signature
Refactor image signature in acceptance tests
2 parents ac60d64 + 5387e73 commit 6498df4

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)