From eab2facf4ea47e008d316a6ccea96307f86a0b00 Mon Sep 17 00:00:00 2001 From: Simon Baird Date: Mon, 2 Mar 2026 18:16:20 -0500 Subject: [PATCH 1/2] Include some v3 bundle signature detail in output If we compare the output for a cosign v2 signed image versus a cosign v3 signed image, there is a lot of detail missing. I'm expecting we will be able to extract the signature info from the bundle, but for now Claude and I are having some difficulty. I think we'll need to gain a better understanding of the way the new sigstore bundles work and try again in the future. For now I think it's okay if we just show the sig field and add some todos in the code. I suspect no one is actually consuming this data, so it won't have an immediate impact if it's missing. Ref: https://issues.redhat.com/browse/EC-1690 Co-authored-by: Claude Code --- .../__snapshots__/task_validate_image.snap | 4 +- .../application_snapshot_image.go | 84 +++++++++++++++++-- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/features/__snapshots__/task_validate_image.snap b/features/__snapshots__/task_validate_image.snap index 91a7a9e69..4cfa7fe45 100755 --- a/features/__snapshots__/task_validate_image.snap +++ b/features/__snapshots__/task_validate_image.snap @@ -251,11 +251,11 @@ true "signatures": [ { "keyid": "", - "sig": "" + "sig": "MEQCIDj5l7I0bPCua+H1ZfAAUnd4Hd4k7wUUEi/lpWYSLkOFAiBGgK9KWiNR1t+C4TbmkU/vnpHonmg5hNnwLRC70xc2Rg==" }, { "keyid": "", - "sig": "" + "sig": "MEUCIBZc+dmgTn8SCx30h9yvCOjsBwj1+aZX0gW53c7TeyuSAiEAp4zWGNHMrjql9NFl/fCmFXnJkgDkOqbN5n7H7mw6aqI=" } ], "attestations": [ diff --git a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go index 3149d6839..7627ffce3 100644 --- a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go +++ b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go @@ -157,11 +157,19 @@ func (a *ApplicationSnapshotImage) FetchImageFiles(ctx context.Context) error { func (a *ApplicationSnapshotImage) ValidateImageSignature(ctx context.Context) error { opts := a.checkOpts client := oci.NewClient(ctx) + useBundles := a.hasBundles(ctx) var sigs []cosignOCI.Signature var err error - if a.hasBundles(ctx) { + if useBundles { + // For v3 bundles, both image signatures and attestations are stored as + // "attestations" in the unified bundle format. So we use VerifyImageAttestations + // to extract image signatures from the bundle, even though it seems unintuitive. + // This is different from v2 where image signatures and attestations were separate. + // + // The certificate extraction requires different handling for bundles and + // should be addressed in future work to achieve full v2 parity. opts.NewBundleFormat = true opts.ClaimVerifier = cosign.IntotoSubjectClaimVerifier sigs, _, err = client.VerifyImageAttestations(a.reference, &opts) @@ -174,11 +182,24 @@ func (a *ApplicationSnapshotImage) ValidateImageSignature(ctx context.Context) e } for _, s := range sigs { - es, err := signature.NewEntitySignature(s) - if err != nil { - return err + if useBundles { + // For bundle image signatures produced by cosign v3, the old + // method of accessing the signatures doesn't work. Instead we have + // to extract them from the bundle. And the bundle actually has + // signatures for both the image itself and the attestation. + signatures, err := extractSignaturesFromBundle(s) + if err != nil { + return fmt.Errorf("cannot extract signatures from bundle: %w", err) + } + a.signatures = append(a.signatures, signatures...) + } else { + // For older non-bundle image signatures produced by cosign v2 + es, err := signature.NewEntitySignature(s) + if err != nil { + return err + } + a.signatures = append(a.signatures, es) } - a.signatures = append(a.signatures, es) } return nil } @@ -461,7 +482,7 @@ func (a *ApplicationSnapshotImage) WriteInputFile(ctx context.Context) (string, log.Debugf("Created dir %s", inputDir) inputJSONPath := path.Join(inputDir, "input.json") - f, err := fs.OpenFile(inputJSONPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0644) + f, err := fs.OpenFile(inputJSONPath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o644) if err != nil { log.Debugf("Problem creating file in %s", inputDir) return "", nil, err @@ -480,3 +501,54 @@ func (a *ApplicationSnapshotImage) WriteInputFile(ctx context.Context) (string, log.Debugf("Done preparing input file:\n%s", inputJSONPath) return inputJSONPath, inputJSON, nil } + +// extractSignaturesFromBundle extracts signature information from a bundle +// image signature attestation, using the same pattern as createEntitySignatures. +// +// TODO: This currently only extracts the signature value from the DSSE envelope. +// Certificate information (keyid, certificate, chain, metadata) is not being +// extracted because it requires different handling for v3 bundles compared to v2. +// Future work should investigate how to access certificate data from bundle +// attestations to achieve full parity with v2 signature output. Also, there might +// be some cosign methods we can use instead of doing it ourselves here. +func extractSignaturesFromBundle(sig cosignOCI.Signature) ([]signature.EntitySignature, error) { + reader, err := sig.Uncompressed() + if err != nil { + return nil, fmt.Errorf("cannot read signature data: %w", err) + } + defer reader.Close() + + var attestationPayload cosign.AttestationPayload + if err := json.NewDecoder(reader).Decode(&attestationPayload); err != nil { + return nil, fmt.Errorf("cannot parse DSSE envelope: %w", err) + } + + // Create the base EntitySignature from the oci.Signature (for certificate info) + es, err := signature.NewEntitySignature(sig) + if err != nil { + return nil, fmt.Errorf("cannot create base signature: %w", err) + } + + var results []signature.EntitySignature + for _, s := range attestationPayload.Signatures { + esNew := es + // The Signature and KeyID can come from two locations, the oci.Signature or + // the cosign.Signature. Prioritize information from oci.Signature, but fallback + // to cosign.Signature when needed (same pattern as createEntitySignatures) + // + // Todo: Actually the above comment might be stale and/or wrong since I belive + // it was copied from similar code in internal/attestation/attestation.go. Let's + // review this later. As far as I can tell this code always produces an empty + // string for the KeyId. + // + if esNew.Signature == "" { + esNew.Signature = s.Sig + } + if esNew.KeyID == "" { + esNew.KeyID = s.KeyID + } + results = append(results, esNew) + } + + return results, nil +} From 8e59a62e8fa673e40b30b831238a8082827eecb3 Mon Sep 17 00:00:00 2001 From: Simon Baird Date: Mon, 2 Mar 2026 15:46:03 -0500 Subject: [PATCH 2/2] Filter v3 attestations and sigs in output In v3 signature bundles, the signature is actually just another type of attestation. So when we list attestations (in v3) we see both the sig and the provenance. Similarly, when we list the image signatures we see the image signature and the attestation signature. In the detailed output for ec validate image we show details about the signatures and attestations. This changes attempts to avoid listing provenance atts in the image sig list, and imag sigs in the provenance att list. As mentioned in the comments, there might be other ways to do this. Ref: https://issues.redhat.com/browse/EC-1690 Co-authored-by: Claude Code --- .../__snapshots__/task_validate_image.snap | 14 ---- .../application_snapshot_image.go | 71 ++++++++++++++++++- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/features/__snapshots__/task_validate_image.snap b/features/__snapshots__/task_validate_image.snap index 4cfa7fe45..0623f8761 100755 --- a/features/__snapshots__/task_validate_image.snap +++ b/features/__snapshots__/task_validate_image.snap @@ -249,10 +249,6 @@ true ], "success": true, "signatures": [ - { - "keyid": "", - "sig": "MEQCIDj5l7I0bPCua+H1ZfAAUnd4Hd4k7wUUEi/lpWYSLkOFAiBGgK9KWiNR1t+C4TbmkU/vnpHonmg5hNnwLRC70xc2Rg==" - }, { "keyid": "", "sig": "MEUCIBZc+dmgTn8SCx30h9yvCOjsBwj1+aZX0gW53c7TeyuSAiEAp4zWGNHMrjql9NFl/fCmFXnJkgDkOqbN5n7H7mw6aqI=" @@ -268,16 +264,6 @@ true "sig": "MEUCIQC5bGm4zzbExXBMrZCmqZ98iqUhi8TV/maq/8dJ/c3POAIgCNw+RkeO7PAkT6JDWIvISZ2AjILu9YuPQ0qqfNwCqug=" } ] - }, - { - "type": "https://in-toto.io/Statement/v0.1", - "predicateType": "https://sigstore.dev/cosign/sign/v1", - "signatures": [ - { - "keyid": "", - "sig": "MEUCID1cJkxyk1oGvXcoAVkDST9A1vfX2gxPEz+LUzN10nDmAiEAxh9rp79yr4fZmAWWOit0dZ5QWK+uYIU8fQVb0/rLIyM=" - } - ] } ] } diff --git a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go index 7627ffce3..9bac0dc87 100644 --- a/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go +++ b/internal/evaluation_target/application_snapshot_image/application_snapshot_image.go @@ -183,6 +183,17 @@ func (a *ApplicationSnapshotImage) ValidateImageSignature(ctx context.Context) e for _, s := range sigs { if useBundles { + // This will appears in the output under "signatures" so filter out + // the sigs that are provenance attestations leaving only the sigs + // that are image signatures. Note: This does seems confusing and + // I'm not 100% sure we're doing the right thing here. Maybe revisit + // once we have a better idea about sigstore bundles and how they're + // handled by cosign itself. + if !isImageSignatureAttestation(s) { + log.Debugf("Skipping non-image signature attestation") + continue + } + // For bundle image signatures produced by cosign v3, the old // method of accessing the signatures doesn't work. Instead we have // to extract them from the bundle. And the bundle actually has @@ -193,7 +204,8 @@ func (a *ApplicationSnapshotImage) ValidateImageSignature(ctx context.Context) e } a.signatures = append(a.signatures, signatures...) } else { - // For older non-bundle image signatures produced by cosign v2 + // For older non-bundle image signatures produced by cosign v2. + // Note that filtering isn't needed, since we have only image sigs here. es, err := signature.NewEntitySignature(s) if err != nil { return err @@ -220,6 +232,11 @@ func (a *ApplicationSnapshotImage) ValidateAttestationSignature(ctx context.Cont return err } + // Todo: + // - For the non-bundle code path we actually check the syntax. + // We should do that for bundles as well probably. + // - Doing an early return like thi shere seems untidy, refactor + // maybe? if useBundles { return a.parseAttestationsFromBundles(layers) } @@ -272,8 +289,19 @@ func (a *ApplicationSnapshotImage) ValidateAttestationSignature(ctx context.Cont // parseAttestationsFromBundles extracts attestations from Sigstore bundles. // Bundle-wrapped layers report an incorrect media type, so we unmarshal the // DSSE envelope from the raw payload directly. +// +// Note: For v3 bundles, this function filters out image signature attestations +// (https://sigstore.dev/cosign/sign/v1) since those are handled in ValidateImageSignature. +// Only provenance and other attestations are added to the attestations array. func (a *ApplicationSnapshotImage) parseAttestationsFromBundles(layers []cosignOCI.Signature) error { for _, sig := range layers { + // For v3 bundles, filter out image signature attestations - those are handled + // in ValidateImageSignature. Only add provenance attestations here. + if isImageSignatureAttestation(sig) { + log.Debugf("Skipping image signature attestation - handled in ValidateImageSignature") + continue + } + payload, err := sig.Payload() if err != nil { log.Debugf("Skipping bundle entry: cannot read payload: %v", err) @@ -296,8 +324,8 @@ func (a *ApplicationSnapshotImage) parseAttestationsFromBundles(layers []cosignO if err != nil { return fmt.Errorf("unable to parse bundle attestation: %w", err) } - t := att.PredicateType() - log.Debugf("Found bundle attestation with predicateType: %s", t) + log.Debugf("Found bundle attestation with predicateType: %s", att.PredicateType()) + a.attestations = append(a.attestations, att) } return nil @@ -502,6 +530,43 @@ func (a *ApplicationSnapshotImage) WriteInputFile(ctx context.Context) (string, return inputJSONPath, inputJSON, nil } +// extractPredicateType extracts the predicateType field from a JSON +// payload lazily, i.e. without unmarshalling all the other fields +func extractPredicateType(payload []byte) (string, error) { + var attestation struct { + PredicateType string `json:"predicateType"` + } + if err := json.Unmarshal(payload, &attestation); err != nil { + return "", err + } + return attestation.PredicateType, nil +} + +// hasPredicateType checks if a JSON payload has the specified predicate type. +func hasPredicateType(payload []byte, expectedType string) bool { + predicateType, err := extractPredicateType(payload) + if err != nil { + log.Debugf("Cannot parse JSON payload: %v", err) + return false + } + return predicateType == expectedType +} + +const cosignSignPredicateType = "https://sigstore.dev/cosign/sign/v1" + +// isImageSignatureAttestation checks if a signature from a bundle represents +// an image signature attestation (vs. a provenance attestation). +func isImageSignatureAttestation(sig cosignOCI.Signature) bool { + payload, err := sig.Payload() + if err != nil { + log.Debugf("Cannot read signature payload: %v", err) + return false + } + + // Image signature attestations use the cosign/sign predicate type + return hasPredicateType(payload, cosignSignPredicateType) +} + // extractSignaturesFromBundle extracts signature information from a bundle // image signature attestation, using the same pattern as createEntitySignatures. //