Skip to content

Commit e198e17

Browse files
committed
feat: Add experimental OCI 1.1 attestation verification support
The implementation discovers attestations using the OCI 1.1 Referrers API instead of legacy tag-based discovery (.att tags), then extracts and verifies DSSE envelopes directly. This enables verification of attestations stored using modern OCI 1.1 specification with any authority type. Signed-off-by: falcorocks <[email protected]>
1 parent 9b10de4 commit e198e17

File tree

2 files changed

+222
-1
lines changed

2 files changed

+222
-1
lines changed

cmd/cosign/cli/verify/verify_attestation.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e
118118
Offline: c.Offline,
119119
IgnoreTlog: c.IgnoreTlog,
120120
MaxWorkers: c.MaxWorkers,
121+
ExperimentalOCI11: c.ExperimentalOCI11,
121122
UseSignedTimestamps: c.TSACertChainPath != "" || c.UseSignedTimestamps,
122123
NewBundleFormat: c.NewBundleFormat,
123124
}

pkg/cosign/verify.go

Lines changed: 221 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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.
10161155
func 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+
16211841
func 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

Comments
 (0)