diff --git a/pkg/config/config.go b/pkg/config/config.go index f93680b8c..8e5d64e35 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -116,6 +116,11 @@ type OIDCIssuer struct { // Optional, the contact for the issuer team // Usually it is a email Contact string `json:"Contact,omitempty" yaml:"contact,omitempty"` + + // CACert is an optional parameter that holds the CA certificate in PEM format. + // This is used to trust the TLS certificate signed by an internal CA when interacting + // with some OIDC providers, preventing x509 certificate verification failures. + CACert string `json:"CACert,omitempty" yaml:"ca-cert,omitempty"` } func metaRegex(issuer string) (*regexp.Regexp, error) { @@ -176,7 +181,6 @@ func (fc *FulcioConfig) GetVerifier(issuerURL string, opts ...InsecureOIDCConfig for _, o := range opts { o(cfg) } - // Look up our fixed issuer verifiers v, ok := fc.verifiers[issuerURL] if ok { for _, c := range v { @@ -186,7 +190,6 @@ func (fc *FulcioConfig) GetVerifier(issuerURL string, opts ...InsecureOIDCConfig } } - // Look in the LRU cache for a verifier untyped, ok := fc.lru.Get(issuerURL) if ok { v := untyped.([]*verifierWithConfig) @@ -197,12 +200,36 @@ func (fc *FulcioConfig) GetVerifier(issuerURL string, opts ...InsecureOIDCConfig } } - // If this issuer hasn't been recently used, or we have special config options, then create a new verifier - // and add it to the LRU cache. + // clone rather than modifying the default transport + var transportClone *http.Transport + if iss.CACert != "" { + rootCAs, _ := x509.SystemCertPool() + if rootCAs == nil { + rootCAs = x509.NewCertPool() + } + if ok := rootCAs.AppendCertsFromPEM([]byte(iss.CACert)); !ok { + log.Logger.Warnf("Failed to append custom CA cert for issuer URL %q", issuerURL) + return nil, false + } + + // If the transport is nil, it will panic. Use the default transport if it is nil. + if t, ok := originalTransport.(*http.Transport); ok { + transportClone = t.Clone() + transportClone.TLSClientConfig.RootCAs = rootCAs + } + } ctx, cancel := context.WithTimeout(context.Background(), defaultOIDCDiscoveryTimeout) defer cancel() - provider, err := oidc.NewProvider(ctx, issuerURL) + + var client *http.Client + if transportClone != nil { + client = &http.Client{Transport: transportClone} + } else { + client = http.DefaultClient + } + + provider, err := oidc.NewProvider(oidc.ClientContext(ctx, client), issuerURL) if err != nil { log.Logger.Warnf("Failed to create provider for issuer URL %q: %v", issuerURL, err) return nil, false diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 6f7095e6a..77282168a 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -16,11 +16,24 @@ package config import ( + "bytes" "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" "fmt" + "math/big" + "net" + "net/http" + "net/http/httptest" "net/url" "reflect" "testing" + "time" "github.com/coreos/go-oidc/v3/oidc" lru "github.com/hashicorp/golang-lru" @@ -703,6 +716,148 @@ func TestVerifierCache(t *testing.T) { } } +func TestVerifierCacheWithCustomCA(t *testing.T) { + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2024), + Subject: pkix.Name{ + CommonName: "Test CA", + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + IsCA: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + } + + caPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + t.Fatal(err) + } + + caPEM := new(bytes.Buffer) + pem.Encode(caPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + + serverCert := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "localhost", + }, + DNSNames: []string{"localhost"}, + IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + + serverPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + + serverCertBytes, err := x509.CreateCertificate( + rand.Reader, + serverCert, + ca, + &serverPrivKey.PublicKey, + caPrivKey, + ) + if err != nil { + t.Fatal(err) + } + + serverCertPEM := new(bytes.Buffer) + pem.Encode(serverCertPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: serverCertBytes, + }) + + serverKeyPEM := new(bytes.Buffer) + pem.Encode(serverKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(serverPrivKey), + }) + + serverTLSCert, err := tls.X509KeyPair(serverCertPEM.Bytes(), serverKeyPEM.Bytes()) + if err != nil { + t.Fatal(err) + } + + var server *httptest.Server + server = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/.well-known/openid-configuration": + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{ + "issuer": server.URL, + "jwks_uri": server.URL + "/keys", + }) + case "/keys": + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "keys": []interface{}{}, + }) + default: + http.NotFound(w, r) + } + })) + + server.TLS = &tls.Config{ + Certificates: []tls.Certificate{serverTLSCert}, + } + server.StartTLS() + defer server.Close() + + cache, err := lru.New2Q(100) + if err != nil { + t.Fatal(err) + } + + // use custom CA for OIDC issuer + fc := &FulcioConfig{ + OIDCIssuers: map[string]OIDCIssuer{ + server.URL: { + IssuerURL: server.URL, + ClientID: "sigstore", + CACert: caPEM.String(), + }, + }, + verifiers: make(map[string][]*verifierWithConfig), + lru: cache, + } + + verifier, ok := fc.GetVerifier(server.URL) + if !ok { + t.Fatal("expected to get verifier") + } + if verifier == nil { + t.Fatal("expected non-nil verifier") + } + + cachedVerifier, ok := fc.GetVerifier(server.URL) + if !ok { + t.Fatal("expected to get cached verifier") + } + if !reflect.DeepEqual(verifier, cachedVerifier) { + t.Fatal("cached verifier doesn't match original verifier") + } + + verifierWithOptions, ok := fc.GetVerifier(server.URL, WithSkipExpiryCheck()) + if !ok { + t.Fatal("expected to get verifier with options") + } + if reflect.DeepEqual(verifier, verifierWithOptions) { + t.Fatal("verifier with options shouldn't match original verifier") + } +} + type mockKeySet struct { }