diff --git a/signature/fulcio_cert.go b/signature/fulcio_cert.go index c11fa46a9..a68831542 100644 --- a/signature/fulcio_cert.go +++ b/signature/fulcio_cert.go @@ -10,6 +10,7 @@ import ( "encoding/asn1" "errors" "fmt" + "strings" "time" "github.com/containers/image/v5/signature/internal" @@ -24,14 +25,15 @@ type fulcioTrustRoot struct { caCertificates *x509.CertPool oidcIssuer string subjectEmail string + URI string } func (f *fulcioTrustRoot) validate() error { if f.oidcIssuer == "" { return errors.New("Internal inconsistency: Fulcio use set up without OIDC issuer") } - if f.subjectEmail == "" { - return errors.New("Internal inconsistency: Fulcio use set up without subject email") + if f.subjectEmail == "" && f.URI == "" { + return errors.New("Internal inconsistency: Fulcio use set up without subject email or URI") } return nil } @@ -177,10 +179,17 @@ func (f *fulcioTrustRoot) verifyFulcioCertificateAtTime(relevantTime time.Time, } // == Validate the OIDC subject - if !slices.Contains(untrustedCertificate.EmailAddresses, f.subjectEmail) { - return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Required email %s not found (got %#v)", - f.subjectEmail, - untrustedCertificate.EmailAddresses)) + if !slices.Contains(untrustedCertificate.EmailAddresses, f.subjectEmail) && !strings.Contains(untrustedCertificate.URIs[0].String(), f.URI) { + if len(untrustedCertificate.EmailAddresses) > 0 { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Required email %s not found (got %#v)", + f.subjectEmail, + untrustedCertificate.EmailAddresses)) + } + if len(untrustedCertificate.URIs) > 0 { + return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Required URI %s not found (got %#v)", + f.URI, + untrustedCertificate.URIs)) + } } // FIXME: Match more subject types? Cosign does: // - .DNSNames (can’t be issued by Fulcio) diff --git a/signature/policy_config_sigstore.go b/signature/policy_config_sigstore.go index d8c6a97f1..794a1caf0 100644 --- a/signature/policy_config_sigstore.go +++ b/signature/policy_config_sigstore.go @@ -261,6 +261,17 @@ func PRSigstoreSignedFulcioWithSubjectEmail(subjectEmail string) PRSigstoreSigne } } +// PRSigstoreSignedFulcioWithURI specifies a value for the "URI" field when calling NewPRSigstoreSignedFulcio +func PRSigstoreSignedFulcioWithURI(URI string) PRSigstoreSignedFulcioOption { + return func(f *prSigstoreSignedFulcio) error { + if f.URI != "" { + return errors.New(`"URI" already specified`) + } + f.URI = URI + return nil + } +} + // newPRSigstoreSignedFulcio is NewPRSigstoreSignedFulcio, except it returns the private type func newPRSigstoreSignedFulcio(options ...PRSigstoreSignedFulcioOption) (*prSigstoreSignedFulcio, error) { res := prSigstoreSignedFulcio{} @@ -279,8 +290,8 @@ func newPRSigstoreSignedFulcio(options ...PRSigstoreSignedFulcioOption) (*prSigs if res.OIDCIssuer == "" { return nil, InvalidPolicyFormatError("oidcIssuer not specified") } - if res.SubjectEmail == "" { - return nil, InvalidPolicyFormatError("subjectEmail not specified") + if res.SubjectEmail == "" && res.URI == "" { + return nil, InvalidPolicyFormatError("subjectEmail and URI not specified") } return &res, nil @@ -297,7 +308,7 @@ var _ json.Unmarshaler = (*prSigstoreSignedFulcio)(nil) func (f *prSigstoreSignedFulcio) UnmarshalJSON(data []byte) error { *f = prSigstoreSignedFulcio{} var tmp prSigstoreSignedFulcio - var gotCAPath, gotCAData, gotOIDCIssuer, gotSubjectEmail bool // = false... + var gotCAPath, gotCAData, gotOIDCIssuer, gotSubjectEmail, gotURI bool // = false... if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) any { switch key { case "caPath": @@ -312,6 +323,9 @@ func (f *prSigstoreSignedFulcio) UnmarshalJSON(data []byte) error { case "subjectEmail": gotSubjectEmail = true return &tmp.SubjectEmail + case "URI": + gotURI = true + return &tmp.URI default: return nil } @@ -329,9 +343,12 @@ func (f *prSigstoreSignedFulcio) UnmarshalJSON(data []byte) error { if gotOIDCIssuer { opts = append(opts, PRSigstoreSignedFulcioWithOIDCIssuer(tmp.OIDCIssuer)) } - if gotSubjectEmail { + if gotSubjectEmail && !gotURI { opts = append(opts, PRSigstoreSignedFulcioWithSubjectEmail(tmp.SubjectEmail)) } + if !gotSubjectEmail && gotURI { + opts = append(opts, PRSigstoreSignedFulcioWithURI(tmp.URI)) + } res, err := newPRSigstoreSignedFulcio(opts...) if err != nil { diff --git a/signature/policy_eval_sigstore.go b/signature/policy_eval_sigstore.go index dcf5592a8..17eeb8ece 100644 --- a/signature/policy_eval_sigstore.go +++ b/signature/policy_eval_sigstore.go @@ -57,6 +57,7 @@ func (f *prSigstoreSignedFulcio) prepareTrustRoot() (*fulcioTrustRoot, error) { caCertificates: certs, oidcIssuer: f.OIDCIssuer, subjectEmail: f.SubjectEmail, + URI: f.URI, } if err := fulcio.validate(); err != nil { return nil, err diff --git a/signature/policy_types.go b/signature/policy_types.go index 96e91a0a9..9eabda8f0 100644 --- a/signature/policy_types.go +++ b/signature/policy_types.go @@ -154,6 +154,8 @@ type prSigstoreSignedFulcio struct { OIDCIssuer string `json:"oidcIssuer,omitempty"` // SubjectEmail specifies the expected email address of the authenticated OIDC identity, recorded by Fulcio into the generated certificates. SubjectEmail string `json:"subjectEmail,omitempty"` + // URI specifies the expected URI of the authenticated OIDC identity, recorded by Fulcio into the generated certificates. + URI string `json:"URI,omitempty"` } // PolicyReferenceMatch specifies a set of image identities accepted in PolicyRequirement.