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 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464
|
// +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 (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"math/big"
"net"
"reflect"
"testing"
"time"
"github.com/golang/protobuf/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/tls/certprovider"
configpb "google.golang.org/grpc/credentials/tls/certprovider/meshca/internal/meshca_experimental"
meshgrpc "google.golang.org/grpc/credentials/tls/certprovider/meshca/internal/v1"
meshpb "google.golang.org/grpc/credentials/tls/certprovider/meshca/internal/v1"
"google.golang.org/grpc/internal/testutils"
)
const (
// Used when waiting for something that is expected to *not* happen.
defaultTestShortTimeout = 10 * time.Millisecond
defaultTestTimeout = 5 * time.Second
defaultTestCertLife = time.Hour
shortTestCertLife = 2 * time.Second
maxErrCount = 2
)
// fakeCA provides a very simple fake implementation of the certificate signing
// service as exported by the MeshCA.
type fakeCA struct {
meshgrpc.UnimplementedMeshCertificateServiceServer
withErrors bool // Whether the CA returns errors to begin with.
withShortLife bool // Whether to create certs with short lifetime
ccChan *testutils.Channel // Channel to get notified about CreateCertificate calls.
errors int // Error count.
key *rsa.PrivateKey // Private key of CA.
cert *x509.Certificate // Signing certificate.
certPEM []byte // PEM encoding of signing certificate.
}
// Returns a new instance of the fake Mesh CA. It generates a new RSA key and a
// self-signed certificate which will be used to sign CSRs received in incoming
// requests.
// withErrors controls whether the fake returns errors before succeeding, while
// withShortLife controls whether the fake returns certs with very small
// lifetimes (to test plugin refresh behavior). Every time a CreateCertificate()
// call succeeds, an event is pushed on the ccChan.
func newFakeMeshCA(ccChan *testutils.Channel, withErrors, withShortLife bool) (*fakeCA, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, fmt.Errorf("RSA key generation failed: %v", err)
}
now := time.Now()
tmpl := &x509.Certificate{
Subject: pkix.Name{CommonName: "my-fake-ca"},
SerialNumber: big.NewInt(10),
NotBefore: now.Add(-time.Hour),
NotAfter: now.Add(time.Hour),
KeyUsage: x509.KeyUsageCertSign,
IsCA: true,
BasicConstraintsValid: true,
}
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
return nil, fmt.Errorf("x509.CreateCertificate(%v) failed: %v", tmpl, err)
}
// The PEM encoding of the self-signed certificate is stored because we need
// to return a chain of certificates in the response, starting with the
// client certificate and ending in the root.
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, fmt.Errorf("x509.ParseCertificate(%v) failed: %v", certDER, err)
}
return &fakeCA{
withErrors: withErrors,
withShortLife: withShortLife,
ccChan: ccChan,
key: key,
cert: cert,
certPEM: certPEM,
}, nil
}
// CreateCertificate helps implement the MeshCA service.
//
// If the fakeMeshCA was created with `withErrors` set to true, the first
// `maxErrCount` number of RPC return errors. Subsequent requests are signed and
// returned without error.
func (f *fakeCA) CreateCertificate(ctx context.Context, req *meshpb.MeshCertificateRequest) (*meshpb.MeshCertificateResponse, error) {
if f.withErrors {
if f.errors < maxErrCount {
f.errors++
return nil, errors.New("fake Mesh CA error")
}
}
csrPEM := []byte(req.GetCsr())
block, _ := pem.Decode(csrPEM)
if block == nil {
return nil, fmt.Errorf("failed to decode received CSR: %v", csrPEM)
}
csr, err := x509.ParseCertificateRequest(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse received CSR: %v", csrPEM)
}
// By default, we create certs which are valid for an hour. But if
// `withShortLife` is set, we create certs which are valid only for a couple
// of seconds.
now := time.Now()
notBefore, notAfter := now.Add(-defaultTestCertLife), now.Add(defaultTestCertLife)
if f.withShortLife {
notBefore, notAfter = now.Add(-shortTestCertLife), now.Add(shortTestCertLife)
}
tmpl := &x509.Certificate{
Subject: pkix.Name{CommonName: "signed-cert"},
SerialNumber: big.NewInt(10),
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageDigitalSignature,
}
certDER, err := x509.CreateCertificate(rand.Reader, tmpl, f.cert, csr.PublicKey, f.key)
if err != nil {
return nil, fmt.Errorf("x509.CreateCertificate(%v) failed: %v", tmpl, err)
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
// Push to ccChan to indicate that the RPC is processed.
f.ccChan.Send(nil)
certChain := []string{
string(certPEM), // Signed certificate corresponding to CSR
string(f.certPEM), // Root certificate
}
return &meshpb.MeshCertificateResponse{CertChain: certChain}, nil
}
// opts wraps the options to be passed to setup.
type opts struct {
// Whether the CA returns certs with short lifetime. Used to test client refresh.
withShortLife bool
// Whether the CA returns errors to begin with. Used to test client backoff.
withbackoff bool
}
// events wraps channels which indicate different events.
type events struct {
// Pushed to when the plugin dials the MeshCA.
dialDone *testutils.Channel
// Pushed to when CreateCertifcate() succeeds on the MeshCA.
createCertDone *testutils.Channel
// Pushed to when the plugin updates the distributor with new key material.
keyMaterialDone *testutils.Channel
// Pushed to when the client backs off after a failed CreateCertificate().
backoffDone *testutils.Channel
}
// setup performs tasks common to all tests in this file.
func setup(t *testing.T, o opts) (events, string, func()) {
t.Helper()
// Create a fake MeshCA which pushes events on the passed channel for
// successful RPCs.
createCertDone := testutils.NewChannel()
fs, err := newFakeMeshCA(createCertDone, o.withbackoff, o.withShortLife)
if err != nil {
t.Fatal(err)
}
// Create a gRPC server and register the fake MeshCA on it.
server := grpc.NewServer()
meshgrpc.RegisterMeshCertificateServiceServer(server, fs)
// Start a net.Listener on a local port, and pass it to the gRPC server
// created above and start serving.
lis, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatal(err)
}
addr := lis.Addr().String()
go server.Serve(lis)
// Override the plugin's dial function and perform a blocking dial. Also
// push on dialDone once the dial is complete so that test can block on this
// event before verifying other things.
dialDone := testutils.NewChannel()
origDialFunc := grpcDialFunc
grpcDialFunc = func(uri string, _ ...grpc.DialOption) (*grpc.ClientConn, error) {
if uri != addr {
t.Fatalf("plugin dialing MeshCA at %s, want %s", uri, addr)
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
cc, err := grpc.DialContext(ctx, uri, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
t.Fatalf("grpc.DialContext(%s) failed: %v", addr, err)
}
dialDone.Send(nil)
return cc, nil
}
// Override the plugin's newDistributorFunc and return a wrappedDistributor
// which allows the test to be notified whenever the plugin pushes new key
// material into the distributor.
origDistributorFunc := newDistributorFunc
keyMaterialDone := testutils.NewChannel()
d := newWrappedDistributor(keyMaterialDone)
newDistributorFunc = func() distributor { return d }
// Override the plugin's backoff function to perform no real backoff, but
// push on a channel so that the test can verifiy that backoff actually
// happened.
backoffDone := testutils.NewChannelWithSize(maxErrCount)
origBackoffFunc := backoffFunc
if o.withbackoff {
// Override the plugin's backoff function with this, so that we can verify
// that a backoff actually was triggered.
backoffFunc = func(v int) time.Duration {
backoffDone.Send(v)
return 0
}
}
// Return all the channels, and a cancel function to undo all the overrides.
e := events{
dialDone: dialDone,
createCertDone: createCertDone,
keyMaterialDone: keyMaterialDone,
backoffDone: backoffDone,
}
done := func() {
server.Stop()
grpcDialFunc = origDialFunc
newDistributorFunc = origDistributorFunc
backoffFunc = origBackoffFunc
}
return e, addr, done
}
// wrappedDistributor wraps a distributor and pushes on a channel whenever new
// key material is pushed to the distributor.
type wrappedDistributor struct {
*certprovider.Distributor
kmChan *testutils.Channel
}
func newWrappedDistributor(kmChan *testutils.Channel) *wrappedDistributor {
return &wrappedDistributor{
kmChan: kmChan,
Distributor: certprovider.NewDistributor(),
}
}
func (wd *wrappedDistributor) Set(km *certprovider.KeyMaterial, err error) {
wd.Distributor.Set(km, err)
wd.kmChan.Send(nil)
}
// TestCreateCertificate verifies the simple case where the MeshCA server
// returns a good certificate.
func (s) TestCreateCertificate(t *testing.T) {
e, addr, cancel := setup(t, opts{})
defer cancel()
// Set the MeshCA targetURI in the plugin configuration to point to our fake
// MeshCA.
cfg := proto.Clone(goodConfigFullySpecified).(*configpb.GoogleMeshCaConfig)
cfg.Server.GrpcServices[0].GetGoogleGrpc().TargetUri = addr
inputConfig := makeJSONConfig(t, cfg)
prov, err := certprovider.GetProvider(pluginName, inputConfig, certprovider.Options{})
if err != nil {
t.Fatalf("certprovider.GetProvider(%s, %s) failed: %v", pluginName, cfg, err)
}
defer prov.Close()
// Wait till the plugin dials the MeshCA server.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err := e.dialDone.Receive(ctx); err != nil {
t.Fatal("timeout waiting for plugin to dial MeshCA")
}
// Wait till the plugin makes a CreateCertificate() call.
ctx, cancel = context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err := e.createCertDone.Receive(ctx); err != nil {
t.Fatal("timeout waiting for plugin to make CreateCertificate RPC")
}
// We don't really care about the exact key material returned here. All we
// care about is whether we get any key material at all, and that we don't
// get any errors.
ctx, cancel = context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err = prov.KeyMaterial(ctx); err != nil {
t.Fatalf("provider.KeyMaterial(ctx) failed: %v", err)
}
}
// TestCreateCertificateWithBackoff verifies the case where the MeshCA server
// returns errors initially and then returns a good certificate. The test makes
// sure that the client backs off when the server returns errors.
func (s) TestCreateCertificateWithBackoff(t *testing.T) {
e, addr, cancel := setup(t, opts{withbackoff: true})
defer cancel()
// Set the MeshCA targetURI in the plugin configuration to point to our fake
// MeshCA.
cfg := proto.Clone(goodConfigFullySpecified).(*configpb.GoogleMeshCaConfig)
cfg.Server.GrpcServices[0].GetGoogleGrpc().TargetUri = addr
inputConfig := makeJSONConfig(t, cfg)
prov, err := certprovider.GetProvider(pluginName, inputConfig, certprovider.Options{})
if err != nil {
t.Fatalf("certprovider.GetProvider(%s, %s) failed: %v", pluginName, cfg, err)
}
defer prov.Close()
// Wait till the plugin dials the MeshCA server.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err := e.dialDone.Receive(ctx); err != nil {
t.Fatal("timeout waiting for plugin to dial MeshCA")
}
// Making the CreateCertificateRPC involves generating the keys, creating
// the CSR etc which seem to take reasonable amount of time. And in this
// test, the first two attempts will fail. Hence we give it a reasonable
// deadline here.
ctx, cancel = context.WithTimeout(context.Background(), 3*defaultTestTimeout)
defer cancel()
if _, err := e.createCertDone.Receive(ctx); err != nil {
t.Fatal("timeout waiting for plugin to make CreateCertificate RPC")
}
// The first `maxErrCount` calls to CreateCertificate end in failure, and
// should lead to a backoff.
for i := 0; i < maxErrCount; i++ {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err := e.backoffDone.Receive(ctx); err != nil {
t.Fatalf("plugin failed to backoff after error from fake server: %v", err)
}
}
// We don't really care about the exact key material returned here. All we
// care about is whether we get any key material at all, and that we don't
// get any errors.
ctx, cancel = context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err = prov.KeyMaterial(ctx); err != nil {
t.Fatalf("provider.KeyMaterial(ctx) failed: %v", err)
}
}
// TestCreateCertificateWithRefresh verifies the case where the MeshCA returns a
// certificate with a really short lifetime, and makes sure that the plugin
// refreshes the cert in time.
func (s) TestCreateCertificateWithRefresh(t *testing.T) {
e, addr, cancel := setup(t, opts{withShortLife: true})
defer cancel()
// Set the MeshCA targetURI in the plugin configuration to point to our fake
// MeshCA.
cfg := proto.Clone(goodConfigFullySpecified).(*configpb.GoogleMeshCaConfig)
cfg.Server.GrpcServices[0].GetGoogleGrpc().TargetUri = addr
inputConfig := makeJSONConfig(t, cfg)
prov, err := certprovider.GetProvider(pluginName, inputConfig, certprovider.Options{})
if err != nil {
t.Fatalf("certprovider.GetProvider(%s, %s) failed: %v", pluginName, cfg, err)
}
defer prov.Close()
// Wait till the plugin dials the MeshCA server.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err := e.dialDone.Receive(ctx); err != nil {
t.Fatal("timeout waiting for plugin to dial MeshCA")
}
// Wait till the plugin makes a CreateCertificate() call.
ctx, cancel = context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if _, err := e.createCertDone.Receive(ctx); err != nil {
t.Fatal("timeout waiting for plugin to make CreateCertificate RPC")
}
ctx, cancel = context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
km1, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial(ctx) failed: %v", err)
}
// At this point, we have read the first key material, and since the
// returned key material has a really short validity period, we expect the
// key material to be refreshed quite soon. We drain the channel on which
// the event corresponding to setting of new key material is pushed. This
// enables us to block on the same channel, waiting for refreshed key
// material.
// Since we do not expect this call to block, it is OK to pass the
// background context.
e.keyMaterialDone.Receive(context.Background())
// Wait for the next call to CreateCertificate() to refresh the certificate
// returned earlier.
ctx, cancel = context.WithTimeout(context.Background(), 2*shortTestCertLife)
defer cancel()
if _, err := e.keyMaterialDone.Receive(ctx); err != nil {
t.Fatalf("CreateCertificate() RPC not made: %v", err)
}
ctx, cancel = context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
km2, err := prov.KeyMaterial(ctx)
if err != nil {
t.Fatalf("provider.KeyMaterial(ctx) failed: %v", err)
}
// TODO(easwars): Remove all references to reflect.DeepEqual and use
// cmp.Equal instead. Currently, the later panics because x509.Certificate
// type defines an Equal method, but does not check for nil. This has been
// fixed in
// https://github.com/golang/go/commit/89865f8ba64ccb27f439cce6daaa37c9aa38f351,
// but this is only available starting go1.14. So, once we remove support
// for go1.13, we can make the switch.
if reflect.DeepEqual(km1, km2) {
t.Error("certificate refresh did not happen in the background")
}
}
|