From 917991069a7c5ce1f96dd08859d56dc44f0068d1 Mon Sep 17 00:00:00 2001 From: Bob Callaway Date: Tue, 26 Oct 2021 04:23:45 -0700 Subject: [PATCH] Add issuer information to code signing certificates (#204) * Add issuer from OIDC token into code signing cert This extracts a claim from the OIDC token (defaults to `iss`) and inserts it into the final code signing certificate issued by Fulcio. This provides an explicit link between the identity (i.e. the email or the URI that is the subject of the certificate) to the identity provider who is vouching for an entity holding valid credentials for said identity. The value is stored in an X509 extension with OID: 1.3.6.1.4.1.57264.1.1 Signed-off-by: Bob Callaway * fix lint errors Signed-off-by: Bob Callaway * fix federation config Signed-off-by: Bob Callaway --- Dockerfile | 2 +- Dockerfile.ctfe_init | 2 +- config/config.jsn | 6 ++++ config/dex/docker-compose-config.yaml | 2 +- config/fulcio-config.yaml | 3 +- docker-compose.yml | 2 +- federation/main.go | 12 ++++--- federation/oauth2.sigstore.dev/config.yaml | 1 + go.mod | 1 + go.sum | 5 +++ pkg/api/googleca_signing_cert.go | 4 ++- pkg/ca/googleca/googleca.go | 16 ++++++++- pkg/ca/pkcs11ca/pkcs11ca.go | 15 +++++++++ pkg/challenges/challenges.go | 39 ++++++++++++++++++++-- pkg/config/config.go | 14 ++++---- pkg/oauthflow/oidc.go | 22 +++++++++++- 16 files changed, 125 insertions(+), 21 deletions(-) diff --git a/Dockerfile b/Dockerfile index db0cbf763..4e92b34f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,7 @@ ENTRYPOINT ["/usr/local/bin/fulcio-server", "serve"] # debug compile options & debugger FROM deploy as debug -RUN go get github.com/go-delve/delve/cmd/dlv +RUN go install github.com/go-delve/delve/cmd/dlv # overwrite server and include debugger COPY --from=builder /opt/app-root/src/server_debug /usr/local/bin/fulcio-server diff --git a/Dockerfile.ctfe_init b/Dockerfile.ctfe_init index 4f63448b2..8185650af 100644 --- a/Dockerfile.ctfe_init +++ b/Dockerfile.ctfe_init @@ -17,7 +17,7 @@ FROM golang:1.17.2 AS builder WORKDIR /root/ -RUN go get github.com/google/trillian/cmd/createtree@v1.3.10 +RUN go install github.com/google/trillian/cmd/createtree@v1.3.10 ADD ./config/logid.sh /root/ ADD ./config/ctfe /root/ctfe RUN chmod +x /root/logid.sh diff --git a/config/config.jsn b/config/config.jsn index f17d6c1bc..2b386c2e4 100644 --- a/config/config.jsn +++ b/config/config.jsn @@ -10,6 +10,12 @@ "ClientID": "sigstore", "Type": "email" }, + "http://dex-idp:8888/auth": { + "IssuerURL": "http://dex-idp:8888/auth", + "ClientID": "fulcio", + "IssuerClaim": "$.federated_claims.connector_id", + "Type": "email" + }, "https://oidc.dlorenc.dev": { "IssuerURL": "https://oidc.dlorenc.dev", "ClientID": "sigstore", diff --git a/config/dex/docker-compose-config.yaml b/config/dex/docker-compose-config.yaml index 9bf77e0f3..f0e01b566 100644 --- a/config/dex/docker-compose-config.yaml +++ b/config/dex/docker-compose-config.yaml @@ -36,7 +36,7 @@ oauth2: connectors: - type: mockCallback - id: mock + id: https://any.valid.url/ name: AlwaysApprovesOIDCProvider staticClients: diff --git a/config/fulcio-config.yaml b/config/fulcio-config.yaml index 7ce32f7ef..b5e72ecbb 100644 --- a/config/fulcio-config.yaml +++ b/config/fulcio-config.yaml @@ -25,7 +25,8 @@ data: "https://oauth2.sigstore.dev/auth": { "IssuerURL": "https://oauth2.sigstore.dev/auth", "ClientID": "sigstore", - "Type": "email" + "Type": "email", + "IssuerClaim": "$.federated_claims.connector_id" }, "https://oidc.dlorenc.dev": { "IssuerURL": "https://oidc.dlorenc.dev", diff --git a/docker-compose.yml b/docker-compose.yml index 3ba67365b..6db95ada3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,7 +46,7 @@ services: - dex-idp - ct_server dex-idp: - image: dexidp/dex:v2.28.0 + image: dexidp/dex:v2.30.0 command: [ "dex", "serve", diff --git a/federation/main.go b/federation/main.go index 6f39916c0..f55e705ab 100644 --- a/federation/main.go +++ b/federation/main.go @@ -42,8 +42,9 @@ var boilerPlate = `# ` type federationConfig struct { - URL string - Type string + URL string + Type string + IssuerClaim string } func main() { @@ -70,9 +71,10 @@ func main() { } fulcioCfg := config.OIDCIssuer{ - IssuerURL: cfg.URL, - ClientID: "sigstore", - Type: config.IssuerType(cfg.Type), + IssuerURL: cfg.URL, + ClientID: "sigstore", + Type: config.IssuerType(cfg.Type), + IssuerClaim: cfg.IssuerClaim, } fulcioConfig.OIDCIssuers[cfg.URL] = fulcioCfg } diff --git a/federation/oauth2.sigstore.dev/config.yaml b/federation/oauth2.sigstore.dev/config.yaml index 1bca81fe9..052f440a0 100644 --- a/federation/oauth2.sigstore.dev/config.yaml +++ b/federation/oauth2.sigstore.dev/config.yaml @@ -13,6 +13,7 @@ # limitations under the License. url: https://oauth2.sigstore.dev/auth +issuerclaim: $.federated_claims.connector_id contact: lorenc.d@gmail.com description: "dex address for fulcio" type: "email" diff --git a/go.mod b/go.mod index 8d2ba1973..38d35fe71 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( cloud.google.com/go/security v0.1.0 + github.com/PaesslerAG/jsonpath v0.1.1 github.com/ThalesIgnite/crypto11 v1.2.5 github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect github.com/coreos/go-oidc/v3 v3.1.0 diff --git a/go.sum b/go.sum index ef38e2dc8..45427d95e 100644 --- a/go.sum +++ b/go.sum @@ -50,6 +50,11 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8= +github.com/PaesslerAG/gval v1.0.0/go.mod h1:y/nm5yEyTeX6av0OfKJNp9rBNj2XrGhAf5+v24IBN1I= +github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= +github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= +github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= diff --git a/pkg/api/googleca_signing_cert.go b/pkg/api/googleca_signing_cert.go index 643672fb2..66c0b012b 100644 --- a/pkg/api/googleca_signing_cert.go +++ b/pkg/api/googleca_signing_cert.go @@ -42,7 +42,9 @@ func GoogleCASigningCertHandler(ctx context.Context, subj *challenges.ChallengeR case challenges.GithubWorkflowValue: privca = googleca.GithubWorkflowSubject(subj.Value) } - req := googleca.Req(parent, privca, publicKey) + + extensions := googleca.IssuerExtension(subj.Issuer) + req := googleca.Req(parent, privca, publicKey, extensions) logger.Infof("requesting cert from %s for %v", parent, Subject) resp, err := googleca.Client().CreateCertificate(ctx, req) diff --git a/pkg/ca/googleca/googleca.go b/pkg/ca/googleca/googleca.go index 970f5f242..3fc21b2a8 100644 --- a/pkg/ca/googleca/googleca.go +++ b/pkg/ca/googleca/googleca.go @@ -65,7 +65,7 @@ func getPubKeyType(pemBytes []byte) interface{} { } } -func Req(parent string, subject *privatecapb.CertificateConfig_SubjectConfig, pemBytes []byte) *privatecapb.CreateCertificateRequest { +func Req(parent string, subject *privatecapb.CertificateConfig_SubjectConfig, pemBytes []byte, extensions []*privatecapb.X509Extension) *privatecapb.CreateCertificateRequest { // TODO, use the right fields :) pubkeyType := getPubKeyType(pemBytes) return &privatecapb.CreateCertificateRequest{ @@ -89,6 +89,7 @@ func Req(parent string, subject *privatecapb.CertificateConfig_SubjectConfig, pe CodeSigning: true, }, }, + AdditionalExtensions: extensions, }, }, }, @@ -122,3 +123,16 @@ func GithubWorkflowSubject(id string) *privatecapb.CertificateConfig_SubjectConf }, } } + +func IssuerExtension(issuer string) []*privatecapb.X509Extension { + if issuer == "" { + return nil + } + + return []*privatecapb.X509Extension{{ + ObjectId: &privatecapb.ObjectId{ + ObjectIdPath: []int32{1, 3, 6, 1, 4, 1, 57264, 1, 1}, + }, + Value: []byte(issuer), + }} +} diff --git a/pkg/ca/pkcs11ca/pkcs11ca.go b/pkg/ca/pkcs11ca/pkcs11ca.go index 99c2b2880..78398af8d 100644 --- a/pkg/ca/pkcs11ca/pkcs11ca.go +++ b/pkg/ca/pkcs11ca/pkcs11ca.go @@ -18,6 +18,8 @@ package pkcs11ca import ( "crypto/rand" "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" "encoding/pem" "math/big" "net/url" @@ -58,6 +60,8 @@ func CreateClientCertificate(rootCA *x509.Certificate, subject *challenges.Chall } cert.URIs = []*url.URL{jobWorkflowURI} } + cert.ExtraExtensions = IssuerExtension(subject.Issuer) + certBytes, err := x509.CreateCertificate(rand.Reader, cert, rootCA, publicKeyPEM, privKey) if err != nil { return "", nil, err @@ -69,3 +73,14 @@ func CreateClientCertificate(rootCA *x509.Certificate, subject *challenges.Chall return string(certPEM), nil, nil } + +func IssuerExtension(issuer string) []pkix.Extension { + if issuer == "" { + return nil + } + + return []pkix.Extension{{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, + Value: []byte(issuer), + }} +} diff --git a/pkg/challenges/challenges.go b/pkg/challenges/challenges.go index 9d0cd40a2..c78cb70c7 100644 --- a/pkg/challenges/challenges.go +++ b/pkg/challenges/challenges.go @@ -42,6 +42,7 @@ const ( ) type ChallengeResult struct { + Issuer string TypeVal ChallengeType Value string } @@ -81,8 +82,20 @@ func Email(ctx context.Context, principal *oidc.IDToken, pubKey, challenge []byt return nil, err } + globalCfg := config.Config() + cfg, ok := globalCfg.OIDCIssuers[principal.Issuer] + if !ok { + return nil, errors.New("invalid configuration for OIDC ID Token issuer") + } + + issuer, err := oauthflow.IssuerFromIDToken(principal, cfg.IssuerClaim) + if err != nil { + return nil, err + } + // Now issue cert! return &ChallengeResult{ + Issuer: issuer, TypeVal: EmailValue, Value: emailAddress, }, nil @@ -96,10 +109,14 @@ func Spiffe(ctx context.Context, principal *oidc.IDToken, pubKey, challenge []by if err != nil { return nil, err } - cfg := config.Config() + globalCfg := config.Config() + cfg, ok := globalCfg.OIDCIssuers[principal.Issuer] + if !ok { + return nil, errors.New("invalid configuration for OIDC ID Token issuer") + } // The Spiffe ID must be a subdomain of the issuer (spiffe://foo.example.com -> example.com/...) - u, err := url.Parse(cfg.OIDCIssuers[principal.Issuer].IssuerURL) + u, err := url.Parse(cfg.IssuerURL) if err != nil { return nil, err } @@ -114,8 +131,14 @@ func Spiffe(ctx context.Context, principal *oidc.IDToken, pubKey, challenge []by return nil, err } + issuer, err := oauthflow.IssuerFromIDToken(principal, cfg.IssuerClaim) + if err != nil { + return nil, err + } + // Now issue cert! return &ChallengeResult{ + Issuer: issuer, TypeVal: SpiffeValue, Value: spiffeID, }, nil @@ -137,8 +160,20 @@ func GithubWorkflow(ctx context.Context, principal *oidc.IDToken, pubKey, challe return nil, err } + globalCfg := config.Config() + cfg, ok := globalCfg.OIDCIssuers[principal.Issuer] + if !ok { + return nil, errors.New("invalid configuration for OIDC ID Token issuer") + } + + issuer, err := oauthflow.IssuerFromIDToken(principal, cfg.IssuerClaim) + if err != nil { + return nil, err + } + // Now issue cert! return &ChallengeResult{ + Issuer: issuer, TypeVal: GithubWorkflowValue, Value: workflowRef, }, nil diff --git a/pkg/config/config.go b/pkg/config/config.go index 43b92e32b..54916c42d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -28,9 +28,10 @@ type FulcioConfig struct { } type OIDCIssuer struct { - IssuerURL string - ClientID string - Type IssuerType + IssuerURL string + ClientID string + Type IssuerType + IssuerClaim string `json:"IssuerClaim,omitempty"` } type IssuerType string @@ -52,9 +53,10 @@ func ParseConfig(b []byte) (FulcioConfig, error) { var DefaultConfig = FulcioConfig{ OIDCIssuers: map[string]OIDCIssuer{ "https://oauth2.sigstore.dev/auth": { - IssuerURL: "https://oauth2.sigstore.dev/auth", - ClientID: "sigstore", - Type: IssuerTypeEmail, + IssuerURL: "https://oauth2.sigstore.dev/auth", + ClientID: "sigstore", + IssuerClaim: "$.federated_claims.connector_id", + Type: IssuerTypeEmail, }, "https://accounts.google.com": { IssuerURL: "https://accounts.google.com", diff --git a/pkg/oauthflow/oidc.go b/pkg/oauthflow/oidc.go index 21767069a..693ef0e0d 100644 --- a/pkg/oauthflow/oidc.go +++ b/pkg/oauthflow/oidc.go @@ -15,7 +15,12 @@ package oauthflow -import "github.com/coreos/go-oidc/v3/oidc" +import ( + "fmt" + + "github.com/PaesslerAG/jsonpath" + "github.com/coreos/go-oidc/v3/oidc" +) func EmailFromIDToken(token *oidc.IDToken) (string, bool, error) { // Extract custom claims @@ -29,3 +34,18 @@ func EmailFromIDToken(token *oidc.IDToken) (string, bool, error) { return claims.Email, claims.Verified, nil } + +func IssuerFromIDToken(token *oidc.IDToken, claimJSONPath string) (string, error) { + if claimJSONPath == "" { + return token.Issuer, nil + } + v := interface{}(nil) + if err := token.Claims(&v); err != nil { + return "", err + } + result, err := jsonpath.Get(claimJSONPath, v) + if err != nil { + return "", err + } + return fmt.Sprintf("%v", result), nil +}