Skip to content

Commit 1f0c308

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

File tree

2 files changed

+372
-1
lines changed

2 files changed

+372
-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: 364 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,364 @@
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+
_, 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 PEM from the bundle
79+
certPEM, err := bundle.GetCertificatePEM()
80+
if err != nil {
81+
return sarRejected, err
82+
}
83+
if certPEM == nil {
84+
return sarRejected, internal.NewInvalidSignatureError("bundle does not contain a certificate for Fulcio verification")
85+
}
86+
87+
// Get intermediate chain PEM if present
88+
chainPEM, err := bundle.GetIntermediateChainPEM()
89+
if err != nil {
90+
return sarRejected, err
91+
}
92+
93+
// For Fulcio verification, we need a trusted timestamp from Rekor
94+
// First, verify the bundle has a tlog entry
95+
if !bundle.HasTlogEntry() {
96+
return sarRejected, internal.NewInvalidSignatureError("bundle does not contain a transparency log entry required for Fulcio verification")
97+
}
98+
99+
// Get the integrated time from the tlog entry
100+
integratedTime, err := bundle.GetIntegratedTime()
101+
if err != nil {
102+
return sarRejected, err
103+
}
104+
if integratedTime.IsZero() {
105+
return sarRejected, internal.NewInvalidSignatureError("bundle transparency log entry has no integrated time")
106+
}
107+
108+
// Verify the Fulcio certificate chain at the tlog integrated time
109+
pk, err := trustRoot.fulcio.verifyFulcioCertificateAtTime(integratedTime, certPEM, chainPEM)
110+
if err != nil {
111+
return sarRejected, err
112+
}
113+
publicKeys = []crypto.PublicKey{pk}
114+
115+
case trustRoot.pki != nil:
116+
// Get certificate PEM from the bundle
117+
certPEM, err := bundle.GetCertificatePEM()
118+
if err != nil {
119+
return sarRejected, err
120+
}
121+
if certPEM == nil {
122+
return sarRejected, internal.NewInvalidSignatureError("bundle does not contain a certificate for PKI verification")
123+
}
124+
125+
// Get intermediate chain PEM if present
126+
chainPEM, err := bundle.GetIntermediateChainPEM()
127+
if err != nil {
128+
return sarRejected, err
129+
}
130+
131+
pk, err := verifyPKI(trustRoot.pki, certPEM, chainPEM)
132+
if err != nil {
133+
return sarRejected, err
134+
}
135+
publicKeys = []crypto.PublicKey{pk}
136+
}
137+
138+
if len(publicKeys) == 0 {
139+
return sarRejected, fmt.Errorf("Internal inconsistency: publicKey not set before verifying sigstore bundle payload")
140+
}
141+
142+
// Verify the payload signature using the extracted components
143+
verifiedPayload, err := internal.VerifySigstorePayload(publicKeys, payload, base64Sig, internal.SigstorePayloadAcceptanceRules{
144+
ValidateSignedDockerReference: func(ref string) error {
145+
if !pr.SignedIdentity.matchesDockerReference(image, ref) {
146+
return PolicyRequirementError(fmt.Sprintf("Signature for identity %q is not accepted", ref))
147+
}
148+
return nil
149+
},
150+
ValidateSignedDockerManifestDigest: func(digest digest.Digest) error {
151+
m, _, err := image.Manifest(ctx)
152+
if err != nil {
153+
return err
154+
}
155+
digestMatches, err := manifest.MatchesDigest(m, digest)
156+
if err != nil {
157+
return err
158+
}
159+
if !digestMatches {
160+
return PolicyRequirementError(fmt.Sprintf("Signature for digest %s does not match", digest))
161+
}
162+
return nil
163+
},
164+
})
165+
if err != nil {
166+
return sarRejected, err
167+
}
168+
if verifiedPayload == nil {
169+
return sarRejected, errors.New("internal error: VerifySigstorePayload succeeded but returned no data")
170+
}
171+
172+
return sarAccepted, nil
173+
}
174+
175+
// verifyDSSEBundle verifies a bundle with DSSE envelope format.
176+
// DSSE bundles contain attestations (not signatures over simple signing payloads).
177+
func (pr *prSigstoreSigned) verifyDSSEBundle(ctx context.Context, image private.UnparsedImage, bundle *internal.Bundle, trustRoot *sigstoreSignedTrustRoot) (signatureAcceptanceResult, error) {
178+
keySources := 0
179+
if trustRoot.publicKeys != nil {
180+
keySources++
181+
}
182+
if trustRoot.fulcio != nil {
183+
keySources++
184+
}
185+
if trustRoot.pki != nil {
186+
keySources++
187+
}
188+
189+
switch {
190+
case keySources > 1:
191+
return sarRejected, errors.New("Internal inconsistency: More than one of public key, Fulcio, or PKI specified")
192+
case keySources == 0:
193+
return sarRejected, errors.New("Internal inconsistency: A public key, Fulcio, or PKI must be specified.")
194+
195+
case trustRoot.publicKeys != nil:
196+
// Use sigstore-go's bundle verification with public keys
197+
result, err := internal.VerifyBundle(bundle.RawBytes(), internal.BundleVerifyOptions{
198+
PublicKeys: trustRoot.publicKeys,
199+
RekorPublicKeys: trustRoot.rekorPublicKeys,
200+
SkipTlogVerification: trustRoot.rekorPublicKeys == nil,
201+
})
202+
if err != nil {
203+
return sarRejected, err
204+
}
205+
// Use the verified payload for further validation
206+
return pr.validateDSSEPayload(ctx, image, result.EnvelopePayload)
207+
208+
case trustRoot.fulcio != nil:
209+
if trustRoot.rekorPublicKeys == nil {
210+
return sarRejected, errors.New("Internal inconsistency: Fulcio CA specified without a Rekor public key")
211+
}
212+
213+
// Get certificate PEM from the bundle for Fulcio verification
214+
certPEM, err := bundle.GetCertificatePEM()
215+
if err != nil {
216+
return sarRejected, err
217+
}
218+
if certPEM == nil {
219+
return sarRejected, internal.NewInvalidSignatureError("bundle does not contain a certificate for Fulcio verification")
220+
}
221+
222+
// Get intermediate chain PEM if present
223+
chainPEM, err := bundle.GetIntermediateChainPEM()
224+
if err != nil {
225+
return sarRejected, err
226+
}
227+
228+
// For Fulcio verification, we need a trusted timestamp from Rekor
229+
if !bundle.HasTlogEntry() {
230+
return sarRejected, internal.NewInvalidSignatureError("bundle does not contain a transparency log entry required for Fulcio verification")
231+
}
232+
233+
integratedTime, err := bundle.GetIntegratedTime()
234+
if err != nil {
235+
return sarRejected, err
236+
}
237+
if integratedTime.IsZero() {
238+
return sarRejected, internal.NewInvalidSignatureError("bundle transparency log entry has no integrated time")
239+
}
240+
241+
// Verify the Fulcio certificate chain at the tlog integrated time
242+
pk, err := trustRoot.fulcio.verifyFulcioCertificateAtTime(integratedTime, certPEM, chainPEM)
243+
if err != nil {
244+
return sarRejected, err
245+
}
246+
247+
// Now verify the DSSE envelope signature using the verified public key
248+
result, err := internal.VerifyBundle(bundle.RawBytes(), internal.BundleVerifyOptions{
249+
PublicKeys: []crypto.PublicKey{pk},
250+
SkipTlogVerification: true, // Tlog already verified above via integrated time
251+
})
252+
if err != nil {
253+
return sarRejected, err
254+
}
255+
256+
return pr.validateDSSEPayload(ctx, image, result.EnvelopePayload)
257+
258+
case trustRoot.pki != nil:
259+
// Get certificate PEM from the bundle
260+
certPEM, err := bundle.GetCertificatePEM()
261+
if err != nil {
262+
return sarRejected, err
263+
}
264+
if certPEM == nil {
265+
return sarRejected, internal.NewInvalidSignatureError("bundle does not contain a certificate for PKI verification")
266+
}
267+
268+
// Get intermediate chain PEM if present
269+
chainPEM, err := bundle.GetIntermediateChainPEM()
270+
if err != nil {
271+
return sarRejected, err
272+
}
273+
274+
// Verify the PKI certificate chain
275+
pk, err := verifyPKI(trustRoot.pki, certPEM, chainPEM)
276+
if err != nil {
277+
return sarRejected, err
278+
}
279+
280+
// Now verify the DSSE envelope signature using the verified public key
281+
result, err := internal.VerifyBundle(bundle.RawBytes(), internal.BundleVerifyOptions{
282+
PublicKeys: []crypto.PublicKey{pk},
283+
SkipTlogVerification: true,
284+
})
285+
if err != nil {
286+
return sarRejected, err
287+
}
288+
289+
return pr.validateDSSEPayload(ctx, image, result.EnvelopePayload)
290+
}
291+
292+
return sarRejected, errors.New("Internal inconsistency: no key source matched")
293+
}
294+
295+
// validateDSSEPayload validates the payload from a verified DSSE envelope.
296+
// The payload is typically an in-toto attestation or a simple signing payload.
297+
// The DSSE signature has already been verified, so we just need to validate the content.
298+
func (pr *prSigstoreSigned) validateDSSEPayload(ctx context.Context, image private.UnparsedImage, payload []byte) (signatureAcceptanceResult, error) {
299+
// Try to parse as a simple signing payload first
300+
parsedPayload, err := internal.ParseSigstorePayload(payload)
301+
if err == nil {
302+
// Validate the parsed payload
303+
if err := pr.validateParsedPayload(ctx, image, parsedPayload); err != nil {
304+
return sarRejected, err
305+
}
306+
return sarAccepted, nil
307+
}
308+
309+
// If it's not a simple signing payload, try to parse as an in-toto attestation
310+
inTotoPayload, err := internal.ParseInTotoStatement(payload)
311+
if err == nil {
312+
// Validate the in-toto statement
313+
if err := pr.validateInTotoStatement(ctx, image, inTotoPayload); err != nil {
314+
return sarRejected, err
315+
}
316+
return sarAccepted, nil
317+
}
318+
319+
// If we can't parse the payload format, reject it
320+
return sarRejected, internal.NewInvalidSignatureError("DSSE payload is neither a simple signing payload nor an in-toto statement")
321+
}
322+
323+
// validateParsedPayload validates a parsed simple signing payload against the image.
324+
func (pr *prSigstoreSigned) validateParsedPayload(ctx context.Context, image private.UnparsedImage, payload *internal.UntrustedSigstorePayload) error {
325+
// Validate the docker reference
326+
if !pr.SignedIdentity.matchesDockerReference(image, payload.UntrustedDockerReference()) {
327+
return PolicyRequirementError(fmt.Sprintf("Signature for identity %q is not accepted", payload.UntrustedDockerReference()))
328+
}
329+
330+
// Validate the manifest digest
331+
m, _, err := image.Manifest(ctx)
332+
if err != nil {
333+
return err
334+
}
335+
digestMatches, err := manifest.MatchesDigest(m, payload.UntrustedDockerManifestDigest())
336+
if err != nil {
337+
return err
338+
}
339+
if !digestMatches {
340+
return PolicyRequirementError(fmt.Sprintf("Signature for digest %s does not match", payload.UntrustedDockerManifestDigest()))
341+
}
342+
343+
return nil
344+
}
345+
346+
// validateInTotoStatement validates an in-toto statement against the image.
347+
func (pr *prSigstoreSigned) validateInTotoStatement(ctx context.Context, image private.UnparsedImage, statement *internal.InTotoStatement) error {
348+
// Get the manifest digest
349+
m, _, err := image.Manifest(ctx)
350+
if err != nil {
351+
return err
352+
}
353+
manifestDigest, err := manifest.Digest(m)
354+
if err != nil {
355+
return err
356+
}
357+
358+
// Validate that one of the subjects matches the image digest
359+
if !statement.MatchesDigest(manifestDigest) {
360+
return PolicyRequirementError(fmt.Sprintf("In-toto statement does not reference digest %s", manifestDigest))
361+
}
362+
363+
return nil
364+
}

0 commit comments

Comments
 (0)