-
Notifications
You must be signed in to change notification settings - Fork 350
/
certs.go
277 lines (244 loc) · 9.09 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
// 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"
"time"
"github.com/GoogleCloudPlatform/cloudsql-proxy/logging"
"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/util"
"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
}
// 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 {
pkey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err) // very unexpected.
}
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"
}
}
return &RemoteCertSource{pkey, serv, !opts.IgnoreRegion, opts.IPAddrTypeOpts}
}
// 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 {
// 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
}
// 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() error) error {
var err error
for i := 0; i < backoffRetries; i++ {
err = do()
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)
}
return err
}
// Local returns a certificate that may be used to establish a TLS
// connection to the specified instance.
func (s *RemoteCertSource) Local(instance string) (ret tls.Certificate, err error) {
pkix, err := x509.MarshalPKIXPublicKey(&s.key.PublicKey)
if err != nil {
return ret, err
}
p, _, n := util.SplitName(instance)
req := s.serv.SslCerts.CreateEphemeral(p, n,
&sqladmin.SslCertsCreateEphemeralRequest{
PublicKey: string(pem.EncodeToMemory(&pem.Block{Bytes: pkix, Type: "RSA PUBLIC KEY"})),
},
)
var data *sqladmin.SslCert
err = backoffAPIRetry("createEphemeral for", instance, func() error {
data, err = req.Do()
return err
})
if err != nil {
return ret, err
}
c, err := parseCert(data.Cert)
if err != nil {
return ret, fmt.Errorf("couldn't parse ephemeral certificate for instance %q: %v", instance, err)
}
return tls.Certificate{
Certificate: [][]byte{c.Raw},
PrivateKey: s.key,
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)
}
// Find the first matching IP address by user input IP address types
func (s *RemoteCertSource) findIPAddr(data *sqladmin.DatabaseInstance, 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 string, err error) {
p, region, n := util.SplitName(instance)
req := s.serv.Instances.Get(p, n)
var data *sqladmin.DatabaseInstance
err = backoffAPIRetry("get instance", instance, func() error {
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, err
}