Skip to content

Commit

Permalink
JWKSCache: add option to set CA certificate to trust (#81)
Browse files Browse the repository at this point in the history
This is helpful when the JWKS is located on a HTTPS endpoint and the certificate is signed by a custom CA.

Signed-off-by: ItalyPaleAle <[email protected]>
  • Loading branch information
ItalyPaleAle authored Jan 11, 2024
1 parent 77f7f03 commit c24d1d2
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 4 deletions.
32 changes: 28 additions & 4 deletions jwkscache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package jwkscache
import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
Expand All @@ -38,6 +39,7 @@ import (

"github.com/dapr/kit/fswatcher"
"github.com/dapr/kit/logger"
"github.com/dapr/kit/utils"
)

const (
Expand All @@ -49,11 +51,11 @@ const (

// JWKSCache is a cache of JWKS objects.
// It fetches a JWKS object from a file on disk, a URL, or from a value passed as-is.
// TODO: Move this to dapr/kit and use it for the JWKS crypto component too
type JWKSCache struct {
location string
requestTimeout time.Duration
minRefreshInterval time.Duration
caCertificate string

jwks jwk.Set
logger logger.Logger
Expand Down Expand Up @@ -113,6 +115,12 @@ func (c *JWKSCache) SetMinRefreshInterval(minRefreshInterval time.Duration) {
c.minRefreshInterval = minRefreshInterval
}

// SetCACertificate sets the CA certificate to trust.
// Can be a path to a local file or an actual, PEM-encoded certificate
func (c *JWKSCache) SetCACertificate(caCertificate string) {
c.caCertificate = caCertificate
}

// SetHTTPClient sets the HTTP client object to use.
func (c *JWKSCache) SetHTTPClient(client *http.Client) {
c.client = client
Expand Down Expand Up @@ -184,12 +192,28 @@ func (c *JWKSCache) initJWKSFromURL(ctx context.Context, url string) error {

// We also need to create a custom HTTP client (if we don't have one already) because otherwise there's no timeout.
if c.client == nil {
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}

// Load CA certificates if we have one
if c.caCertificate != "" {
caCert, err := utils.GetPEM(c.caCertificate)
if err != nil {
return fmt.Errorf("failed to load CA certificate: %w", err)
}

caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return errors.New("failed to add root certificate to certificate pool")
}
tlsConfig.RootCAs = caCertPool
}

c.client = &http.Client{
Timeout: c.requestTimeout,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
TLSClientConfig: tlsConfig,
},
}
}
Expand Down
41 changes: 41 additions & 0 deletions utils/pem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
Copyright 2023 The Dapr 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 utils

import (
"encoding/pem"
"fmt"
"os"
)

// GetPEM loads a PEM-encoded file (certificate or key).
func GetPEM(val string) ([]byte, error) {
// If val is already a PEM-encoded string, return it as-is
if IsValidPEM(val) {
return []byte(val), nil
}

// Assume it's a file
pemBytes, err := os.ReadFile(val)
if err != nil {
return nil, fmt.Errorf("value is neither a valid file path or nor a valid PEM-encoded string: %w", err)
}
return pemBytes, nil
}

// IsValidPEM validates the provided input has PEM formatted block.
func IsValidPEM(val string) bool {
block, _ := pem.Decode([]byte(val))
return block != nil
}

0 comments on commit c24d1d2

Please sign in to comment.