diff --git a/.github/workflows/verify-k8s.yml b/.github/workflows/verify-k8s.yml index 919b00887..26591c12f 100644 --- a/.github/workflows/verify-k8s.yml +++ b/.github/workflows/verify-k8s.yml @@ -121,8 +121,23 @@ jobs: env: CGO_ENABLED: 1 run: | + # Reduce the resource requests of Fulcio sed -i -e 's,memory: "1G",memory: "100m",g' ${{ github.workspace }}/config/deployment.yaml sed -i -e 's,cpu: ".5",memory: "50m",g' ${{ github.workspace }}/config/deployment.yaml + # Switch to the ephemeralca for testing. + sed -i -e 's,--ca=googleca,--ca=ephemeralca,g' ${{ github.workspace }}/config/deployment.yaml + # Drop the ct-log flag's value to elide CT-log uploads. + sed -i -E 's,"--ct-log-url=[^"]+","--ct-log-url=",g' ${{ github.workspace }}/config/deployment.yaml + + # From: https://banzaicloud.com/blog/kubernetes-oidc/ + # To be able to fetch the public keys and validate the JWT tokens against + # the Kubernetes cluster’s issuer we have to allow external unauthenticated + # requests. To do this, we bind this special role with a ClusterRoleBinding + # to unauthenticated users (make sure that this is safe in your environment, + # but only public keys are visible on this URL) + kubectl create clusterrolebinding oidc-reviewer \ + --clusterrole=system:service-account-issuer-discovery \ + --group=system:unauthenticated kubectl create ns fulcio-dev @@ -132,6 +147,60 @@ jobs: kubectl get po -n fulcio-dev + - name: Run signing job + env: + CGO_ENABLED: 1 + run: | + DIGEST=$(ko publish .) + + cat </locations// (only used with --ca googleca)") rootCmd.PersistentFlags().String("hsm-caroot-id", "", "HSM ID for Root CA (only used with --ca pkcs11ca)") diff --git a/cmd/app/serve.go b/cmd/app/serve.go index 73e83907c..ac0c870bf 100644 --- a/cmd/app/serve.go +++ b/cmd/app/serve.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/sigstore/fulcio/pkg/ca/ephemeralca" "github.com/sigstore/fulcio/pkg/config" "github.com/sigstore/fulcio/pkg/generated/restapi" "github.com/sigstore/fulcio/pkg/generated/restapi/operations" @@ -37,16 +38,24 @@ var serveCmd = &cobra.Command{ Long: `Starts a http server and serves the configured api`, Run: func(cmd *cobra.Command, args []string) { - if viper.GetString("ca") != "pkcs11ca" && viper.GetString("ca") != "googleca" { - log.Logger.Fatal("unknown CA: ", viper.GetString("ca")) - } + switch viper.GetString("ca") { + case "pkcs11ca": + if !viper.IsSet("hsm-caroot-id") { + log.Logger.Fatal("hsm-caroot-id must be set when using pkcs11ca") + } - if viper.GetString("ca") == "googleca" && !viper.IsSet("gcp_private_ca_parent") { - panic("gcp_private_ca_parent must be set when using googleca") - } + case "googleca": + if !viper.IsSet("gcp_private_ca_parent") { + log.Logger.Fatal("gcp_private_ca_parent must be set when using googleca") + } + + case "ephemeralca": + if err := ephemeralca.Initialize(cmd.Context()); err != nil { + log.Logger.Fatalw("error initializing ephemeral CA", err) + } - if viper.GetString("ca") == "pkcs11ca" && !viper.IsSet("hsm-caroot-id") { - panic("hsm-caroot-id must be set when using pkcs11ca") + default: + log.Logger.Fatal("unknown CA: ", viper.GetString("ca")) } // Setup the logger to dev/prod diff --git a/config/deployment.yaml b/config/deployment.yaml index 2defb223d..104291902 100644 --- a/config/deployment.yaml +++ b/config/deployment.yaml @@ -53,6 +53,8 @@ spec: volumeMounts: - name: fulcio-config mountPath: /etc/fulcio-config + - name: oidc-info + mountPath: /var/run/fulcio resources: requests: memory: "1G" @@ -61,7 +63,15 @@ spec: - name: fulcio-config configMap: name: fulcio-config - + - name: oidc-info + projected: + sources: + - configMap: + name: kube-root-ca.crt + items: + - key: ca.crt + path: ca.crt + mode: 0666 --- apiVersion: v1 kind: Service diff --git a/config/fulcio-config.yaml b/config/fulcio-config.yaml index b5e72ecbb..0568021d7 100644 --- a/config/fulcio-config.yaml +++ b/config/fulcio-config.yaml @@ -22,6 +22,11 @@ data: "ClientID": "sigstore", "Type": "email" }, + "https://kubernetes.default.svc": { + "IssuerURL": "https://kubernetes.default.svc", + "ClientID": "sigstore", + "Type": "kubernetes" + }, "https://oauth2.sigstore.dev/auth": { "IssuerURL": "https://oauth2.sigstore.dev/auth", "ClientID": "sigstore", diff --git a/federation/kubernetes.default.svc/config.yaml b/federation/kubernetes.default.svc/config.yaml new file mode 100644 index 000000000..2e5bd8dbf --- /dev/null +++ b/federation/kubernetes.default.svc/config.yaml @@ -0,0 +1,18 @@ +# Copyright 2021 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +url: https://kubernetes.default.svc +contact: mattmoor@chainguard.dev +description: "service account projected volumes" +type: "kubernetes" diff --git a/pkg/api/ca.go b/pkg/api/ca.go index 5ed1cf73d..aa69481b4 100644 --- a/pkg/api/ca.go +++ b/pkg/api/ca.go @@ -63,6 +63,8 @@ func SigningCertHandler(params operations.SigningCertParams, principal *oidc.IDT PemCertificate, PemCertificateChain, err = GoogleCASigningCertHandler(ctx, subj, publicKeyPEM) case "pkcs11ca": PemCertificate, PemCertificateChain, err = Pkcs11CASigningCertHandler(ctx, subj, publicKey) + case "ephemeralca": + PemCertificate, PemCertificateChain, err = EphemeralCASigningCertHandler(ctx, subj, publicKey) default: return handleFulcioAPIError(params, http.StatusInternalServerError, err, genericCAError) } @@ -110,6 +112,8 @@ func Subject(ctx context.Context, tok *oidc.IDToken, cfg config.FulcioConfig, pu return challenges.Spiffe(ctx, tok, publicKey, challenge) case config.IssuerTypeGithubWorkflow: return challenges.GithubWorkflow(ctx, tok, publicKey, challenge) + case config.IssuerTypeKubernetes: + return challenges.Kubernetes(ctx, tok, publicKey, challenge) default: return nil, fmt.Errorf("unsupported issuer: %s", iss.Type) } diff --git a/pkg/api/ephemeralca_signing_cert.go b/pkg/api/ephemeralca_signing_cert.go new file mode 100644 index 000000000..5b61ac467 --- /dev/null +++ b/pkg/api/ephemeralca_signing_cert.go @@ -0,0 +1,48 @@ +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package api + +import ( + "context" + "crypto/x509" + "encoding/pem" + + "github.com/sigstore/fulcio/pkg/ca/ephemeralca" + "github.com/sigstore/fulcio/pkg/ca/x509ca" + "github.com/sigstore/fulcio/pkg/challenges" +) + +func EphemeralCASigningCertHandler(ctx context.Context, subj *challenges.ChallengeResult, publicKey []byte) (string, []string, error) { + rootCA, privKey := ephemeralca.CA() + + pkixPubKey, err := x509.ParsePKIXPublicKey(publicKey) + if err != nil { + return "", nil, err + } + + clientCert, _, err := x509ca.CreateClientCertificate(rootCA, subj, pkixPubKey, privKey) + if err != nil { + return "", nil, err + } + + // Format in PEM + rootPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: rootCA.Raw, + }) + + return clientCert, []string{string(rootPEM)}, nil +} diff --git a/pkg/api/googleca_signing_cert.go b/pkg/api/googleca_signing_cert.go index 66c0b012b..9710a56d2 100644 --- a/pkg/api/googleca_signing_cert.go +++ b/pkg/api/googleca_signing_cert.go @@ -41,6 +41,8 @@ func GoogleCASigningCertHandler(ctx context.Context, subj *challenges.ChallengeR privca = googleca.SpiffeSubject(subj.Value) case challenges.GithubWorkflowValue: privca = googleca.GithubWorkflowSubject(subj.Value) + case challenges.KubernetesValue: + privca = googleca.KubernetesSubject(subj.Value) } extensions := googleca.IssuerExtension(subj.Issuer) diff --git a/pkg/api/pkcs11ca_signing_cert.go b/pkg/api/pkcs11ca_signing_cert.go index 64e634264..6de24e658 100644 --- a/pkg/api/pkcs11ca_signing_cert.go +++ b/pkg/api/pkcs11ca_signing_cert.go @@ -22,7 +22,7 @@ import ( "os" "path/filepath" - "github.com/sigstore/fulcio/pkg/ca/pkcs11ca" + "github.com/sigstore/fulcio/pkg/ca/x509ca" "github.com/sigstore/fulcio/pkg/challenges" "github.com/sigstore/fulcio/pkg/log" "github.com/sigstore/fulcio/pkg/pkcs11" @@ -74,7 +74,7 @@ func Pkcs11CASigningCertHandler(ctx context.Context, subj *challenges.ChallengeR return "", nil, err } - clientCert, _, err := pkcs11ca.CreateClientCertificate(rootCA, subj, pkixPubKey, privKey) + clientCert, _, err := x509ca.CreateClientCertificate(rootCA, subj, pkixPubKey, privKey) if err != nil { return "", nil, err } diff --git a/pkg/ca/ephemeralca/ephemeral.go b/pkg/ca/ephemeralca/ephemeral.go new file mode 100644 index 000000000..a2ed1593f --- /dev/null +++ b/pkg/ca/ephemeralca/ephemeral.go @@ -0,0 +1,79 @@ +// Copyright 2021 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +package ephemeralca + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "math" + "math/big" + "time" +) + +var ( + ca *x509.Certificate + privKey interface{} +) + +func Initialize(context.Context) error { + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return err + } + privKey = priv + + // TODO: We could make it so this could be passed in by the user + serialNumber, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) + if err != nil { + return err + } + rootCA := &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"sigstore"}, + Country: []string{"USA"}, + Province: []string{"WA"}, + Locality: []string{"Kirkland"}, + StreetAddress: []string{"767 6th St S"}, + PostalCode: []string{"98033"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, MaxPathLen: 1, + } + + caBytes, err := x509.CreateCertificate(rand.Reader, rootCA, rootCA, priv.Public(), priv) + if err != nil { + return err + } + + ca, err = x509.ParseCertificate(caBytes) + if err != nil { + return err + } + + return nil +} + +func CA() (*x509.Certificate, interface{}) { + return ca, privKey +} diff --git a/pkg/ca/googleca/googleca.go b/pkg/ca/googleca/googleca.go index 3fc21b2a8..421e8b660 100644 --- a/pkg/ca/googleca/googleca.go +++ b/pkg/ca/googleca/googleca.go @@ -124,6 +124,14 @@ func GithubWorkflowSubject(id string) *privatecapb.CertificateConfig_SubjectConf } } +func KubernetesSubject(id string) *privatecapb.CertificateConfig_SubjectConfig { + return &privatecapb.CertificateConfig_SubjectConfig{ + SubjectAltName: &privatecapb.SubjectAltNames{ + Uris: []string{id}, + }, + } +} + func IssuerExtension(issuer string) []*privatecapb.X509Extension { if issuer == "" { return nil diff --git a/pkg/ca/pkcs11ca/pkcs11ca_test.go b/pkg/ca/pkcs11ca/pkcs11ca_test.go deleted file mode 100644 index 313908050..000000000 --- a/pkg/ca/pkcs11ca/pkcs11ca_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright 2021 The Sigstore Authors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -package pkcs11ca - -import ( - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "testing" - - "github.com/sigstore/fulcio/pkg/challenges" -) - -func failErr(t *testing.T, err error) { - if err != nil { - t.Fatal(err) - } -} - -func TestCheckSignatureECDSA(t *testing.T) { - - priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - failErr(t, err) - - email := "test@gmail.com" - if err := challenges.CheckSignature(&priv.PublicKey, []byte("foo"), email); err == nil { - t.Fatal("check should have failed") - } - - h := sha256.Sum256([]byte(email)) - signature, err := priv.Sign(rand.Reader, h[:], crypto.SHA256) - failErr(t, err) - - if err := challenges.CheckSignature(&priv.PublicKey, signature, email); err != nil { - t.Fatal(err) - } - - // Try a bad email but "good" signature - if err := challenges.CheckSignature(&priv.PublicKey, signature, "bad@email.com"); err == nil { - t.Fatal("check should have failed") - } -} - -func TestCheckSignatureRSA(t *testing.T) { - priv, err := rsa.GenerateKey(rand.Reader, 2048) - failErr(t, err) - - email := "test@gmail.com" - if err := challenges.CheckSignature(&priv.PublicKey, []byte("foo"), email); err == nil { - t.Fatal("check should have failed") - } - - h := sha256.Sum256([]byte(email)) - signature, err := priv.Sign(rand.Reader, h[:], crypto.SHA256) - failErr(t, err) - - if err := challenges.CheckSignature(&priv.PublicKey, signature, email); err != nil { - t.Fatal(err) - } - - // Try a bad email but "good" signature - if err := challenges.CheckSignature(&priv.PublicKey, signature, "bad@email.com"); err == nil { - t.Fatal("check should have failed") - } -} diff --git a/pkg/ca/pkcs11ca/pkcs11ca.go b/pkg/ca/x509ca/x509ca.go similarity index 89% rename from pkg/ca/pkcs11ca/pkcs11ca.go rename to pkg/ca/x509ca/x509ca.go index 78398af8d..325f4be61 100644 --- a/pkg/ca/pkcs11ca/pkcs11ca.go +++ b/pkg/ca/x509ca/x509ca.go @@ -13,7 +13,7 @@ // limitations under the License. // -package pkcs11ca +package x509ca import ( "crypto/rand" @@ -25,12 +25,11 @@ import ( "net/url" "time" - "github.com/ThalesIgnite/crypto11" "github.com/google/uuid" "github.com/sigstore/fulcio/pkg/challenges" ) -func CreateClientCertificate(rootCA *x509.Certificate, subject *challenges.ChallengeResult, publicKeyPEM interface{}, privKey crypto11.Signer) (string, []string, error) { +func CreateClientCertificate(rootCA *x509.Certificate, subject *challenges.ChallengeResult, publicKeyPEM interface{}, privKey interface{}) (string, []string, error) { // TODO: Track / increment serial nums instead, although unlikely we will create dupes, it could happen uuid := uuid.New() var serialNumber big.Int @@ -59,6 +58,12 @@ func CreateClientCertificate(rootCA *x509.Certificate, subject *challenges.Chall return "", nil, err } cert.URIs = []*url.URL{jobWorkflowURI} + case challenges.KubernetesValue: + k8sURI, err := url.Parse(subject.Value) + if err != nil { + return "", nil, err + } + cert.URIs = []*url.URL{k8sURI} } cert.ExtraExtensions = IssuerExtension(subject.Issuer) diff --git a/pkg/challenges/challenges.go b/pkg/challenges/challenges.go index c78cb70c7..2258ee34f 100644 --- a/pkg/challenges/challenges.go +++ b/pkg/challenges/challenges.go @@ -39,6 +39,7 @@ const ( EmailValue ChallengeType = iota SpiffeValue GithubWorkflowValue + KubernetesValue ) type ChallengeResult struct { @@ -144,6 +145,41 @@ func Spiffe(ctx context.Context, principal *oidc.IDToken, pubKey, challenge []by }, nil } +func Kubernetes(ctx context.Context, principal *oidc.IDToken, pubKey, challenge []byte) (*ChallengeResult, error) { + k8sURI, err := kubernetesToken(principal) + if err != nil { + return nil, err + } + + pkixPubKey, err := x509.ParsePKIXPublicKey(pubKey) + if err != nil { + return nil, err + } + + // Check the proof + if err := CheckSignature(pkixPubKey, challenge, principal.Subject); err != nil { + 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: KubernetesValue, + Value: k8sURI, + }, nil +} + func GithubWorkflow(ctx context.Context, principal *oidc.IDToken, pubKey, challenge []byte) (*ChallengeResult, error) { workflowRef, err := workflowFromIDToken(principal) if err != nil { @@ -179,6 +215,40 @@ func GithubWorkflow(ctx context.Context, principal *oidc.IDToken, pubKey, challe }, nil } +func kubernetesToken(token *oidc.IDToken) (string, error) { + // Extract custom claims + var claims struct { + // "kubernetes.io": { + // "namespace": "default", + // "pod": { + // "name": "oidc-test", + // "uid": "49ad3572-b3dd-43a6-8d77-5858d3660275" + // }, + // "serviceaccount": { + // "name": "default", + // "uid": "f5720c1d-e152-4356-a897-11b07aff165d" + // } + // } + Kubernetes struct { + Namespace string `json:"namespace"` + Pod struct { + Name string `json:"name"` + UID string `json:"uid"` + } `json:"pod"` + ServiceAccount struct { + Name string `json:"name"` + UID string `json:"uid"` + } `json:"serviceaccount"` + } `json:"kubernetes.io"` + } + if err := token.Claims(&claims); err != nil { + return "", err + } + + // We use this in URIs, so it has to be a URI. + return "https://kubernetes.io/namespaces/" + claims.Kubernetes.Namespace + "/serviceaccounts/" + claims.Kubernetes.ServiceAccount.Name, nil +} + func workflowFromIDToken(token *oidc.IDToken) (string, error) { // Extract custom claims var claims struct { diff --git a/pkg/challenges/challenges_test.go b/pkg/challenges/challenges_test.go index 407c2e4a6..203a1b4ef 100644 --- a/pkg/challenges/challenges_test.go +++ b/pkg/challenges/challenges_test.go @@ -15,7 +15,15 @@ package challenges -import "testing" +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "testing" +) func Test_isSpiffeIDAllowed(t *testing.T) { tests := []struct { @@ -23,38 +31,32 @@ func Test_isSpiffeIDAllowed(t *testing.T) { host string spiffeID string want bool - }{ - { - name: "match", - host: "foobar.com", - spiffeID: "spiffe://foobar.com/stuff", - want: true, - }, - { - name: "subdomain match", - host: "foobar.com", - spiffeID: "spiffe://spife.foobar.com/stuff", - want: true, - }, - { - name: "subdomain mismatch", - host: "foo.foobar.com", - spiffeID: "spiffe://spife.foobar.com/stuff", - want: false, - }, - { - name: "inverted mismatch", - host: "foo.foobar.com", - spiffeID: "spiffe://foobar.com/stuff", - want: false, - }, - { - name: "no dot mismatch", - host: "foobar.com", - spiffeID: "spiffe://foofoobar.com/stuff", - want: false, - }, - } + }{{ + name: "match", + host: "foobar.com", + spiffeID: "spiffe://foobar.com/stuff", + want: true, + }, { + name: "subdomain match", + host: "foobar.com", + spiffeID: "spiffe://spife.foobar.com/stuff", + want: true, + }, { + name: "subdomain mismatch", + host: "foo.foobar.com", + spiffeID: "spiffe://spife.foobar.com/stuff", + want: false, + }, { + name: "inverted mismatch", + host: "foo.foobar.com", + spiffeID: "spiffe://foobar.com/stuff", + want: false, + }, { + name: "no dot mismatch", + host: "foobar.com", + spiffeID: "spiffe://foofoobar.com/stuff", + want: false, + }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := isSpiffeIDAllowed(tt.host, tt.spiffeID); got != tt.want { @@ -63,3 +65,56 @@ func Test_isSpiffeIDAllowed(t *testing.T) { }) } } + +func failErr(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } +} + +func TestCheckSignatureECDSA(t *testing.T) { + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + failErr(t, err) + + email := "test@gmail.com" + if err := CheckSignature(&priv.PublicKey, []byte("foo"), email); err == nil { + t.Fatal("check should have failed") + } + + h := sha256.Sum256([]byte(email)) + signature, err := priv.Sign(rand.Reader, h[:], crypto.SHA256) + failErr(t, err) + + if err := CheckSignature(&priv.PublicKey, signature, email); err != nil { + t.Fatal(err) + } + + // Try a bad email but "good" signature + if err := CheckSignature(&priv.PublicKey, signature, "bad@email.com"); err == nil { + t.Fatal("check should have failed") + } +} + +func TestCheckSignatureRSA(t *testing.T) { + priv, err := rsa.GenerateKey(rand.Reader, 2048) + failErr(t, err) + + email := "test@gmail.com" + if err := CheckSignature(&priv.PublicKey, []byte("foo"), email); err == nil { + t.Fatal("check should have failed") + } + + h := sha256.Sum256([]byte(email)) + signature, err := priv.Sign(rand.Reader, h[:], crypto.SHA256) + failErr(t, err) + + if err := CheckSignature(&priv.PublicKey, signature, email); err != nil { + t.Fatal(err) + } + + // Try a bad email but "good" signature + if err := CheckSignature(&priv.PublicKey, signature, "bad@email.com"); err == nil { + t.Fatal("check should have failed") + } +} diff --git a/pkg/config/config.go b/pkg/config/config.go index 54916c42d..6cbbf4e69 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -16,8 +16,10 @@ package config import ( + "crypto/x509" "encoding/json" "io/ioutil" + "net/http" "os" "github.com/sigstore/fulcio/pkg/log" @@ -39,6 +41,7 @@ type IssuerType string const ( IssuerTypeEmail = "email" IssuerTypeGithubWorkflow = "github-workflow" + IssuerTypeKubernetes = "kubernetes" IssuerTypeSpiffe = "spiffe" ) @@ -72,6 +75,7 @@ var DefaultConfig = FulcioConfig{ } var config *FulcioConfig +var originalTransport = http.DefaultTransport func Config() FulcioConfig { if config == nil { @@ -95,6 +99,33 @@ func Load(configPath string) error { if err != nil { return err } + + if _, ok := cfg.OIDCIssuers["https://kubernetes.default.svc"]; ok { + // Add the Kubernetes cluster's CA to the system CA pool, and to + // the default transport. + rootCAs, _ := x509.SystemCertPool() + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + const k8sCA = "/var/run/fulcio/ca.crt" + certs, err := ioutil.ReadFile(k8sCA) + if err != nil { + return err + } + if ok := rootCAs.AppendCertsFromPEM(certs); !ok { + return err + } + + t := originalTransport.(*http.Transport).Clone() + t.TLSClientConfig.RootCAs = rootCAs + http.DefaultTransport = t + } else { + // If we parse a config that doesn't include a cluster issuer + // signed with the cluster'sCA, then restore the original transport + // (in case we overwrote it) + http.DefaultTransport = originalTransport + } + config = &cfg log.Logger.Infof("Loaded config %v from %s", cfg, configPath) return nil