blob: e3f2b558a2609378891767598dcdde5f671b5539 [file] [log] [blame]
// Copyright Istio 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 caclient
import (
"context"
"crypto/tls"
"fmt"
"net"
"path"
"path/filepath"
"reflect"
"strings"
"sync"
"testing"
"time"
)
import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
pb "istio.io/api/security/v1alpha1"
)
import (
testutil "github.com/apache/dubbo-go-pixiu/pilot/test/util"
"github.com/apache/dubbo-go-pixiu/pkg/config/constants"
"github.com/apache/dubbo-go-pixiu/pkg/file"
"github.com/apache/dubbo-go-pixiu/pkg/security"
"github.com/apache/dubbo-go-pixiu/pkg/spiffe"
"github.com/apache/dubbo-go-pixiu/pkg/test/env"
"github.com/apache/dubbo-go-pixiu/pkg/test/util/retry"
"github.com/apache/dubbo-go-pixiu/security/pkg/credentialfetcher/plugin"
"github.com/apache/dubbo-go-pixiu/security/pkg/monitoring"
"github.com/apache/dubbo-go-pixiu/security/pkg/nodeagent/util"
ca2 "github.com/apache/dubbo-go-pixiu/security/pkg/server/ca"
)
const (
mockServerAddress = "localhost:0"
)
var (
fakeCert = []string{"foo", "bar"}
fakeToken = "Bearer fakeToken"
validToken = "Bearer validToken"
authorizationMeta = "authorization"
)
type mockCAServer struct {
pb.UnimplementedIstioCertificateServiceServer
Certs []string
Authenticator *security.FakeAuthenticator
Err error
}
func (ca *mockCAServer) CreateCertificate(ctx context.Context, in *pb.IstioCertificateRequest) (*pb.IstioCertificateResponse, error) {
if ca.Authenticator != nil {
caller := ca2.Authenticate(ctx, []security.Authenticator{ca.Authenticator})
if caller == nil {
return nil, status.Error(codes.Unauthenticated, "request authenticate failure")
}
}
if ca.Err == nil {
return &pb.IstioCertificateResponse{CertChain: ca.Certs}, nil
}
return nil, ca.Err
}
func tlsOptions(t *testing.T) grpc.ServerOption {
t.Helper()
cert, err := tls.LoadX509KeyPair(
filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/cert-chain.pem"),
filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/key.pem"))
if err != nil {
t.Fatal(err)
}
peerCertVerifier := spiffe.NewPeerCertVerifier()
if err := peerCertVerifier.AddMappingFromPEM("cluster.local",
testutil.ReadFile(t, filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot/root-cert.pem"))); err != nil {
t.Fatal(err)
}
return grpc.Creds(credentials.NewTLS(&tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.VerifyClientCertIfGiven,
ClientCAs: peerCertVerifier.GetGeneralCertPool(),
}))
}
func serve(t *testing.T, ca mockCAServer, opts ...grpc.ServerOption) string {
// create a local grpc server
s := grpc.NewServer(opts...)
t.Cleanup(s.Stop)
lis, err := net.Listen("tcp", mockServerAddress)
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go func() {
pb.RegisterIstioCertificateServiceServer(s, &ca)
if err := s.Serve(lis); err != nil {
t.Logf("failed to serve: %v", err)
}
}()
_, port, _ := net.SplitHostPort(lis.Addr().String())
return fmt.Sprintf("localhost:%s", port)
}
func TestCitadelClientRotation(t *testing.T) {
checkSign := func(t *testing.T, cli security.Client, expectError bool) {
t.Helper()
resp, err := cli.CSRSign([]byte{0o1}, 1)
if expectError != (err != nil) {
t.Fatalf("expected error:%v, got error:%v", expectError, err)
}
if !expectError && !reflect.DeepEqual(resp, fakeCert) {
t.Fatalf("expected cert: %v", resp)
}
}
certDir := filepath.Join(env.IstioSrc, "./tests/testdata/certs/pilot")
t.Run("cert always present", func(t *testing.T) {
server := mockCAServer{Certs: fakeCert, Err: nil, Authenticator: security.NewFakeAuthenticator("ca")}
addr := serve(t, server, tlsOptions(t))
opts := &security.Options{
CAEndpoint: addr,
CredFetcher: plugin.CreateTokenPlugin("testdata/token"),
ProvCert: certDir,
}
rootCert := path.Join(certDir, constants.RootCertFilename)
key := path.Join(certDir, constants.KeyFilename)
cert := path.Join(certDir, constants.CertChainFilename)
tlsOpts := &TLSOptions{
RootCert: rootCert,
Key: key,
Cert: cert,
}
cli, err := NewCitadelClient(opts, tlsOpts)
if err != nil {
t.Errorf("failed to create ca client: %v", err)
}
t.Cleanup(cli.Close)
server.Authenticator.Set("fake", "")
checkSign(t, cli, false)
// Expiring the token is harder, so just switch to only allow certs
server.Authenticator.Set("", "istiod.dubbo-system.svc")
checkSign(t, cli, false)
checkSign(t, cli, false)
})
t.Run("cert never present", func(t *testing.T) {
server := mockCAServer{Certs: fakeCert, Err: nil, Authenticator: security.NewFakeAuthenticator("ca")}
addr := serve(t, server, tlsOptions(t))
opts := &security.Options{
CAEndpoint: addr,
CredFetcher: plugin.CreateTokenPlugin("testdata/token"),
ProvCert: ".",
}
rootCert := path.Join(certDir, constants.RootCertFilename)
key := path.Join(opts.ProvCert, constants.KeyFilename)
cert := path.Join(opts.ProvCert, constants.CertChainFilename)
tlsOpts := &TLSOptions{
RootCert: rootCert,
Key: key,
Cert: cert,
}
cli, err := NewCitadelClient(opts, tlsOpts)
if err != nil {
t.Errorf("failed to create ca client: %v", err)
}
t.Cleanup(cli.Close)
server.Authenticator.Set("fake", "")
checkSign(t, cli, false)
server.Authenticator.Set("", "istiod.dubbo-system.svc")
checkSign(t, cli, true)
})
t.Run("cert present later", func(t *testing.T) {
dir := t.TempDir()
server := mockCAServer{Certs: fakeCert, Err: nil, Authenticator: security.NewFakeAuthenticator("ca")}
addr := serve(t, server, tlsOptions(t))
opts := &security.Options{
CAEndpoint: addr,
CredFetcher: plugin.CreateTokenPlugin("testdata/token"),
ProvCert: dir,
}
rootCert := path.Join(certDir, constants.RootCertFilename)
key := path.Join(opts.ProvCert, constants.KeyFilename)
cert := path.Join(opts.ProvCert, constants.CertChainFilename)
tlsOpts := &TLSOptions{
RootCert: rootCert,
Key: key,
Cert: cert,
}
cli, err := NewCitadelClient(opts, tlsOpts)
if err != nil {
t.Errorf("failed to create ca client: %v", err)
}
t.Cleanup(cli.Close)
server.Authenticator.Set("fake", "")
checkSign(t, cli, false)
checkSign(t, cli, false)
server.Authenticator.Set("", "istiod.dubbo-system.svc")
checkSign(t, cli, true)
if err := file.Copy(filepath.Join(certDir, "cert-chain.pem"), dir, "cert-chain.pem"); err != nil {
t.Fatal(err)
}
if err := file.Copy(filepath.Join(certDir, "key.pem"), dir, "key.pem"); err != nil {
t.Fatal(err)
}
checkSign(t, cli, false)
})
}
func TestCitadelClient(t *testing.T) {
testCases := map[string]struct {
server mockCAServer
expectedCert []string
expectedErr string
expectRetry bool
}{
"Valid certs": {
server: mockCAServer{Certs: fakeCert, Err: nil},
expectedCert: fakeCert,
expectedErr: "",
},
"Error in response": {
server: mockCAServer{Certs: nil, Err: fmt.Errorf("test failure")},
expectedCert: nil,
expectedErr: "rpc error: code = Unknown desc = test failure",
},
"Empty response": {
server: mockCAServer{Certs: []string{}, Err: nil},
expectedCert: nil,
expectedErr: "invalid empty CertChain",
},
"retry": {
server: mockCAServer{Certs: nil, Err: status.Error(codes.Unavailable, "test failure")},
expectedCert: nil,
expectedErr: "rpc error: code = Unavailable desc = test failure",
expectRetry: true,
},
}
for id, tc := range testCases {
t.Run(id, func(t *testing.T) {
addr := serve(t, tc.server)
cli, err := NewCitadelClient(&security.Options{CAEndpoint: addr}, nil)
if err != nil {
t.Errorf("failed to create ca client: %v", err)
}
t.Cleanup(cli.Close)
resp, err := cli.CSRSign([]byte{0o1}, 1)
if err != nil {
if !strings.Contains(err.Error(), tc.expectedErr) {
t.Errorf("error (%s) does not match expected error (%s)", err.Error(), tc.expectedErr)
}
} else {
if tc.expectedErr != "" {
t.Errorf("expect error: %s but got no error", tc.expectedErr)
} else if !reflect.DeepEqual(resp, tc.expectedCert) {
t.Errorf("resp: got %+v, expected %v", resp, tc.expectedCert)
}
}
if tc.expectRetry {
retry.UntilSuccessOrFail(t, func() error {
g, err := util.GetMetricsCounterValueWithTags("num_outgoing_retries", map[string]string{
"request_type": monitoring.CSR,
})
if err != nil {
return err
}
if g <= 0 {
return fmt.Errorf("expected retries, got %v", g)
}
return nil
}, retry.Timeout(time.Second*5))
}
})
}
}
type mockTokenCAServer struct {
pb.UnimplementedIstioCertificateServiceServer
Certs []string
}
func (ca *mockTokenCAServer) CreateCertificate(ctx context.Context, in *pb.IstioCertificateRequest) (*pb.IstioCertificateResponse, error) {
targetJWT, err := extractBearerToken(ctx)
if err != nil {
return nil, err
}
if targetJWT != validToken {
return nil, fmt.Errorf("token is not valid, wanted %q got %q", validToken, targetJWT)
}
return &pb.IstioCertificateResponse{CertChain: ca.Certs}, nil
}
func extractBearerToken(ctx context.Context) (string, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", fmt.Errorf("no metadata is attached")
}
authHeader, exists := md[authorizationMeta]
if !exists {
return "", fmt.Errorf("no HTTP authorization header exists")
}
for _, value := range authHeader {
if strings.HasPrefix(value, bearerTokenPrefix) {
return strings.TrimPrefix(value, bearerTokenPrefix), nil
}
}
return "", fmt.Errorf("no bearer token exists in HTTP authorization header")
}
// this test is to test whether the server side receive the correct token when
// we build the CSR sign request
func TestCitadelClientWithDifferentTypeToken(t *testing.T) {
testCases := map[string]struct {
server mockTokenCAServer
expectedCert []string
expectedErr string
token string
}{
"Valid Token": {
server: mockTokenCAServer{Certs: fakeCert},
expectedCert: fakeCert,
expectedErr: "",
token: validToken,
},
"Empty Token": {
server: mockTokenCAServer{Certs: nil},
expectedCert: nil,
expectedErr: "rpc error: code = Unknown desc = no HTTP authorization header exists",
token: "",
},
"InValid Token": {
server: mockTokenCAServer{Certs: []string{}},
expectedCert: nil,
expectedErr: "rpc error: code = Unknown desc = token is not valid",
token: fakeToken,
},
}
for id, tc := range testCases {
t.Run(id, func(t *testing.T) {
s := grpc.NewServer()
defer s.Stop()
lis, err := net.Listen("tcp", mockServerAddress)
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go func() {
pb.RegisterIstioCertificateServiceServer(s, &tc.server)
if err := s.Serve(lis); err != nil {
t.Logf("failed to serve: %v", err)
}
}()
opts := &security.Options{CAEndpoint: lis.Addr().String(), ClusterID: "Kubernetes", CredFetcher: plugin.CreateMockPlugin(tc.token)}
err = retry.UntilSuccess(func() error {
cli, err := NewCitadelClient(opts, nil)
if err != nil {
return fmt.Errorf("failed to create ca client: %v", err)
}
t.Cleanup(cli.Close)
resp, err := cli.CSRSign([]byte{0o1}, 1)
if err != nil {
if !strings.Contains(err.Error(), tc.expectedErr) {
return fmt.Errorf("error (%s) does not match expected error (%s)", err.Error(), tc.expectedErr)
}
} else {
if tc.expectedErr != "" {
return fmt.Errorf("expect error: %s but got no error", tc.expectedErr)
} else if !reflect.DeepEqual(resp, tc.expectedCert) {
return fmt.Errorf("resp: got %+v, expected %v", resp, tc.expectedCert)
}
}
return nil
}, retry.Timeout(2*time.Second), retry.Delay(time.Millisecond))
if err != nil {
t.Fatalf("test failed error is: %+v", err)
}
})
}
}
func TestCertExpired(t *testing.T) {
testCases := map[string]struct {
filepath string
expected bool
}{
"Expired Cert": {
filepath: "./testdata/expired-cert.pem",
expected: true,
},
"Not Expired Cert": {
filepath: "./testdata/notexpired-cert.pem",
expected: false,
},
}
for id, tc := range testCases {
var wg sync.WaitGroup
wg.Add(1)
s := grpc.NewServer()
defer func() {
s.Stop()
wg.Wait()
}()
lis, err := net.Listen("tcp", mockServerAddress)
if err != nil {
t.Fatalf("failed to listen: %v", err)
}
go func() {
defer wg.Done()
pb.RegisterIstioCertificateServiceServer(s, &mockTokenCAServer{Certs: []string{}})
if err := s.Serve(lis); err != nil {
t.Logf("failed to serve: %v", err)
}
}()
opts := &security.Options{CAEndpoint: lis.Addr().String(), ClusterID: "Kubernetes", CredFetcher: plugin.CreateMockPlugin(validToken)}
cli, err := NewCitadelClient(opts, nil)
if err != nil {
t.Fatalf("failed to create ca client: %v", err)
}
t.Cleanup(cli.Close)
t.Run(id, func(t *testing.T) {
certExpired, err := cli.isCertExpired(tc.filepath)
if err != nil {
t.Fatalf("failed to check the cert, err is: %v", err)
}
if certExpired != tc.expected {
t.Errorf("isCertExpired: get %v, want %v", certExpired, tc.expected)
}
})
}
}