Skip to content

Commit ea3bd77

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 <14293929+falcorocks@users.noreply.github.com>
1 parent 9b10de4 commit ea3bd77

File tree

2 files changed

+239
-2
lines changed

2 files changed

+239
-2
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: 238 additions & 2 deletions
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"
@@ -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 its 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.
10161069
func 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+
16211857
func 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

Comments
 (0)