Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions image/docs/containers-policy.json.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,8 @@ This requirement requires an image to be signed using a sigstore signature with
"caPath": "/path/to/local/CA/file",
"caData": "base64-encoded-CA-data",
"oidcIssuer": "https://expected.OIDC.issuer/",
"subjectEmail", "expected-signing-user@example.com",
"subjectEmail": "expected-signing-user@example.com",
"buildSignerURI": "https://github.com/OWNER/REPO/.github/workflows/WORKFLOW.yml@refs/heads/BRANCH"
},
"pki": {
"caRootsPath": "/path/to/local/CARoots/file",
Expand All @@ -344,9 +345,12 @@ Only signatures made by any key in the list are accepted.

If `fulcio` is present, the signature must be based on a Fulcio-issued certificate.
One of `caPath` and `caData` must be specified, containing the public key of the Fulcio instance.
Both `oidcIssuer` and `subjectEmail` are mandatory,
exactly specifying the expected identity provider,
and the identity of the user obtaining the Fulcio certificate.
`oidcIssuer` is mandatory, specifying the expected identity provider.
At least one of `subjectEmail` or `buildSignerURI` must be specified to identify the signer:
`subjectEmail` matches the email address of a user who signed interactively,
while `buildSignerURI` matches the Build Signer URI certificate extension (OID 1.3.6.1.4.1.57264.1.9),
typically used for signatures from CI workflows like GitHub Actions.
If both are specified, both must match.

If `pki` is present, the signature must be based on a non-Fulcio X.509 certificate.
One of `caRootsPath` and `caRootsData` must be specified, containing certificates of the CAs.
Expand Down Expand Up @@ -395,9 +399,10 @@ selectively allow individual transports and scopes as desired.
"keyPath": "/path/to/sigstore-pubkey.pub"
}
],
/* A sigstore-signed repository using the community Fulcio+Rekor servers.
/* A sigstore-signed repository using the community Fulcio+Rekor servers,
signed by a user interactively.

The community servers public keys can be obtained from
The community servers' public keys can be obtained from
https://github.com/sigstore/sigstore/tree/main/pkg/tuf/repository/targets . */
"hostname:5000/myns/sigstore-signed-fulcio-rekor": [
{
Expand All @@ -407,7 +412,23 @@ selectively allow individual transports and scopes as desired.
"oidcIssuer": "https://github.com/login/oauth",
"subjectEmail": "test-user@example.com"
},
"rekorPublicKeyPath": "/path/to/rekor.pub",
"rekorPublicKeyPath": "/path/to/rekor.pub"
}
],
/* A sigstore-signed repository using keyless signing from a GitHub Actions workflow.
This verifies that the image was signed by a specific workflow in a specific repository.

The community servers' public keys can be obtained from
https://github.com/sigstore/sigstore/tree/main/pkg/tuf/repository/targets . */
"hostname:5000/myns/sigstore-signed-github-actions": [
{
"type": "sigstoreSigned",
"fulcio": {
"caPath": "/path/to/fulcio_v1.crt.pem",
"oidcIssuer": "https://token.actions.githubusercontent.com",
"buildSignerURI": "https://github.com/myorg/myrepo/.github/workflows/release.yml@refs/heads/main"
},
"rekorPublicKeyPath": "/path/to/rekor.pub"
}
],
/* A Sigstore-signed repository using a certificate generated by a custom public-key infrastructure.*/
Expand Down
59 changes: 50 additions & 9 deletions image/signature/fulcio_cert.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ type fulcioTrustRoot struct {
caCertificates *x509.CertPool
oidcIssuer string
subjectEmail string
buildSignerURI 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.buildSignerURI == "" {
return errors.New("Internal inconsistency: Fulcio use set up without subject identity (subjectEmail or buildSignerURI)")
}
return nil
}
Expand Down Expand Up @@ -85,6 +86,31 @@ func fulcioIssuerInCertificate(untrustedCertificate *x509.Certificate) (string,
}
}

// fulcioBuildSignerURIInCertificate extracts the BuildSignerURI (OID 1.3.6.1.4.1.57264.1.9) from the certificate.
// Returns an empty string and no error if the extension is not present.
func fulcioBuildSignerURIInCertificate(untrustedCertificate *x509.Certificate) (string, error) {
var buildSignerURI string
found := false
for _, untrustedExt := range untrustedCertificate.Extensions {
if untrustedExt.Id.Equal(certificate.OIDBuildSignerURI) {
if found {
// Coverage: This is unreachable in Go ≥1.19, which rejects certificates with duplicate extensions
// already in ParseCertificate.
return "", internal.NewInvalidSignatureError("Fulcio certificate has a duplicate BuildSignerURI extension")
}
rest, err := asn1.Unmarshal(untrustedExt.Value, &buildSignerURI)
if err != nil {
return "", internal.NewInvalidSignatureError(fmt.Sprintf("invalid ASN.1 in BuildSignerURI extension: %v", err))
}
if len(rest) != 0 {
return "", internal.NewInvalidSignatureError("invalid ASN.1 in BuildSignerURI extension, trailing data")
}
found = true
}
}
return buildSignerURI, nil
}

func (f *fulcioTrustRoot) verifyFulcioCertificateAtTime(relevantTime time.Time, untrustedCertificateBytes []byte, untrustedIntermediateChainBytes []byte) (crypto.PublicKey, error) {
// == Verify the certificate is correctly signed
var untrustedIntermediatePool *x509.CertPool // = nil
Expand Down Expand Up @@ -164,15 +190,30 @@ func (f *fulcioTrustRoot) verifyFulcioCertificateAtTime(relevantTime time.Time,
return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Unexpected Fulcio OIDC issuer %q", oidcIssuer))
}

// == Validate the OIDC subject
if !slices.Contains(untrustedCertificate.EmailAddresses, f.subjectEmail) {
return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Required email %q not found (got %q)",
f.subjectEmail,
untrustedCertificate.EmailAddresses))
// == Validate the OIDC subject identity
// At least one of subjectEmail or buildSignerURI must be specified (enforced by validate()).
// If both are specified, both must match (AND semantics).
if f.subjectEmail != "" {
if !slices.Contains(untrustedCertificate.EmailAddresses, f.subjectEmail) {
return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Required email %q not found (got %q)",
f.subjectEmail,
untrustedCertificate.EmailAddresses))
}
}
if f.buildSignerURI != "" {
certBuildSignerURI, err := fulcioBuildSignerURIInCertificate(untrustedCertificate)
if err != nil {
return nil, err
}
if certBuildSignerURI != f.buildSignerURI {
return nil, internal.NewInvalidSignatureError(fmt.Sprintf("Required BuildSignerURI %q not found (got %q)",
f.buildSignerURI,
certBuildSignerURI))
}
}
// FIXME: Match more subject types? Cosign does:
// - .DNSNames (cant be issued by Fulcio)
// - .IPAddresses (cant be issued by Fulcio)
// - .DNSNames (can't be issued by Fulcio)
// - .IPAddresses (can't be issued by Fulcio)
// - .URIs (CAN be issued by Fulcio)
// - OtherName values in SAN (CAN be issued by Fulcio)
// - Various values about GitHub workflows (CAN be issued by Fulcio)
Expand Down
Loading