Skip to content

Commit a285903

Browse files
committed
sigstore-bundle: add policy verification for sigstore bundle format
Signed-off-by: Robert Sturla <[email protected]>
1 parent 74557ec commit a285903

File tree

2 files changed

+301
-1
lines changed

2 files changed

+301
-1
lines changed

image/signature/policy_eval_sigstore.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,12 @@ func (pr *prSigstoreSigned) isSignatureAccepted(ctx context.Context, image priva
242242
return sarRejected, err
243243
}
244244

245+
// Check if this is a sigstore bundle format
246+
mimeType := sig.UntrustedMIMEType()
247+
if signature.IsSigstoreBundleMediaType(mimeType) {
248+
return pr.isSignatureAcceptedBundle(ctx, image, sig, trustRoot)
249+
}
250+
245251
untrustedAnnotations := sig.UntrustedAnnotations()
246252
untrustedBase64Signature, ok := untrustedAnnotations[signature.SigstoreSignatureAnnotationKey]
247253
if !ok {
@@ -395,7 +401,8 @@ func (pr *prSigstoreSigned) isRunningImageAllowed(ctx context.Context, image pri
395401
foundNonSigstoreSignatures++
396402
continue
397403
}
398-
if sigstoreSig.UntrustedMIMEType() != signature.SigstoreSignatureMIMEType {
404+
// Accept both legacy simple signing format and new Cosign v3 bundle format
405+
if !signature.IsSigstoreSignatureMediaType(sigstoreSig.UntrustedMIMEType()) {
399406
foundSigstoreNonAttachments++
400407
continue
401408
}
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
package signature
2+
3+
import (
4+
"context"
5+
"crypto"
6+
"errors"
7+
"fmt"
8+
9+
digest "github.com/opencontainers/go-digest"
10+
"go.podman.io/image/v5/internal/private"
11+
"go.podman.io/image/v5/internal/signature"
12+
"go.podman.io/image/v5/manifest"
13+
"go.podman.io/image/v5/signature/internal"
14+
)
15+
16+
// isSignatureAcceptedBundle verifies a Cosign v3 sigstore bundle format signature.
17+
func (pr *prSigstoreSigned) isSignatureAcceptedBundle(ctx context.Context, image private.UnparsedImage, sig signature.Sigstore, trustRoot *sigstoreSignedTrustRoot) (signatureAcceptanceResult, error) {
18+
bundleBytes := sig.UntrustedPayload()
19+
20+
// Parse the bundle using sigstore-go
21+
bundle, err := internal.LoadBundle(bundleBytes)
22+
if err != nil {
23+
return sarRejected, err
24+
}
25+
26+
// Handle DSSE bundles differently from MessageSignature bundles
27+
if bundle.IsDSSE() {
28+
return pr.verifyDSSEBundle(ctx, image, bundle, trustRoot)
29+
}
30+
31+
// For MessageSignature bundles, use the legacy conversion approach
32+
return pr.verifyMessageSignatureBundle(ctx, image, bundle, trustRoot)
33+
}
34+
35+
// verifyMessageSignatureBundle verifies a bundle with MessageSignature format.
36+
func (pr *prSigstoreSigned) verifyMessageSignatureBundle(ctx context.Context, image private.UnparsedImage, bundle *internal.Bundle, trustRoot *sigstoreSignedTrustRoot) (signatureAcceptanceResult, error) {
37+
// Extract verification material from the bundle
38+
keyOrCertPEM, base64Sig, payload, err := internal.ConvertBundleToLegacyFormat(bundle)
39+
if err != nil {
40+
return sarRejected, err
41+
}
42+
43+
keySources := 0
44+
if trustRoot.publicKeys != nil {
45+
keySources++
46+
}
47+
if trustRoot.fulcio != nil {
48+
keySources++
49+
}
50+
if trustRoot.pki != nil {
51+
keySources++
52+
}
53+
54+
var publicKeys []crypto.PublicKey
55+
switch {
56+
case keySources > 1:
57+
return sarRejected, errors.New("Internal inconsistency: More than one of public key, Fulcio, or PKI specified")
58+
case keySources == 0:
59+
return sarRejected, errors.New("Internal inconsistency: A public key, Fulcio, or PKI must be specified.")
60+
61+
case trustRoot.publicKeys != nil:
62+
// Use sigstore-go's bundle verification with public keys
63+
result, err := internal.VerifyBundle(bundle.RawBytes(), internal.BundleVerifyOptions{
64+
PublicKeys: trustRoot.publicKeys,
65+
RekorPublicKeys: trustRoot.rekorPublicKeys,
66+
SkipTlogVerification: trustRoot.rekorPublicKeys == nil,
67+
})
68+
if err != nil {
69+
return sarRejected, err
70+
}
71+
publicKeys = []crypto.PublicKey{result.PublicKey}
72+
73+
case trustRoot.fulcio != nil:
74+
if trustRoot.rekorPublicKeys == nil {
75+
return sarRejected, errors.New("Internal inconsistency: Fulcio CA specified without a Rekor public key")
76+
}
77+
78+
// Get certificate from the bundle
79+
cert, err := bundle.GetCertificate()
80+
if err != nil {
81+
return sarRejected, err
82+
}
83+
if cert == nil {
84+
return sarRejected, internal.NewInvalidSignatureError("bundle does not contain a certificate for Fulcio verification")
85+
}
86+
87+
// TODO: Use sigstore-go TrustedMaterial for full Fulcio verification
88+
// For now, use the certificate's public key
89+
publicKeys = []crypto.PublicKey{cert.PublicKey}
90+
91+
case trustRoot.pki != nil:
92+
// Get certificate from the bundle
93+
cert, err := bundle.GetCertificate()
94+
if err != nil {
95+
return sarRejected, err
96+
}
97+
if cert == nil {
98+
return sarRejected, internal.NewInvalidSignatureError("bundle does not contain a certificate for PKI verification")
99+
}
100+
101+
pk, err := verifyPKI(trustRoot.pki, keyOrCertPEM, nil)
102+
if err != nil {
103+
return sarRejected, err
104+
}
105+
publicKeys = []crypto.PublicKey{pk}
106+
}
107+
108+
if len(publicKeys) == 0 {
109+
return sarRejected, fmt.Errorf("Internal inconsistency: publicKey not set before verifying sigstore bundle payload")
110+
}
111+
112+
// Verify the payload signature using the extracted components
113+
verifiedPayload, err := internal.VerifySigstorePayload(publicKeys, payload, base64Sig, internal.SigstorePayloadAcceptanceRules{
114+
ValidateSignedDockerReference: func(ref string) error {
115+
if !pr.SignedIdentity.matchesDockerReference(image, ref) {
116+
return PolicyRequirementError(fmt.Sprintf("Signature for identity %q is not accepted", ref))
117+
}
118+
return nil
119+
},
120+
ValidateSignedDockerManifestDigest: func(digest digest.Digest) error {
121+
m, _, err := image.Manifest(ctx)
122+
if err != nil {
123+
return err
124+
}
125+
digestMatches, err := manifest.MatchesDigest(m, digest)
126+
if err != nil {
127+
return err
128+
}
129+
if !digestMatches {
130+
return PolicyRequirementError(fmt.Sprintf("Signature for digest %s does not match", digest))
131+
}
132+
return nil
133+
},
134+
})
135+
if err != nil {
136+
return sarRejected, err
137+
}
138+
if verifiedPayload == nil {
139+
return sarRejected, errors.New("internal error: VerifySigstorePayload succeeded but returned no data")
140+
}
141+
142+
return sarAccepted, nil
143+
}
144+
145+
// verifyDSSEBundle verifies a bundle with DSSE envelope format.
146+
// DSSE bundles contain attestations (not signatures over simple signing payloads).
147+
func (pr *prSigstoreSigned) verifyDSSEBundle(ctx context.Context, image private.UnparsedImage, bundle *internal.Bundle, trustRoot *sigstoreSignedTrustRoot) (signatureAcceptanceResult, error) {
148+
keySources := 0
149+
if trustRoot.publicKeys != nil {
150+
keySources++
151+
}
152+
if trustRoot.fulcio != nil {
153+
keySources++
154+
}
155+
if trustRoot.pki != nil {
156+
keySources++
157+
}
158+
159+
switch {
160+
case keySources > 1:
161+
return sarRejected, errors.New("Internal inconsistency: More than one of public key, Fulcio, or PKI specified")
162+
case keySources == 0:
163+
return sarRejected, errors.New("Internal inconsistency: A public key, Fulcio, or PKI must be specified.")
164+
165+
case trustRoot.publicKeys != nil:
166+
// Use sigstore-go's bundle verification with public keys
167+
result, err := internal.VerifyBundle(bundle.RawBytes(), internal.BundleVerifyOptions{
168+
PublicKeys: trustRoot.publicKeys,
169+
RekorPublicKeys: trustRoot.rekorPublicKeys,
170+
SkipTlogVerification: trustRoot.rekorPublicKeys == nil,
171+
})
172+
if err != nil {
173+
return sarRejected, err
174+
}
175+
// Use the verified payload for further validation
176+
return pr.validateDSSEPayload(ctx, image, result.EnvelopePayload)
177+
178+
case trustRoot.fulcio != nil:
179+
// Get certificate from the bundle
180+
cert, err := bundle.GetCertificate()
181+
if err != nil {
182+
return sarRejected, err
183+
}
184+
if cert == nil {
185+
return sarRejected, internal.NewInvalidSignatureError("bundle does not contain a certificate for Fulcio verification")
186+
}
187+
188+
// Verify using certificate's public key
189+
result, err := internal.VerifyBundle(bundle.RawBytes(), internal.BundleVerifyOptions{
190+
PublicKeys: []crypto.PublicKey{cert.PublicKey},
191+
SkipTlogVerification: true, // TODO: proper Fulcio+Rekor verification
192+
})
193+
if err != nil {
194+
return sarRejected, err
195+
}
196+
197+
return pr.validateDSSEPayload(ctx, image, result.EnvelopePayload)
198+
199+
case trustRoot.pki != nil:
200+
// Get certificate from the bundle
201+
cert, err := bundle.GetCertificate()
202+
if err != nil {
203+
return sarRejected, err
204+
}
205+
if cert == nil {
206+
return sarRejected, internal.NewInvalidSignatureError("bundle does not contain a certificate for PKI verification")
207+
}
208+
209+
// Verify using certificate's public key
210+
result, err := internal.VerifyBundle(bundle.RawBytes(), internal.BundleVerifyOptions{
211+
PublicKeys: []crypto.PublicKey{cert.PublicKey},
212+
SkipTlogVerification: true,
213+
})
214+
if err != nil {
215+
return sarRejected, err
216+
}
217+
218+
return pr.validateDSSEPayload(ctx, image, result.EnvelopePayload)
219+
}
220+
221+
return sarRejected, errors.New("Internal inconsistency: no key source matched")
222+
}
223+
224+
// validateDSSEPayload validates the payload from a verified DSSE envelope.
225+
// The payload is typically an in-toto attestation or a simple signing payload.
226+
// The DSSE signature has already been verified, so we just need to validate the content.
227+
func (pr *prSigstoreSigned) validateDSSEPayload(ctx context.Context, image private.UnparsedImage, payload []byte) (signatureAcceptanceResult, error) {
228+
// Try to parse as a simple signing payload first
229+
parsedPayload, err := internal.ParseSigstorePayload(payload)
230+
if err == nil {
231+
// Validate the parsed payload
232+
if err := pr.validateParsedPayload(ctx, image, parsedPayload); err != nil {
233+
return sarRejected, err
234+
}
235+
return sarAccepted, nil
236+
}
237+
238+
// If it's not a simple signing payload, try to parse as an in-toto attestation
239+
inTotoPayload, err := internal.ParseInTotoStatement(payload)
240+
if err == nil {
241+
// Validate the in-toto statement
242+
if err := pr.validateInTotoStatement(ctx, image, inTotoPayload); err != nil {
243+
return sarRejected, err
244+
}
245+
return sarAccepted, nil
246+
}
247+
248+
// If we can't parse the payload format, reject it
249+
return sarRejected, internal.NewInvalidSignatureError("DSSE payload is neither a simple signing payload nor an in-toto statement")
250+
}
251+
252+
// validateParsedPayload validates a parsed simple signing payload against the image.
253+
func (pr *prSigstoreSigned) validateParsedPayload(ctx context.Context, image private.UnparsedImage, payload *internal.UntrustedSigstorePayload) error {
254+
// Validate the docker reference
255+
if !pr.SignedIdentity.matchesDockerReference(image, payload.UntrustedDockerReference()) {
256+
return PolicyRequirementError(fmt.Sprintf("Signature for identity %q is not accepted", payload.UntrustedDockerReference()))
257+
}
258+
259+
// Validate the manifest digest
260+
m, _, err := image.Manifest(ctx)
261+
if err != nil {
262+
return err
263+
}
264+
digestMatches, err := manifest.MatchesDigest(m, payload.UntrustedDockerManifestDigest())
265+
if err != nil {
266+
return err
267+
}
268+
if !digestMatches {
269+
return PolicyRequirementError(fmt.Sprintf("Signature for digest %s does not match", payload.UntrustedDockerManifestDigest()))
270+
}
271+
272+
return nil
273+
}
274+
275+
// validateInTotoStatement validates an in-toto statement against the image.
276+
func (pr *prSigstoreSigned) validateInTotoStatement(ctx context.Context, image private.UnparsedImage, statement *internal.InTotoStatement) error {
277+
// Get the manifest digest
278+
m, _, err := image.Manifest(ctx)
279+
if err != nil {
280+
return err
281+
}
282+
manifestDigest, err := manifest.Digest(m)
283+
if err != nil {
284+
return err
285+
}
286+
287+
// Validate that one of the subjects matches the image digest
288+
if !statement.MatchesDigest(manifestDigest) {
289+
return PolicyRequirementError(fmt.Sprintf("In-toto statement does not reference digest %s", manifestDigest))
290+
}
291+
292+
return nil
293+
}

0 commit comments

Comments
 (0)