@@ -28,6 +28,7 @@ import (
2828 "encoding/pem"
2929 "errors"
3030 "fmt"
31+ "io"
3132 "io/fs"
3233 "log"
3334 "net/http"
@@ -139,9 +140,11 @@ type CheckOpts struct {
139140 // It is a map from log id to LogIDMetadata. It is a map from LogID to crypto.PublicKey. LogID is derived from the PublicKey (see RFC 6962 S3.2).
140141 CTLogPubKeys * TrustedTransparencyLogPubKeys
141142
142- // 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).
143+ // 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
@@ -303,10 +306,12 @@ func verifyOCIAttestation(ctx context.Context, verifier signature.Verifier, att
303306 fmt .Errorf ("invalid payloadType %s on envelope. Expected %s" , env .PayloadType , types .IntotoPayloadType ),
304307 }
305308 }
309+
306310 dssev , err := ssldsse .NewEnvelopeVerifier (& dsse.VerifierAdapter {SignatureVerifier : verifier })
307311 if err != nil {
308312 return err
309313 }
314+
310315 _ , err = dssev .Verify (ctx , & env )
311316 return err
312317}
@@ -1011,13 +1016,70 @@ func loadSignatureFromFile(ctx context.Context, sigRef string, signedImgRef name
10111016 }, nil
10121017}
10131018
1019+ // loadAttestationFromFile loads an attestation from a file or URL, similar to loadSignatureFromFile.
1020+ // This is used when AttestationRef is specified in CheckOpts for experimental OCI 1.1 verification.
1021+ func loadAttestationFromFile (ctx context.Context , attRef string , signedImgRef name.Reference , co * CheckOpts ) (oci.Signatures , error ) {
1022+ var b64att string
1023+ targetAtt , err := blob .LoadFileOrURL (attRef )
1024+ if err != nil {
1025+ if ! errors .Is (err , fs .ErrNotExist ) {
1026+ return nil , err
1027+ }
1028+ targetAtt = []byte (attRef )
1029+ }
1030+
1031+ _ , err = base64 .StdEncoding .DecodeString (string (targetAtt ))
1032+
1033+ if err == nil {
1034+ b64att = string (targetAtt )
1035+ } else {
1036+ b64att = base64 .StdEncoding .EncodeToString (targetAtt )
1037+ }
1038+
1039+ var payload []byte
1040+ if co .PayloadRef != "" {
1041+ payload , err = blob .LoadFileOrURL (co .PayloadRef )
1042+ if err != nil {
1043+ return nil , err
1044+ }
1045+ } else {
1046+ digest , err := ociremote .ResolveDigest (signedImgRef , co .RegistryClientOpts ... )
1047+ if err != nil {
1048+ return nil , err
1049+ }
1050+ payload , err = ObsoletePayload (ctx , digest )
1051+ if err != nil {
1052+ return nil , err
1053+ }
1054+ }
1055+
1056+ att , err := static .NewSignature (payload , b64att )
1057+ if err != nil {
1058+ return nil , err
1059+ }
1060+ return & fakeOCISignatures {
1061+ signatures : []oci.Signature {att },
1062+ }, nil
1063+ }
1064+
10141065// VerifyImageAttestations does all the main cosign checks in a loop, returning the verified attestations.
10151066// If there were no valid attestations, we return an error.
1067+ // Note that if co.ExperimentalOCI11 is set, we will attempt to verify
1068+ // attestations using the experimental OCI 1.1 behavior.
10161069func VerifyImageAttestations (ctx context.Context , signedImgRef name.Reference , co * CheckOpts ) (checkedAttestations []oci.Signature , bundleVerified bool , err error ) {
10171070 // Enforce this up front.
10181071 if co .RootCerts == nil && co .SigVerifier == nil && co .TrustedMaterial == nil {
10191072 return nil , false , errors .New ("one of verifier, root certs, or TrustedMaterial is required" )
10201073 }
1074+
1075+ // Try first using OCI 1.1 behavior if experimental flag is set.
1076+ if co .ExperimentalOCI11 {
1077+ verified , bundleVerified , err := verifyImageAttestationsExperimentalOCI (ctx , signedImgRef , co )
1078+ if err == nil {
1079+ return verified , bundleVerified , nil
1080+ }
1081+ }
1082+
10211083 if co .NewBundleFormat {
10221084 return verifyImageAttestationsSigstoreBundle (ctx , signedImgRef , co )
10231085 }
@@ -1618,6 +1680,180 @@ func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name
16181680 return verifySignatures (ctx , sigs , h , co )
16191681}
16201682
1683+ // verifyImageAttestationsExperimentalOCI verifies attestations using OCI 1.1+ Referrers API for discovery.
1684+ // This function discovers attestations using the OCI 1.1 Referrers API instead of legacy tag-based discovery,
1685+ // then verifies the DSSE envelopes directly. It supports both keyless and non-keyless (KMS) authorities.
1686+ // If there were no valid attestations, we return an error.
1687+ func verifyImageAttestationsExperimentalOCI (ctx context.Context , signedImgRef name.Reference , co * CheckOpts ) (checkedAttestations []oci.Signature , bundleVerified bool , err error ) {
1688+ // Enforce this up front.
1689+ if co .RootCerts == nil && co .SigVerifier == nil && co .TrustedMaterial == nil {
1690+ return nil , false , errors .New ("one of verifier, root certs, or trusted root is required" )
1691+ }
1692+
1693+ // This is a carefully optimized sequence for fetching the attestations of the
1694+ // entity that minimizes registry requests when supplied with a digest input
1695+ digest , err := ociremote .ResolveDigest (signedImgRef , co .RegistryClientOpts ... )
1696+ if err != nil {
1697+ return nil , false , err
1698+ }
1699+ // We don't need the hash for direct DSSE verification
1700+
1701+ var allAttestations []oci.Signature
1702+
1703+ if co .AttestationRef == "" {
1704+ // Use OCI 1.1 Referrers API to find attestations
1705+ // Get all referrers and filter for attestation types
1706+ index , err := ociremote .Referrers (digest , "" , co .RegistryClientOpts ... )
1707+ if err != nil {
1708+ return nil , false , err
1709+ }
1710+
1711+ // Filter for attestation artifact types (in-toto related)
1712+ var attestationResults []v1.Descriptor
1713+ for _ , manifest := range index .Manifests {
1714+ if strings .Contains (manifest .ArtifactType , "in-toto" ) {
1715+ attestationResults = append (attestationResults , manifest )
1716+ }
1717+ }
1718+
1719+ numResults := len (attestationResults )
1720+ if numResults == 0 {
1721+ return nil , false , fmt .Errorf ("unable to locate attestation references" )
1722+ } else if numResults > 1 {
1723+ // TODO: if there is more than 1 result.. what does that even mean?
1724+ ui .Warnf (ctx , "there were a total of %d attestation references\n " , numResults )
1725+ }
1726+
1727+ // Parse OCI 1.1 attestations from all found references
1728+ for _ , result := range attestationResults {
1729+ // Get the attestation manifest
1730+ attRef , err := name .ParseReference (fmt .Sprintf ("%s@%s" , digest .Repository , result .Digest .String ()))
1731+ if err != nil {
1732+ return nil , false , err
1733+ }
1734+
1735+ // For OCI 1.1 attestations, we need to read the DSSE envelope from the layer
1736+ // and create a proper signature with it
1737+
1738+ // Get the signed image to access layers containing DSSE envelope
1739+ signedImg , err := ociremote .SignedImage (attRef , co .RegistryClientOpts ... )
1740+ if err != nil {
1741+ continue
1742+ }
1743+
1744+ // Get the layers (should contain the DSSE envelope)
1745+ layers , err := signedImg .Layers ()
1746+ if err != nil {
1747+ continue
1748+ }
1749+
1750+ // Read the DSSE envelope from the first layer
1751+ if len (layers ) == 0 {
1752+ continue
1753+ }
1754+
1755+ layer := layers [0 ] // Attestations typically have one layer with the DSSE envelope
1756+ rc , err := layer .Uncompressed ()
1757+ if err != nil {
1758+ continue
1759+ }
1760+
1761+ dsseEnvelope , err := io .ReadAll (rc )
1762+ rc .Close () // Close immediately after reading
1763+ if err != nil {
1764+ continue
1765+ }
1766+
1767+ // Parse the DSSE envelope to extract payload and signature
1768+ var envelope struct {
1769+ Payload string `json:"payload"`
1770+ PayloadType string `json:"payloadType"`
1771+ Signatures []struct {
1772+ Keyid string `json:"keyid"`
1773+ Sig string `json:"sig"`
1774+ } `json:"signatures"`
1775+ }
1776+
1777+ if err := json .Unmarshal (dsseEnvelope , & envelope ); err != nil {
1778+ continue
1779+ }
1780+
1781+ // Fix the payloadType if it's empty - this is required for verification
1782+ if envelope .PayloadType == "" {
1783+ envelope .PayloadType = types .IntotoPayloadType
1784+
1785+ // Re-marshal the envelope with the correct payloadType
1786+ dsseEnvelope , err = json .Marshal (envelope )
1787+ if err != nil {
1788+ continue
1789+ }
1790+ }
1791+
1792+ if len (envelope .Signatures ) == 0 {
1793+ continue
1794+ }
1795+
1796+ // Follow cosign's existing pattern: reject multiple signatures
1797+ // This is consistent with how cosign handles DSSE envelopes elsewhere
1798+ if len (envelope .Signatures ) > 1 {
1799+ continue // Skip attestations with multiple signatures for now
1800+ }
1801+
1802+ // Use the single signature
1803+ signature := envelope .Signatures [0 ]
1804+
1805+ // Create annotations with the required signature annotation
1806+ annotations := map [string ]string {
1807+ "dev.cosignproject.cosign/signature" : signature .Sig ,
1808+ }
1809+
1810+ // Create a signature with the DSSE envelope as-is
1811+ sig , err := static .NewSignature (dsseEnvelope , signature .Sig , static .WithAnnotations (annotations ))
1812+ if err != nil {
1813+ continue
1814+ }
1815+
1816+ allAttestations = append (allAttestations , sig )
1817+ }
1818+
1819+ if len (allAttestations ) == 0 {
1820+ return nil , false , fmt .Errorf ("no attestation layers found in OCI 1.1 references" )
1821+ }
1822+
1823+ } else {
1824+ if co .PayloadRef == "" {
1825+ return nil , false , errors .New ("payload is required with a manually-provided attestation" )
1826+ }
1827+ // For file-based attestations, use the existing logic
1828+ atts , err := loadAttestationFromFile (ctx , co .AttestationRef , signedImgRef , co )
1829+ if err != nil {
1830+ return nil , false , err
1831+ }
1832+ fileAtts , err := atts .Get ()
1833+ if err != nil {
1834+ return nil , false , err
1835+ }
1836+ allAttestations = append (allAttestations , fileAtts ... )
1837+ }
1838+
1839+ // For OCI 1.1 attestations, verify DSSE envelopes directly
1840+ // instead of going through the legacy verification pipeline
1841+ var verifiedAttestations []oci.Signature
1842+ for _ , att := range allAttestations {
1843+ // Verify the DSSE envelope directly using the provided verifier
1844+ if err := verifyOCIAttestation (ctx , co .SigVerifier , att ); err != nil {
1845+ continue
1846+ }
1847+ verifiedAttestations = append (verifiedAttestations , att )
1848+ }
1849+
1850+ if len (verifiedAttestations ) == 0 {
1851+ return nil , false , fmt .Errorf ("no OCI 1.1 attestations passed verification" )
1852+ }
1853+
1854+ return verifiedAttestations , false , nil
1855+ }
1856+
16211857func GetBundles (_ context.Context , signedImgRef name.Reference , co * CheckOpts ) ([]* sgbundle.Bundle , * v1.Hash , error ) {
16221858 // This is a carefully optimized sequence for fetching the signatures of the
16231859 // entity that minimizes registry requests when supplied with a digest input
0 commit comments