-
Notifications
You must be signed in to change notification settings - Fork 345
/
certs.go
367 lines (327 loc) · 12.1 KB
/
certs.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 certs implements a CertSource which speaks to the public Cloud SQL API endpoint.
package certs
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"math"
mrand "math/rand"
"net/http"
"strings"
"sync"
"time"
"github.com/GoogleCloudPlatform/cloudsql-proxy/logging"
"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/util"
"golang.org/x/oauth2"
"google.golang.org/api/googleapi"
sqladmin "google.golang.org/api/sqladmin/v1beta4"
)
const defaultUserAgent = "custom cloud_sql_proxy version >= 1.10"
// NewCertSource returns a CertSource which can be used to authenticate using
// the provided client, which must not be nil.
//
// This function is deprecated; use NewCertSourceOpts instead.
func NewCertSource(host string, c *http.Client, checkRegion bool) *RemoteCertSource {
return NewCertSourceOpts(c, RemoteOpts{
APIBasePath: host,
IgnoreRegion: !checkRegion,
UserAgent: defaultUserAgent,
})
}
// RemoteOpts are a collection of options for NewCertSourceOpts. All fields are
// optional.
type RemoteOpts struct {
// APIBasePath specifies the base path for the sqladmin API. If left blank,
// the default from the autogenerated sqladmin library is used (which is
// sufficient for nearly all users)
APIBasePath string
// IgnoreRegion specifies whether a missing or mismatched region in the
// instance name should be ignored. In a future version this value will be
// forced to 'false' by the RemoteCertSource.
IgnoreRegion bool
// A string for the RemoteCertSource to identify itself when contacting the
// sqladmin API.
UserAgent string
// IP address type options
IPAddrTypeOpts []string
// Enable IAM proxy db authentication
EnableIAMLogin bool
// Token source for token information used in cert creation
TokenSource oauth2.TokenSource
// DelayKeyGenerate, if true, causes the RSA key to be generated lazily
// on the first connection to a database. The default behavior is to generate
// the key when the CertSource is created.
DelayKeyGenerate bool
}
// NewCertSourceOpts returns a CertSource configured with the provided Opts.
// The provided http.Client must not be nil.
//
// Use this function instead of NewCertSource; it has a more forward-compatible
// signature.
func NewCertSourceOpts(c *http.Client, opts RemoteOpts) *RemoteCertSource {
serv, err := sqladmin.New(c)
if err != nil {
panic(err) // Only will happen if the provided client is nil.
}
if opts.APIBasePath != "" {
serv.BasePath = opts.APIBasePath
}
ua := opts.UserAgent
if ua == "" {
ua = defaultUserAgent
}
serv.UserAgent = ua
// Set default value to be "PUBLIC,PRIVATE" if not specified
if len(opts.IPAddrTypeOpts) == 0 {
opts.IPAddrTypeOpts = []string{"PUBLIC", "PRIVATE"}
}
// Add "PUBLIC" as an alias for "PRIMARY"
for index, ipAddressType := range opts.IPAddrTypeOpts {
if strings.ToUpper(ipAddressType) == "PUBLIC" {
opts.IPAddrTypeOpts[index] = "PRIMARY"
}
}
certSource := &RemoteCertSource{
serv: serv,
checkRegion: !opts.IgnoreRegion,
IPAddrTypes: opts.IPAddrTypeOpts,
EnableIAMLogin: opts.EnableIAMLogin,
TokenSource: opts.TokenSource,
}
if !opts.DelayKeyGenerate {
// Generate the RSA key now, but don't block on it.
go certSource.generateKey()
}
return certSource
}
// RemoteCertSource implements a CertSource, using Cloud SQL APIs to
// return Local certificates for identifying oneself as a specific user
// to the remote instance and Remote certificates for confirming the
// remote database's identity.
type RemoteCertSource struct {
// keyOnce is used to create `key` lazily.
keyOnce sync.Once
// key is the private key used for certificates returned by Local.
key *rsa.PrivateKey
// serv is used to make authenticated API calls to Cloud SQL.
serv *sqladmin.Service
// If set, providing an incorrect region in their connection string will be
// treated as an error. This is to provide the same functionality that will
// occur when API calls require the region.
checkRegion bool
// a list of ip address types that users select
IPAddrTypes []string
// flag to enable IAM proxy db authentication
EnableIAMLogin bool
// token source for the token information used in cert creation
TokenSource oauth2.TokenSource
}
// Constants for backoffAPIRetry. These cause the retry logic to scale the
// backoff delay from 200ms to around 3.5s.
const (
baseBackoff = float64(200 * time.Millisecond)
backoffMult = 1.618
backoffRetries = 5
)
func backoffAPIRetry(desc, instance string, do func(staleRead time.Time) error) error {
var (
err error
t time.Time
)
for i := 0; i < backoffRetries; i++ {
err = do(t)
gErr, ok := err.(*googleapi.Error)
switch {
case !ok:
// 'ok' will also be false if err is nil.
return err
case gErr.Code == 403 && len(gErr.Errors) > 0 && gErr.Errors[0].Reason == "insufficientPermissions":
// The case where the admin API has not yet been enabled.
return fmt.Errorf("ensure that the Cloud SQL API is enabled for your project (https://console.cloud.google.com/flows/enableapi?apiid=sqladmin). Error during %s %s: %v", desc, instance, err)
case gErr.Code == 404 || gErr.Code == 403:
return fmt.Errorf("ensure that the account has access to %q (and make sure there's no typo in that name). Error during %s %s: %v", instance, desc, instance, err)
case gErr.Code < 500:
// Only Server-level HTTP errors are immediately retryable.
return err
}
// sleep = baseBackoff * backoffMult^(retries + randomFactor)
exp := float64(i+1) + mrand.Float64()
sleep := time.Duration(baseBackoff * math.Pow(backoffMult, exp))
logging.Errorf("Error in %s %s: %v; retrying in %v", desc, instance, err, sleep)
time.Sleep(sleep)
// Create timestamp 30 seconds before now for stale read requests
t = time.Now().UTC().Add(-30 * time.Second)
}
return err
}
func refreshToken(ts oauth2.TokenSource, tok *oauth2.Token) (*oauth2.Token, error) {
expiredToken := &oauth2.Token{
AccessToken: tok.AccessToken,
TokenType: tok.TokenType,
RefreshToken: tok.RefreshToken,
Expiry: time.Time{}.Add(1), // Expired
}
return oauth2.ReuseTokenSource(expiredToken, ts).Token()
}
// Local returns a certificate that may be used to establish a TLS
// connection to the specified instance.
func (s *RemoteCertSource) Local(instance string) (tls.Certificate, error) {
pkix, err := x509.MarshalPKIXPublicKey(s.generateKey().Public())
if err != nil {
return tls.Certificate{}, err
}
p, r, n := util.SplitName(instance)
regionName := fmt.Sprintf("%s~%s", r, n)
pubKey := string(pem.EncodeToMemory(&pem.Block{Bytes: pkix, Type: "RSA PUBLIC KEY"}))
generateEphemeralCertRequest := &sqladmin.GenerateEphemeralCertRequest{
PublicKey: pubKey,
}
var tok *oauth2.Token
// If IAM login is enabled, add the OAuth2 token into the ephemeral
// certificate request.
if s.EnableIAMLogin {
var tokErr error
tok, tokErr = s.TokenSource.Token()
if tokErr != nil {
return tls.Certificate{}, tokErr
}
// Always refresh the token to ensure its expiration is far enough in
// the future.
tok, tokErr = refreshToken(s.TokenSource, tok)
if tokErr != nil {
return tls.Certificate{}, tokErr
}
// TODO: remove this once issue with OAuth2 Tokens is resolved.
// See https://github.com/GoogleCloudPlatform/cloudsql-proxy/issues/852.
generateEphemeralCertRequest.AccessToken = strings.TrimRight(tok.AccessToken, ".")
}
req := s.serv.Connect.GenerateEphemeralCert(p, regionName, generateEphemeralCertRequest)
var data *sqladmin.GenerateEphemeralCertResponse
err = backoffAPIRetry("generateEphemeral for", instance, func(staleRead time.Time) error {
if !staleRead.IsZero() {
generateEphemeralCertRequest.ReadTime = staleRead.Format(time.RFC3339)
}
data, err = req.Do()
return err
})
if err != nil {
return tls.Certificate{}, err
}
c, err := parseCert(data.EphemeralCert.Cert)
if err != nil {
return tls.Certificate{}, fmt.Errorf("couldn't parse ephemeral certificate for instance %q: %v", instance, err)
}
if s.EnableIAMLogin {
// Adjust the certificate's expiration to be the earlier of tok.Expiry or c.NotAfter
if tok.Expiry.Before(c.NotAfter) {
c.NotAfter = tok.Expiry
}
}
return tls.Certificate{
Certificate: [][]byte{c.Raw},
PrivateKey: s.generateKey(),
Leaf: c,
}, nil
}
func parseCert(pemCert string) (*x509.Certificate, error) {
bl, _ := pem.Decode([]byte(pemCert))
if bl == nil {
return nil, errors.New("invalid PEM: " + pemCert)
}
return x509.ParseCertificate(bl.Bytes)
}
// Return the RSA private key, which is lazily initialized.
func (s *RemoteCertSource) generateKey() *rsa.PrivateKey {
s.keyOnce.Do(func() {
start := time.Now()
pkey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err) // very unexpected.
}
logging.Verbosef("Generated RSA key in %v", time.Since(start))
s.key = pkey
})
return s.key
}
// Find the first matching IP address by user input IP address types
func (s *RemoteCertSource) findIPAddr(data *sqladmin.ConnectSettings, instance string) (ipAddrInUse string, err error) {
for _, eachIPAddrTypeByUser := range s.IPAddrTypes {
for _, eachIPAddrTypeOfInstance := range data.IpAddresses {
if strings.ToUpper(eachIPAddrTypeOfInstance.Type) == strings.ToUpper(eachIPAddrTypeByUser) {
ipAddrInUse = eachIPAddrTypeOfInstance.IpAddress
return ipAddrInUse, nil
}
}
}
ipAddrTypesOfInstance := ""
for _, eachIPAddrTypeOfInstance := range data.IpAddresses {
ipAddrTypesOfInstance += fmt.Sprintf("(TYPE=%v, IP_ADDR=%v)", eachIPAddrTypeOfInstance.Type, eachIPAddrTypeOfInstance.IpAddress)
}
ipAddrTypeOfUser := fmt.Sprintf("%v", s.IPAddrTypes)
return "", fmt.Errorf("User input IP address type %v does not match the instance %v, the instance's IP addresses are %v ", ipAddrTypeOfUser, instance, ipAddrTypesOfInstance)
}
// Remote returns the specified instance's CA certificate, address, and name.
func (s *RemoteCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) {
p, region, n := util.SplitName(instance)
regionName := fmt.Sprintf("%s~%s", region, n)
req := s.serv.Connect.Get(p, regionName)
var data *sqladmin.ConnectSettings
err = backoffAPIRetry("get instance", instance, func(staleRead time.Time) error {
if !staleRead.IsZero() {
req.ReadTime(staleRead.Format(time.RFC3339))
}
data, err = req.Do()
return err
})
if err != nil {
return nil, "", "", "", err
}
// TODO(chowski): remove this when us-central is removed.
if data.Region == "us-central" {
data.Region = "us-central1"
}
if data.Region != region {
if region == "" {
err = fmt.Errorf("instance %v doesn't provide region", instance)
} else {
err = fmt.Errorf(`for connection string "%s": got region %q, want %q`, instance, region, data.Region)
}
if s.checkRegion {
return nil, "", "", "", err
}
logging.Errorf("%v", err)
logging.Errorf("WARNING: specifying the correct region in an instance string will become required in a future version!")
}
if len(data.IpAddresses) == 0 {
return nil, "", "", "", fmt.Errorf("no IP address found for %v", instance)
}
if data.BackendType == "FIRST_GEN" {
logging.Errorf("WARNING: proxy client does not support first generation Cloud SQL instances.")
return nil, "", "", "", fmt.Errorf("%q is a first generation instance", instance)
}
// Find the first matching IP address by user input IP address types
ipAddrInUse := ""
ipAddrInUse, err = s.findIPAddr(data, instance)
if err != nil {
return nil, "", "", "", err
}
c, err := parseCert(data.ServerCaCert.Cert)
return c, ipAddrInUse, p + ":" + n, data.DatabaseVersion, err
}