| // +build go1.13 |
| |
| /* |
| * |
| * Copyright 2020 gRPC 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 meshca |
| |
| import ( |
| "crypto/x509" |
| "encoding/json" |
| "fmt" |
| "sync" |
| |
| "google.golang.org/grpc" |
| "google.golang.org/grpc/credentials" |
| "google.golang.org/grpc/credentials/sts" |
| "google.golang.org/grpc/credentials/tls/certprovider" |
| "google.golang.org/grpc/internal/backoff" |
| ) |
| |
| const pluginName = "mesh_ca" |
| |
| // For overriding in unit tests. |
| var ( |
| grpcDialFunc = grpc.Dial |
| backoffFunc = backoff.DefaultExponential.Backoff |
| ) |
| |
| func init() { |
| certprovider.Register(newPluginBuilder()) |
| } |
| |
| func newPluginBuilder() *pluginBuilder { |
| return &pluginBuilder{clients: make(map[ccMapKey]*refCountedCC)} |
| } |
| |
| // Key for the map containing ClientConns to the MeshCA server. Only the server |
| // name and the STS options (which is used to create call creds) from the plugin |
| // configuration determine if two configs can share the same ClientConn. Hence |
| // only those form the key to this map. |
| type ccMapKey struct { |
| name string |
| stsOpts sts.Options |
| } |
| |
| // refCountedCC wraps a grpc.ClientConn to MeshCA along with a reference count. |
| type refCountedCC struct { |
| cc *grpc.ClientConn |
| refCnt int |
| } |
| |
| // pluginBuilder is an implementation of the certprovider.Builder interface, |
| // which build certificate provider instances which get certificates signed from |
| // the MeshCA. |
| type pluginBuilder struct { |
| // A collection of ClientConns to the MeshCA server along with a reference |
| // count. Provider instances whose config point to the same server name will |
| // end up sharing the ClientConn. |
| mu sync.Mutex |
| clients map[ccMapKey]*refCountedCC |
| } |
| |
| // Build returns a MeshCA certificate provider for the passed in configuration |
| // and options. |
| // |
| // This builder takes care of sharing the ClientConn to the MeshCA server among |
| // different plugin instantiations. |
| func (b *pluginBuilder) Build(c certprovider.StableConfig, opts certprovider.Options) certprovider.Provider { |
| cfg, ok := c.(*pluginConfig) |
| if !ok { |
| // This is not expected when passing config returned by ParseConfig(). |
| // This could indicate a bug in the certprovider.Store implementation or |
| // in cases where the user is directly using these APIs, could be a user |
| // error. |
| logger.Errorf("unsupported config type: %T", c) |
| return nil |
| } |
| |
| b.mu.Lock() |
| defer b.mu.Unlock() |
| |
| ccmk := ccMapKey{ |
| name: cfg.serverURI, |
| stsOpts: cfg.stsOpts, |
| } |
| rcc, ok := b.clients[ccmk] |
| if !ok { |
| // STS call credentials take care of exchanging a locally provisioned |
| // JWT token for an access token which will be accepted by the MeshCA. |
| callCreds, err := sts.NewCredentials(cfg.stsOpts) |
| if err != nil { |
| logger.Errorf("sts.NewCredentials() failed: %v", err) |
| return nil |
| } |
| |
| // MeshCA is a public endpoint whose certificate is Web-PKI compliant. |
| // So, we just need to use the system roots to authenticate the MeshCA. |
| cp, err := x509.SystemCertPool() |
| if err != nil { |
| logger.Errorf("x509.SystemCertPool() failed: %v", err) |
| return nil |
| } |
| transportCreds := credentials.NewClientTLSFromCert(cp, "") |
| |
| cc, err := grpcDialFunc(cfg.serverURI, grpc.WithTransportCredentials(transportCreds), grpc.WithPerRPCCredentials(callCreds)) |
| if err != nil { |
| logger.Errorf("grpc.Dial(%s) failed: %v", cfg.serverURI, err) |
| return nil |
| } |
| |
| rcc = &refCountedCC{cc: cc} |
| b.clients[ccmk] = rcc |
| } |
| rcc.refCnt++ |
| |
| p := newProviderPlugin(providerParams{ |
| cc: rcc.cc, |
| cfg: cfg, |
| opts: opts, |
| backoff: backoffFunc, |
| doneFunc: func() { |
| // The plugin implementation will invoke this function when it is |
| // being closed, and here we take care of closing the ClientConn |
| // when there are no more plugins using it. We need to acquire the |
| // lock before accessing the rcc from the enclosing function. |
| b.mu.Lock() |
| defer b.mu.Unlock() |
| rcc.refCnt-- |
| if rcc.refCnt == 0 { |
| logger.Infof("Closing grpc.ClientConn to %s", ccmk.name) |
| rcc.cc.Close() |
| delete(b.clients, ccmk) |
| } |
| }, |
| }) |
| return p |
| } |
| |
| // ParseConfig parses the configuration to be passed to the MeshCA plugin |
| // implementation. Expects the config to be a json.RawMessage which contains a |
| // serialized JSON representation of the meshca_experimental.GoogleMeshCaConfig |
| // proto message. |
| func (b *pluginBuilder) ParseConfig(c interface{}) (certprovider.StableConfig, error) { |
| data, ok := c.(json.RawMessage) |
| if !ok { |
| return nil, fmt.Errorf("meshca: unsupported config type: %T", c) |
| } |
| return pluginConfigFromJSON(data) |
| } |
| |
| // Name returns the MeshCA plugin name. |
| func (b *pluginBuilder) Name() string { |
| return pluginName |
| } |