x509_provider.go raw

   1  // Copyright 2024 Google LLC
   2  //
   3  // Licensed under the Apache License, Version 2.0 (the "License");
   4  // you may not use this file except in compliance with the License.
   5  // You may obtain a copy of the License at
   6  //
   7  //      http://www.apache.org/licenses/LICENSE-2.0
   8  //
   9  // Unless required by applicable law or agreed to in writing, software
  10  // distributed under the License is distributed on an "AS IS" BASIS,
  11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12  // See the License for the specific language governing permissions and
  13  // limitations under the License.
  14  
  15  package externalaccount
  16  
  17  import (
  18  	"context"
  19  	"crypto/tls"
  20  	"crypto/x509"
  21  	"encoding/base64"
  22  	"encoding/json"
  23  	"encoding/pem"
  24  	"errors"
  25  	"fmt"
  26  	"io/fs"
  27  	"net/http"
  28  	"os"
  29  	"strings"
  30  	"time"
  31  
  32  	"cloud.google.com/go/auth/internal/transport/cert"
  33  )
  34  
  35  // x509Provider implements the subjectTokenProvider type for x509 workload
  36  // identity credentials. This provider retrieves and formats a JSON array
  37  // containing the leaf certificate and trust chain (if provided) as
  38  // base64-encoded strings. This JSON array serves as the subject token for
  39  // mTLS authentication.
  40  type x509Provider struct {
  41  	// TrustChainPath is the path to the file containing the trust chain certificates.
  42  	// The file should contain one or more PEM-encoded certificates.
  43  	TrustChainPath string
  44  	// ConfigFilePath is the path to the configuration file containing the path
  45  	// to the leaf certificate file.
  46  	ConfigFilePath string
  47  }
  48  
  49  const pemCertificateHeader = "-----BEGIN CERTIFICATE-----"
  50  
  51  func (xp *x509Provider) providerType() string {
  52  	return x509ProviderType
  53  }
  54  
  55  // loadLeafCertificate loads and parses the leaf certificate from the specified
  56  // configuration file. It retrieves the certificate path from the config file,
  57  // reads the certificate file, and parses the certificate data.
  58  func loadLeafCertificate(configFilePath string) (*x509.Certificate, error) {
  59  	// Get the path to the certificate file from the configuration file.
  60  	path, err := cert.GetCertificatePath(configFilePath)
  61  	if err != nil {
  62  		return nil, fmt.Errorf("failed to get certificate path from config file: %w", err)
  63  	}
  64  	leafCertBytes, err := os.ReadFile(path)
  65  	if err != nil {
  66  		return nil, fmt.Errorf("failed to read leaf certificate file: %w", err)
  67  	}
  68  	// Parse the certificate bytes.
  69  	return parseCertificate(leafCertBytes)
  70  }
  71  
  72  // encodeCert encodes a x509.Certificate to a base64 string.
  73  func encodeCert(cert *x509.Certificate) string {
  74  	// cert.Raw contains the raw DER-encoded certificate. Encode the raw certificate bytes to base64.
  75  	return base64.StdEncoding.EncodeToString(cert.Raw)
  76  }
  77  
  78  // parseCertificate parses a PEM-encoded certificate from the given byte slice.
  79  func parseCertificate(certData []byte) (*x509.Certificate, error) {
  80  	if len(certData) == 0 {
  81  		return nil, errors.New("invalid certificate data: empty input")
  82  	}
  83  	// Decode the PEM-encoded data.
  84  	block, _ := pem.Decode(certData)
  85  	if block == nil {
  86  		return nil, errors.New("invalid PEM-encoded certificate data: no PEM block found")
  87  	}
  88  	if block.Type != "CERTIFICATE" {
  89  		return nil, fmt.Errorf("invalid PEM-encoded certificate data: expected CERTIFICATE block type, got %s", block.Type)
  90  	}
  91  	// Parse the DER-encoded certificate.
  92  	certificate, err := x509.ParseCertificate(block.Bytes)
  93  	if err != nil {
  94  		return nil, fmt.Errorf("failed to parse certificate: %w", err)
  95  	}
  96  	return certificate, nil
  97  }
  98  
  99  // readTrustChain reads a file of PEM-encoded X.509 certificates and returns a slice of parsed certificates.
 100  // It splits the file content into PEM certificate blocks and parses each one.
 101  func readTrustChain(trustChainPath string) ([]*x509.Certificate, error) {
 102  	certificateTrustChain := []*x509.Certificate{}
 103  
 104  	// If no trust chain path is provided, return an empty slice.
 105  	if trustChainPath == "" {
 106  		return certificateTrustChain, nil
 107  	}
 108  
 109  	// Read the trust chain file.
 110  	trustChainData, err := os.ReadFile(trustChainPath)
 111  	if err != nil {
 112  		if errors.Is(err, fs.ErrNotExist) {
 113  			return nil, fmt.Errorf("trust chain file not found: %w", err)
 114  		}
 115  		return nil, fmt.Errorf("failed to read trust chain file: %w", err)
 116  	}
 117  
 118  	// Split the file content into PEM certificate blocks.
 119  	certBlocks := strings.Split(string(trustChainData), pemCertificateHeader)
 120  
 121  	// Iterate over each certificate block.
 122  	for _, certBlock := range certBlocks {
 123  		// Trim whitespace from the block.
 124  		certBlock = strings.TrimSpace(certBlock)
 125  
 126  		if certBlock != "" {
 127  			// Add the PEM header to the block.
 128  			certData := pemCertificateHeader + "\n" + certBlock
 129  
 130  			// Parse the certificate data.
 131  			cert, err := parseCertificate([]byte(certData))
 132  			if err != nil {
 133  				return nil, fmt.Errorf("error parsing certificate from trust chain file: %w", err)
 134  			}
 135  
 136  			// Append the certificate to the trust chain.
 137  			certificateTrustChain = append(certificateTrustChain, cert)
 138  		}
 139  	}
 140  
 141  	return certificateTrustChain, nil
 142  }
 143  
 144  // subjectToken retrieves the X.509 subject token. It loads the leaf
 145  // certificate and, if a trust chain path is configured, the trust chain
 146  // certificates. It then constructs a JSON array containing the base64-encoded
 147  // leaf certificate and each base64-encoded certificate in the trust chain.
 148  // The leaf certificate must be at the top of the trust chain file. This JSON
 149  // array is used as the subject token for mTLS authentication.
 150  func (xp *x509Provider) subjectToken(context.Context) (string, error) {
 151  	// Load the leaf certificate.
 152  	leafCert, err := loadLeafCertificate(xp.ConfigFilePath)
 153  	if err != nil {
 154  		return "", fmt.Errorf("failed to load leaf certificate: %w", err)
 155  	}
 156  
 157  	// Read the trust chain.
 158  	trustChain, err := readTrustChain(xp.TrustChainPath)
 159  	if err != nil {
 160  		return "", fmt.Errorf("failed to read trust chain: %w", err)
 161  	}
 162  
 163  	// Initialize the certificate chain with the leaf certificate.
 164  	certChain := []string{encodeCert(leafCert)}
 165  
 166  	// If there is a trust chain, add certificates to the certificate chain.
 167  	if len(trustChain) > 0 {
 168  		firstCert := encodeCert(trustChain[0])
 169  
 170  		// If the first certificate in the trust chain is not the same as the leaf certificate, add it to the chain.
 171  		if firstCert != certChain[0] {
 172  			certChain = append(certChain, firstCert)
 173  		}
 174  
 175  		// Iterate over the remaining certificates in the trust chain.
 176  		for i := 1; i < len(trustChain); i++ {
 177  			encoded := encodeCert(trustChain[i])
 178  
 179  			// Return an error if the current certificate is the same as the leaf certificate.
 180  			if encoded == certChain[0] {
 181  				return "", errors.New("the leaf certificate must be at the top of the trust chain file")
 182  			}
 183  
 184  			// Add the current certificate to the chain.
 185  			certChain = append(certChain, encoded)
 186  		}
 187  	}
 188  
 189  	// Convert the certificate chain to a JSON array of base64-encoded strings.
 190  	jsonChain, err := json.Marshal(certChain)
 191  	if err != nil {
 192  		return "", fmt.Errorf("failed to format certificate data: %w", err)
 193  	}
 194  
 195  	// Return the JSON-formatted certificate chain.
 196  	return string(jsonChain), nil
 197  
 198  }
 199  
 200  // createX509Client creates a new client that is configured with mTLS, using the
 201  // certificate configuration specified in the credential source.
 202  func createX509Client(certificateConfigLocation string) (*http.Client, error) {
 203  	certProvider, err := cert.NewWorkloadX509CertProvider(certificateConfigLocation)
 204  	if err != nil {
 205  		return nil, err
 206  	}
 207  	trans := http.DefaultTransport.(*http.Transport).Clone()
 208  
 209  	trans.TLSClientConfig = &tls.Config{
 210  		GetClientCertificate: certProvider,
 211  	}
 212  
 213  	// Create a client with default settings plus the X509 workload cert and key.
 214  	client := &http.Client{
 215  		Transport: trans,
 216  		Timeout:   30 * time.Second,
 217  	}
 218  
 219  	return client, nil
 220  }
 221