@@ -28,6 +28,7 @@ import (
2828 "encoding/pem"
2929 "errors"
3030 "fmt"
31+ "io"
3132 "io/fs"
3233 "log"
3334 "net/http"
@@ -141,7 +142,9 @@ type CheckOpts struct {
141142
142143 // SignatureRef is the reference to the signature file. PayloadRef should always be specified as well (though it’s possible for a _some_ signatures to be verified without it, with a warning).
143144 SignatureRef string
144- // PayloadRef is a reference to the payload file. Applicable only if SignatureRef is set.
145+ // AttestationRef is the reference to the attestation file for experimental OCI 1.1 verification. PayloadRef should always be specified as well.
146+ AttestationRef string
147+ // PayloadRef is a reference to the payload file. Applicable only if SignatureRef or AttestationRef is set.
145148 PayloadRef string
146149
147150 // Identities is an array of Identity (Subject, Issuer) matchers that have
@@ -610,6 +613,96 @@ func (fos *fakeOCISignatures) Get() ([]oci.Signature, error) {
610613 return fos .signatures , nil
611614}
612615
616+ // processOCI11AttestationRef processes a single OCI 1.1 attestation reference
617+ // and returns the oci.Signature objects contained within it.
618+ func processOCI11AttestationRef (result v1.Descriptor , repository name.Repository , registryOpts []ociremote.Option ) ([]oci.Signature , error ) {
619+ // Get the attestation manifest
620+ attRef , err := name .ParseReference (fmt .Sprintf ("%s@%s" , repository , result .Digest .String ()))
621+ if err != nil {
622+ return nil , err
623+ }
624+
625+ // Get the signed image to access layers containing DSSE envelope
626+ signedImg , err := ociremote .SignedImage (attRef , registryOpts ... )
627+ if err != nil {
628+ return nil , err
629+ }
630+
631+ // Get the layers (should contain the DSSE envelope)
632+ layers , err := signedImg .Layers ()
633+ if err != nil {
634+ return nil , err
635+ }
636+
637+ // Read the DSSE envelope from the first layer
638+ if len (layers ) == 0 {
639+ return nil , errors .New ("no layers found" )
640+ }
641+
642+ layer := layers [0 ] // Attestations typically have one layer with the DSSE envelope
643+ rc , err := layer .Uncompressed ()
644+ if err != nil {
645+ return nil , err
646+ }
647+
648+ dsseEnvelope , err := io .ReadAll (rc )
649+ rc .Close () // Close immediately after reading
650+ if err != nil {
651+ return nil , err
652+ }
653+
654+ // Parse the DSSE envelope to extract payload and signature
655+ var envelope struct {
656+ Payload string `json:"payload"`
657+ PayloadType string `json:"payloadType"`
658+ Signatures []struct {
659+ Keyid string `json:"keyid"`
660+ Sig string `json:"sig"`
661+ } `json:"signatures"`
662+ }
663+
664+ if err := json .Unmarshal (dsseEnvelope , & envelope ); err != nil {
665+ return nil , err
666+ }
667+
668+ // Fix the payloadType if it's empty - this is required for verification
669+ if envelope .PayloadType == "" {
670+ envelope .PayloadType = types .IntotoPayloadType
671+
672+ // Re-marshal the envelope with the correct payloadType
673+ dsseEnvelope , err = json .Marshal (envelope )
674+ if err != nil {
675+ return nil , err
676+ }
677+ }
678+
679+ if len (envelope .Signatures ) == 0 {
680+ return nil , errors .New ("no signatures found" )
681+ }
682+
683+ // Follow cosign's existing pattern: reject multiple signatures
684+ // This is consistent with how cosign handles DSSE envelopes elsewhere
685+ if len (envelope .Signatures ) > 1 {
686+ return nil , errors .New ("multiple signatures not supported" )
687+ }
688+
689+ // Use the single signature
690+ signature := envelope .Signatures [0 ]
691+
692+ // Create annotations with the required signature annotation
693+ annotations := map [string ]string {
694+ "dev.cosignproject.cosign/signature" : signature .Sig ,
695+ }
696+
697+ // Create a signature with the DSSE envelope as-is
698+ sig , err := static .NewSignature (dsseEnvelope , signature .Sig , static .WithAnnotations (annotations ))
699+ if err != nil {
700+ return nil , err
701+ }
702+
703+ return []oci.Signature {sig }, nil
704+ }
705+
613706// VerifyImageSignatures does all the main cosign checks in a loop, returning the verified signatures.
614707// If there were no valid signatures, we return an error.
615708// Note that if co.ExperimentlOCI11 is set, we will attempt to verify
@@ -1011,13 +1104,68 @@ func loadSignatureFromFile(ctx context.Context, sigRef string, signedImgRef name
10111104 }, nil
10121105}
10131106
1107+ // loadAttestationFromFile loads an attestation from a file or URL, similar to loadSignatureFromFile.
1108+ // This is used when AttestationRef is specified in CheckOpts for experimental OCI 1.1 verification.
1109+ func loadAttestationFromFile (ctx context.Context , attRef string , signedImgRef name.Reference , co * CheckOpts ) (oci.Signatures , error ) {
1110+ var b64att string
1111+ targetAtt , err := blob .LoadFileOrURL (attRef )
1112+ if err != nil {
1113+ if ! errors .Is (err , fs .ErrNotExist ) {
1114+ return nil , err
1115+ }
1116+ targetAtt = []byte (attRef )
1117+ }
1118+
1119+ _ , err = base64 .StdEncoding .DecodeString (string (targetAtt ))
1120+
1121+ if err == nil {
1122+ b64att = string (targetAtt )
1123+ } else {
1124+ b64att = base64 .StdEncoding .EncodeToString (targetAtt )
1125+ }
1126+
1127+ var payload []byte
1128+ if co .PayloadRef != "" {
1129+ payload , err = blob .LoadFileOrURL (co .PayloadRef )
1130+ if err != nil {
1131+ return nil , err
1132+ }
1133+ } else {
1134+ digest , err := ociremote .ResolveDigest (signedImgRef , co .RegistryClientOpts ... )
1135+ if err != nil {
1136+ return nil , err
1137+ }
1138+ payload , err = ObsoletePayload (ctx , digest )
1139+ if err != nil {
1140+ return nil , err
1141+ }
1142+ }
1143+
1144+ att , err := static .NewSignature (payload , b64att )
1145+ if err != nil {
1146+ return nil , err
1147+ }
1148+ return & fakeOCISignatures {
1149+ signatures : []oci.Signature {att },
1150+ }, nil
1151+ }
1152+
10141153// VerifyImageAttestations does all the main cosign checks in a loop, returning the verified attestations.
10151154// If there were no valid attestations, we return an error.
10161155func VerifyImageAttestations (ctx context.Context , signedImgRef name.Reference , co * CheckOpts ) (checkedAttestations []oci.Signature , bundleVerified bool , err error ) {
10171156 // Enforce this up front.
10181157 if co .RootCerts == nil && co .SigVerifier == nil && co .TrustedMaterial == nil {
10191158 return nil , false , errors .New ("one of verifier, root certs, or TrustedMaterial is required" )
10201159 }
1160+
1161+ // Try first using OCI 1.1 behavior if experimental flag is set.
1162+ if co .ExperimentalOCI11 {
1163+ verified , bundleVerified , err := verifyImageAttestationsExperimentalOCI (ctx , signedImgRef , co )
1164+ if err == nil {
1165+ return verified , bundleVerified , nil
1166+ }
1167+ }
1168+
10211169 if co .NewBundleFormat {
10221170 return verifyImageAttestationsSigstoreBundle (ctx , signedImgRef , co )
10231171 }
@@ -1618,6 +1766,78 @@ func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name
16181766 return verifySignatures (ctx , sigs , h , co )
16191767}
16201768
1769+ // verifyImageAttestationsExperimentalOCI verifies attestations using OCI 1.1+ Referrers API for discovery.
1770+ // This function discovers attestations using the OCI 1.1 Referrers API instead of legacy tag-based discovery,
1771+ // then uses the existing verification pipeline.
1772+ func verifyImageAttestationsExperimentalOCI (ctx context.Context , signedImgRef name.Reference , co * CheckOpts ) (checkedAttestations []oci.Signature , bundleVerified bool , err error ) {
1773+ // This is a carefully optimized sequence for fetching the attestations of the
1774+ // entity that minimizes registry requests when supplied with a digest input
1775+ digest , err := ociremote .ResolveDigest (signedImgRef , co .RegistryClientOpts ... )
1776+ if err != nil {
1777+ return nil , false , err
1778+ }
1779+ h , err := v1 .NewHash (digest .Identifier ())
1780+ if err != nil {
1781+ return nil , false , err
1782+ }
1783+
1784+ var atts oci.Signatures
1785+
1786+ if co .AttestationRef == "" {
1787+ // Use OCI 1.1 Referrers API to find attestations instead of legacy .att tags
1788+ index , err := ociremote .Referrers (digest , "" , co .RegistryClientOpts ... )
1789+ if err != nil {
1790+ return nil , false , err
1791+ }
1792+
1793+ // Filter for attestation artifact types (in-toto related)
1794+ var attestationResults []v1.Descriptor
1795+ for _ , manifest := range index .Manifests {
1796+ if strings .Contains (manifest .ArtifactType , "in-toto" ) {
1797+ attestationResults = append (attestationResults , manifest )
1798+ }
1799+ }
1800+
1801+ numResults := len (attestationResults )
1802+ if numResults == 0 {
1803+ return nil , false , fmt .Errorf ("unable to locate attestation references" )
1804+ } else if numResults > 1 {
1805+ // TODO: if there is more than 1 result.. what does that even mean?
1806+ ui .Warnf (ctx , "there were a total of %d attestation references\n " , numResults )
1807+ }
1808+
1809+ // Process all OCI 1.1 attestation references and collect signatures
1810+ var allSigs []oci.Signature
1811+ for _ , result := range attestationResults {
1812+ sigs , err := processOCI11AttestationRef (result , digest .Repository , co .RegistryClientOpts )
1813+ if err != nil {
1814+ continue
1815+ }
1816+ allSigs = append (allSigs , sigs ... )
1817+ }
1818+
1819+ if len (allSigs ) == 0 {
1820+ return nil , false , fmt .Errorf ("no signatures found in OCI 1.1 attestation references" )
1821+ }
1822+
1823+ // Use the existing fakeOCISignatures wrapper
1824+ atts = & fakeOCISignatures {signatures : allSigs }
1825+ } else {
1826+ if co .PayloadRef == "" {
1827+ return nil , false , errors .New ("payload is required with a manually-provided attestation" )
1828+ }
1829+ // For file-based attestations, use the existing logic
1830+ atts , err = loadAttestationFromFile (ctx , co .AttestationRef , signedImgRef , co )
1831+ if err != nil {
1832+ return nil , false , err
1833+ }
1834+ }
1835+
1836+ // Use the existing verification pipeline - this handles all the DSSE parsing,
1837+ // signature verification, error handling, etc.
1838+ return VerifyImageAttestation (ctx , atts , h , co )
1839+ }
1840+
16211841func GetBundles (_ context.Context , signedImgRef name.Reference , co * CheckOpts ) ([]* sgbundle.Bundle , * v1.Hash , error ) {
16221842 // This is a carefully optimized sequence for fetching the signatures of the
16231843 // entity that minimizes registry requests when supplied with a digest input
0 commit comments