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