Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tls verification on OIDC issuers #1932

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
37 changes: 32 additions & 5 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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
Expand Down
155 changes: 155 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
}

Expand Down