You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by al...@apache.org on 2022/09/23 13:19:54 UTC

[dubbo-go] branch 3.0 updated: feat: support xds certificate (#1945)

This is an automated email from the ASF dual-hosted git repository.

alexstocks pushed a commit to branch 3.0
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git


The following commit(s) were added to refs/heads/3.0 by this push:
     new 96683b08b feat: support xds certificate (#1945)
96683b08b is described below

commit 96683b08bb93cfe5c89eb2f5f7d9cb8dfa726ba7
Author: ZLBer <10...@qq.com>
AuthorDate: Fri Sep 23 21:19:47 2022 +0800

    feat: support xds certificate (#1945)
    
    * cert provider
    
    * adjust code
    
    * adjust code
    
    * add comment
    
    * add env param
    
    * add env param
    
    * fix golint
    
    * fix golint
    
    * fix type compatible
    
    * fix imports-formatter
    
    * fix imports-formatter
    
    * fix go-lint
    
    * fix go-lint
    
    * fix go-lint
    
    * fix go-lint
    
    * fix go-lint
    
    * make host configurable
    
    * fix
---
 remoting/xds/client_test.go                        | 799 ---------------------
 xds/balancer/cdsbalancer/cdsbalancer.go            |   2 +-
 xds/client/bootstrap/bootstrap.go                  |   6 +-
 xds/credentials/cert_manager.go                    | 271 +++++++
 xds/credentials/certgenerate/crypto.go             | 166 +++++
 xds/credentials/certgenerate/dual_use.go           |  43 ++
 xds/credentials/certgenerate/generate_cert.go      | 440 ++++++++++++
 xds/credentials/certgenerate/generate_csr.go       | 137 ++++
 xds/credentials/certgenerate/san.go                | 213 ++++++
 xds/credentials/certprovider/distributor.go        | 121 ++++
 xds/credentials/certprovider/pemfile/builder.go    | 105 +++
 xds/credentials/certprovider/pemfile/watcher.go    | 265 +++++++
 xds/credentials/certprovider/provider.go           | 104 +++
 .../certprovider/remote/istioca_client.go          | 210 ++++++
 .../certprovider/remote/v1alpha1/ca.pb.go          | 287 ++++++++
 .../certprovider/remote/v1alpha1/ca_grpc.pb.go     | 128 ++++
 xds/credentials/certprovider/store.go              | 161 +++++
 xds/credentials/env.go                             |  51 ++
 xds/credentials/token_provider.go                  |  55 ++
 xds/server/conn_wrapper.go                         |   5 +-
 xds/utils/credentials/xds/handshake_info.go        |   3 +-
 21 files changed, 2763 insertions(+), 809 deletions(-)

diff --git a/remoting/xds/client_test.go b/remoting/xds/client_test.go
deleted file mode 100644
index 23bd845e8..000000000
--- a/remoting/xds/client_test.go
+++ /dev/null
@@ -1,799 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 xds
-
-import (
-	"testing"
-	"time"
-)
-
-import (
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/mock"
-
-	"go.uber.org/atomic"
-)
-
-import (
-	"dubbo.apache.org/dubbo-go/v3/common/constant"
-	"dubbo.apache.org/dubbo-go/v3/registry"
-	registryMocks "dubbo.apache.org/dubbo-go/v3/registry/mocks"
-	"dubbo.apache.org/dubbo-go/v3/remoting"
-	"dubbo.apache.org/dubbo-go/v3/remoting/xds/common"
-	"dubbo.apache.org/dubbo-go/v3/xds/client"
-	"dubbo.apache.org/dubbo-go/v3/xds/client/mocks"
-	"dubbo.apache.org/dubbo-go/v3/xds/client/resource"
-)
-
-const (
-	dubbogoPortFoo  = "20000"
-	istioXDSPortFoo = "15010"
-
-	podNameFoo        = "mockPodName"
-	localNamespaceFoo = "default"
-
-	localIPFoo = "172.16.100.1"
-	istioIPFoo = "172.16.100.2"
-
-	localAddrFoo    = localIPFoo + ":" + dubbogoPortFoo
-	istioXDSAddrFoo = istioIPFoo + ":" + istioXDSPortFoo
-
-	istioHostNameFoo = "istiod.istio-system.svc.cluster.local"
-	istioHostAddrFoo = istioHostNameFoo + ":" + istioXDSPortFoo
-	localHostNameFoo = "dubbo-go-app." + localNamespaceFoo + ".svc.cluster.local"
-	localHostAddrFoo = localHostNameFoo + ":" + dubbogoPortFoo
-
-	istioClusterNameFoo = "outbound|" + istioXDSPortFoo + "||" + istioHostNameFoo
-	localClusterNameFoo = "outbound|" + dubbogoPortFoo + "||" + localHostNameFoo
-)
-
-const (
-	providerIPFoo     = "172.16.100.3"
-	providerV1IPFoo   = "172.16.100.4"
-	providerV2IPFoo   = "172.16.100.5"
-	providerAddrFoo   = providerIPFoo + ":" + dubbogoPortFoo
-	providerV1AddrFoo = providerV1IPFoo + ":" + dubbogoPortFoo
-	providerV2AddrFoo = providerV2IPFoo + ":" + dubbogoPortFoo
-
-	dubbogoProviderHostName    = "dubbo-go-app-provider." + localNamespaceFoo + ".svc.cluster.local"
-	dubbogoProviderHostAddrFoo = dubbogoProviderHostName + ":" + dubbogoPortFoo
-
-	dubbogoProviderInterfaceNameFoo    = "api.Greeter"
-	dubbogoProviderserivceUniqueKeyFoo = "provider::" + dubbogoProviderInterfaceNameFoo
-
-	providerClusterNameFoo   = "outbound|" + dubbogoPortFoo + "||" + dubbogoProviderHostName
-	providerClusterV1NameFoo = "outbound|" + dubbogoPortFoo + "|v1|" + dubbogoProviderHostName
-	providerClusterV2NameFoo = "outbound|" + dubbogoPortFoo + "|v2|" + dubbogoProviderHostName
-)
-
-func TestWrappedClientImpl(t *testing.T) {
-	// test New WrappedClientImpl
-	testFailedWithIstioCDS(t)
-	testFailedWithLocalCDS(t)
-	testFailedWithNoneCDS(t)
-
-	testFailedWithLocalEDSFailed(t)
-	testFailedWithIstioEDSFailed(t)
-
-	testWithDiscoverySuccess(t)
-
-	assert.NotNil(t, GetXDSWrappedClient())
-
-	// test Subscription
-	testSubscribe(t)
-}
-
-func testWithDiscoverySuccess(t *testing.T) {
-	mockXDSClient := &mocks.XDSClient{}
-	cancelCalledCounter := &atomic.Int32{}
-	// all cluster CDS
-	mockXDSClient.On("WatchCluster", "*",
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.ClusterUpdate, err error)) bool {
-			// istioClusterName CDS, this and next calling must be async because testify.Mock.MethodCalled() has calling lock
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: istioClusterNameFoo,
-			}, nil)
-
-			// localClusterName CDS
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: localClusterNameFoo,
-			}, nil)
-			return true
-		})).
-		Return(func() {})
-
-	// istio cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			return clusterName == istioClusterNameFoo
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: istioXDSAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {
-			// return cancel function
-			cancelCalledCounter.Inc()
-		})
-
-	// local cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			return clusterName == localClusterNameFoo
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: localAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {
-			// return cancel function
-			cancelCalledCounter.Inc()
-		})
-
-	xdsClientFactoryFunction = func(localIP, podName, namespace string, istioAddr common.HostAddr) (client.XDSClient, error) {
-		return mockXDSClient, nil
-	}
-	xdsWrappedClient, err := NewXDSWrappedClient(Config{
-		PodName:   podNameFoo,
-		Namespace: localNamespaceFoo,
-		LocalIP:   localIPFoo,
-		IstioAddr: common.NewHostNameOrIPAddr(istioHostAddrFoo),
-	})
-	assert.Nil(t, err)
-	assert.NotNil(t, xdsWrappedClient)
-
-	// assert eds cancel is called
-	assert.Equal(t, int32(2), cancelCalledCounter.Load())
-	// discovery istiod pod ip
-	assert.Equal(t, istioIPFoo, xdsWrappedClient.GetIstioPodIP())
-	address := xdsWrappedClient.GetHostAddress()
-	assert.Equal(t, localHostAddrFoo, address.String())
-}
-
-func testFailedWithIstioCDS(t *testing.T) {
-	mockXDSClient := &mocks.XDSClient{}
-	cancelCalledCounter := &atomic.Int32{}
-	// all cluster CDS
-	mockXDSClient.On("WatchCluster", "*",
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.ClusterUpdate, err error)) bool {
-			// do not send istioClusterName message, which cause discover istio ip failed
-			//go clusterUpdateHandler(resource.ClusterUpdate{
-			//	ClusterName: istioClusterNameFoo,
-			//}, nil)
-
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: localClusterNameFoo,
-			}, nil)
-			return true
-		})).
-		Return(func() {})
-
-	// istio cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			return clusterName == istioClusterNameFoo
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: istioXDSAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {
-			// return cancel function
-			cancelCalledCounter.Inc()
-		})
-
-	// local cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			return clusterName == localClusterNameFoo
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: localAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {
-			// return cancel function
-			cancelCalledCounter.Inc()
-		})
-
-	xdsClientFactoryFunction = func(localIP, podName, namespace string, istioAddr common.HostAddr) (client.XDSClient, error) {
-		return mockXDSClient, nil
-	}
-	xdsWrappedClient, err := NewXDSWrappedClient(Config{
-		PodName:   podNameFoo,
-		Namespace: localNamespaceFoo,
-		LocalIP:   localIPFoo,
-		IstioAddr: common.NewHostNameOrIPAddr(istioHostAddrFoo),
-	})
-	assert.Equal(t, DiscoverIstiodPodIpError, err)
-	assert.Nil(t, xdsWrappedClient)
-	assert.Equal(t, int32(1), cancelCalledCounter.Load())
-}
-
-func testFailedWithLocalCDS(t *testing.T) {
-	mockXDSClient := &mocks.XDSClient{}
-	cancelCalledCounter := &atomic.Int32{}
-	// all cluster CDS
-	mockXDSClient.On("WatchCluster", "*",
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.ClusterUpdate, err error)) bool {
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: istioClusterNameFoo,
-			}, nil)
-
-			// do not send localClusterNameFoo cds message, which cause discover local addr failed
-			//go clusterUpdateHandler(resource.ClusterUpdate{
-			//	ClusterName: localClusterNameFoo,
-			//}, nil)
-			return true
-		})).
-		Return(func() {})
-
-	// istio cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			return clusterName == istioClusterNameFoo
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: istioXDSAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {
-			// return cancel function
-			cancelCalledCounter.Inc()
-		})
-
-	// local cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			return clusterName == localClusterNameFoo
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: localAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {
-			// return cancel function
-			cancelCalledCounter.Inc()
-		})
-
-	xdsClientFactoryFunction = func(localIP, podName, namespace string, istioAddr common.HostAddr) (client.XDSClient, error) {
-		return mockXDSClient, nil
-	}
-	xdsWrappedClient, err := NewXDSWrappedClient(Config{
-		PodName:   podNameFoo,
-		Namespace: localNamespaceFoo,
-		LocalIP:   localIPFoo,
-		IstioAddr: common.NewHostNameOrIPAddr(istioHostAddrFoo),
-	})
-	assert.Equal(t, DiscoverLocalError, err)
-	assert.Nil(t, xdsWrappedClient)
-	assert.Equal(t, int32(1), cancelCalledCounter.Load())
-}
-
-func testFailedWithNoneCDS(t *testing.T) {
-	mockXDSClient := &mocks.XDSClient{}
-	cancelCalledCounter := &atomic.Int32{}
-	// all cluster CDS
-	mockXDSClient.On("WatchCluster", "*",
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.ClusterUpdate, err error)) bool {
-			// do not send any cds message, which cause discover failed
-			//go clusterUpdateHandler(resource.ClusterUpdate{
-			//	ClusterName: istioClusterNameFoo,
-			//}, nil)
-
-			//go clusterUpdateHandler(resource.ClusterUpdate{
-			//	ClusterName: localClusterNameFoo,
-			//}, nil)
-			return true
-		})).
-		Return(func() {})
-
-	// istio cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			return clusterName == istioClusterNameFoo
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: istioXDSAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {
-			// return cancel function
-			cancelCalledCounter.Inc()
-		})
-
-	// local cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			return clusterName == localClusterNameFoo
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: localAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {
-			// return cancel function
-			cancelCalledCounter.Inc()
-		})
-
-	xdsClientFactoryFunction = func(localIP, podName, namespace string, istioAddr common.HostAddr) (client.XDSClient, error) {
-		return mockXDSClient, nil
-	}
-	xdsWrappedClient, err := NewXDSWrappedClient(Config{
-		PodName:   podNameFoo,
-		Namespace: localNamespaceFoo,
-		LocalIP:   localIPFoo,
-		IstioAddr: common.NewHostNameOrIPAddr(istioHostAddrFoo),
-	})
-	assert.Equal(t, DiscoverIstiodPodIpError, err)
-	assert.Nil(t, xdsWrappedClient)
-	assert.Equal(t, int32(0), cancelCalledCounter.Load())
-}
-
-func testFailedWithLocalEDSFailed(t *testing.T) {
-	mockXDSClient := &mocks.XDSClient{}
-	cancelCalledCounter := &atomic.Int32{}
-	// all cluster CDS
-	mockXDSClient.On("WatchCluster", "*",
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.ClusterUpdate, err error)) bool {
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: istioClusterNameFoo,
-			}, nil)
-
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: localClusterNameFoo,
-			}, nil)
-			return true
-		})).
-		Return(func() {})
-
-	// istio cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			return clusterName == istioClusterNameFoo
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: istioXDSAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {
-			// return cancel function
-			cancelCalledCounter.Inc()
-		})
-
-	// local cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			return clusterName == localClusterNameFoo
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			// do not send local eds message
-			//clusterUpdateHandler(resource.EndpointsUpdate{
-			//	Localities: []resource.Locality{
-			//		{
-			//			Endpoints: []resource.Endpoint{
-			//				{
-			//					Address: localAddrFoo,
-			//				},
-			//			},
-			//		},
-			//	},
-			//}, nil)
-			return true
-		})).
-		Return(func() {
-			// return cancel function
-			cancelCalledCounter.Inc()
-		})
-
-	xdsClientFactoryFunction = func(localIP, podName, namespace string, istioAddr common.HostAddr) (client.XDSClient, error) {
-		return mockXDSClient, nil
-	}
-	xdsWrappedClient, err := NewXDSWrappedClient(Config{
-		PodName:   podNameFoo,
-		Namespace: localNamespaceFoo,
-		LocalIP:   localIPFoo,
-		IstioAddr: common.NewHostNameOrIPAddr(istioHostAddrFoo),
-	})
-	assert.Equal(t, DiscoverLocalError, err)
-	assert.Nil(t, xdsWrappedClient)
-	assert.Equal(t, int32(2), cancelCalledCounter.Load())
-}
-
-func testFailedWithIstioEDSFailed(t *testing.T) {
-	mockXDSClient := &mocks.XDSClient{}
-	cancelCalledCounter := &atomic.Int32{}
-	// all cluster CDS
-	mockXDSClient.On("WatchCluster", "*",
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.ClusterUpdate, err error)) bool {
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: istioClusterNameFoo,
-			}, nil)
-
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: localClusterNameFoo,
-			}, nil)
-			return true
-		})).
-		Return(func() {})
-
-	// istio cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			return clusterName == istioClusterNameFoo
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			// do not send istio eds message
-			//clusterUpdateHandler(resource.EndpointsUpdate{
-			//	Localities: []resource.Locality{
-			//		{
-			//			Endpoints: []resource.Endpoint{
-			//				{
-			//					Address: istioXDSAddrFoo,
-			//				},
-			//			},
-			//		},
-			//	},
-			//}, nil)
-			return true
-		})).
-		Return(func() {
-			// return cancel function
-			cancelCalledCounter.Inc()
-		})
-
-	// local cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			return clusterName == localClusterNameFoo
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: localAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {
-			// return cancel function
-			cancelCalledCounter.Inc()
-		})
-
-	xdsClientFactoryFunction = func(localIP, podName, namespace string, istioAddr common.HostAddr) (client.XDSClient, error) {
-		return mockXDSClient, nil
-	}
-	xdsWrappedClient, err := NewXDSWrappedClient(Config{
-		PodName:   podNameFoo,
-		Namespace: localNamespaceFoo,
-		LocalIP:   localIPFoo,
-		IstioAddr: common.NewHostNameOrIPAddr(istioHostAddrFoo),
-	})
-	assert.Equal(t, DiscoverIstiodPodIpError, err)
-	assert.Nil(t, xdsWrappedClient)
-	assert.Equal(t, int32(2), cancelCalledCounter.Load())
-}
-
-func testSubscribe(t *testing.T) {
-	mockXDSClient := &mocks.XDSClient{}
-	// all cluster CDS
-	mockXDSClient.On("WatchCluster", "*",
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.ClusterUpdate, err error)) bool {
-			// istio ClusterName CDS, this and next calling must be async because testify.Mock.MethodCalled() has calling lock
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: istioClusterNameFoo,
-			}, nil)
-
-			// local ClusterName CDS
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: localClusterNameFoo,
-			}, nil)
-
-			// provider Cluster Name CDS
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: providerClusterNameFoo,
-			}, nil)
-
-			// provider Cluster Name v1 CDS
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: providerClusterV1NameFoo,
-			}, nil)
-
-			// provider Cluster Name v2 CDS
-			go clusterUpdateHandler(resource.ClusterUpdate{
-				ClusterName: providerClusterV2NameFoo,
-			}, nil)
-			return true
-		})).
-		Return(func() {})
-
-	// istio cluster EDS
-	clusterMatch := false
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			clusterMatch = clusterName == istioClusterNameFoo
-			return clusterMatch
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			if !clusterMatch {
-				return false
-			}
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: istioXDSAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {})
-
-	// local cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			clusterMatch = clusterName == localClusterNameFoo
-			return clusterMatch
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			if !clusterMatch {
-				return false
-			}
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: localAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {})
-
-	// provider cluster EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			clusterMatch = clusterName == providerClusterNameFoo
-			return clusterMatch
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			if !clusterMatch {
-				return false
-			}
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: providerAddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {})
-
-	// provider cluster v1 EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			clusterMatch = clusterName == providerClusterV1NameFoo
-			return clusterMatch
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			if !clusterMatch {
-				return false
-			}
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: providerV1AddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {})
-
-	// todo test router RDS
-	mockXDSClient.On("WatchRouteConfig", mock.Anything, mock.Anything).Return(func() {})
-
-	// provider cluster v2 EDS
-	mockXDSClient.On("WatchEndpoints",
-		mock.MatchedBy(func(clusterName string) bool {
-			clusterMatch = clusterName == providerClusterV2NameFoo
-			return clusterMatch
-		}),
-		mock.MatchedBy(func(clusterUpdateHandler func(update resource.EndpointsUpdate, err error)) bool {
-			if !clusterMatch {
-				return false
-			}
-			clusterUpdateHandler(resource.EndpointsUpdate{
-				Localities: []resource.Locality{
-					{
-						Endpoints: []resource.Endpoint{
-							{
-								Address: providerV2AddrFoo,
-							},
-						},
-					},
-				},
-			}, nil)
-			return true
-		})).
-		Return(func() {})
-
-	xdsClientFactoryFunction = func(localIP, podName, namespace string, istioAddr common.HostAddr) (client.XDSClient, error) {
-		return mockXDSClient, nil
-	}
-
-	xdsWrappedClient = nil
-	xdsWrappedClient, err := NewXDSWrappedClient(Config{
-		PodName:   podNameFoo,
-		Namespace: localNamespaceFoo,
-		LocalIP:   localIPFoo,
-		IstioAddr: common.NewHostNameOrIPAddr(istioHostAddrFoo),
-	})
-	assert.Nil(t, err)
-	assert.NotNil(t, xdsWrappedClient)
-
-	notifyListener := &registryMocks.NotifyListener{}
-	notifyListener.On("Notify", mock.Anything).Return(nil)
-
-	go xdsWrappedClient.Subscribe(dubbogoProviderserivceUniqueKeyFoo, dubbogoProviderInterfaceNameFoo, dubbogoProviderHostAddrFoo, notifyListener)
-
-	time.Sleep(time.Second)
-	notifyListener.AssertCalled(t, "Notify", mock.MatchedBy(func(event *registry.ServiceEvent) bool {
-		if event.Action == remoting.EventTypeUpdate &&
-			event.Service.Ip == providerV2IPFoo &&
-			event.Service.GetParam(constant.MeshClusterIDKey, "") == providerClusterV2NameFoo {
-			return true
-		}
-		return false
-	}))
-
-	notifyListener.AssertCalled(t, "Notify", mock.MatchedBy(func(event *registry.ServiceEvent) bool {
-		if event.Action == remoting.EventTypeUpdate &&
-			event.Service.Ip == providerV1IPFoo &&
-			event.Service.GetParam(constant.MeshClusterIDKey, "") == providerClusterV1NameFoo {
-			return true
-		}
-		return false
-	}))
-
-	notifyListener.AssertCalled(t, "Notify", mock.MatchedBy(func(event *registry.ServiceEvent) bool {
-		if event.Action == remoting.EventTypeUpdate &&
-			event.Service.Ip == providerIPFoo &&
-			event.Service.GetParam(constant.MeshClusterIDKey, "") == providerClusterNameFoo {
-			return true
-		}
-		return false
-	}))
-}
-
-// todo TestDestroy
-// todo TestRDS
diff --git a/xds/balancer/cdsbalancer/cdsbalancer.go b/xds/balancer/cdsbalancer/cdsbalancer.go
index 621c2a0b9..f163fd7de 100644
--- a/xds/balancer/cdsbalancer/cdsbalancer.go
+++ b/xds/balancer/cdsbalancer/cdsbalancer.go
@@ -39,7 +39,6 @@ import (
 	"google.golang.org/grpc/connectivity"
 
 	"google.golang.org/grpc/credentials"
-	"google.golang.org/grpc/credentials/tls/certprovider"
 
 	"google.golang.org/grpc/resolver"
 
@@ -51,6 +50,7 @@ import (
 	"dubbo.apache.org/dubbo-go/v3/xds/balancer/ringhash"
 	"dubbo.apache.org/dubbo-go/v3/xds/client"
 	"dubbo.apache.org/dubbo-go/v3/xds/client/resource"
+	"dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider"
 	"dubbo.apache.org/dubbo-go/v3/xds/utils/buffer"
 	xdsinternal "dubbo.apache.org/dubbo-go/v3/xds/utils/credentials/xds"
 	"dubbo.apache.org/dubbo-go/v3/xds/utils/grpcsync"
diff --git a/xds/client/bootstrap/bootstrap.go b/xds/client/bootstrap/bootstrap.go
index ffa02fe66..7cc623db9 100644
--- a/xds/client/bootstrap/bootstrap.go
+++ b/xds/client/bootstrap/bootstrap.go
@@ -45,12 +45,12 @@ import (
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials/google"
 	"google.golang.org/grpc/credentials/insecure"
-	"google.golang.org/grpc/credentials/tls/certprovider"
 )
 
 import (
 	"dubbo.apache.org/dubbo-go/v3/xds/client/resource/version"
-	internal2 "dubbo.apache.org/dubbo-go/v3/xds/internal"
+	"dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider"
+	"dubbo.apache.org/dubbo-go/v3/xds/internal"
 	"dubbo.apache.org/dubbo-go/v3/xds/utils/envconfig"
 	"dubbo.apache.org/dubbo-go/v3/xds/utils/pretty"
 )
@@ -346,7 +346,7 @@ func NewConfigFromContents(data []byte) (*Config, error) {
 				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
 			}
 			configs := make(map[string]*certprovider.BuildableConfig)
-			getBuilder := internal2.GetCertificateProviderBuilder.(func(string) certprovider.Builder)
+			getBuilder := internal.GetCertificateProviderBuilder.(func(string) certprovider.Builder)
 			for instance, data := range providerInstances {
 				var nameAndConfig struct {
 					PluginName string          `json:"plugin_name"`
diff --git a/xds/credentials/cert_manager.go b/xds/credentials/cert_manager.go
new file mode 100644
index 000000000..0e50d96b3
--- /dev/null
+++ b/xds/credentials/cert_manager.go
@@ -0,0 +1,271 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 credentials
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strconv"
+	"strings"
+	"time"
+)
+
+import (
+	"github.com/dubbogo/gost/log/logger"
+)
+
+import (
+	"dubbo.apache.org/dubbo-go/v3/xds/client/bootstrap"
+	"dubbo.apache.org/dubbo-go/v3/xds/credentials/certgenerate"
+	"dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider"
+	"dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider/pemfile"
+	"dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider/remote"
+	"dubbo.apache.org/dubbo-go/v3/xds/internal"
+	"dubbo.apache.org/dubbo-go/v3/xds/utils/envconfig"
+)
+
+func init() {
+	//init file_watcher builder
+	certprovider.Register(&pemfile.PluginBuilder{})
+	//init builder func
+	internal.GetCertificateProviderBuilder = certprovider.GetBuilder
+}
+
+//CertManager manage agent or no agent cert
+type CertManager interface {
+	GetCertificate() ([]tls.Certificate, error)
+	GetRootCertificate() (*x509.CertPool, error)
+}
+
+// NewCertManager return a manager
+func NewCertManager() (CertManager, error) {
+	bootstrapPath := os.Getenv(envconfig.XDSBootstrapFileNameEnv)
+	if bootstrapPath != "" {
+		manager := &AgentCertManager{}
+		config, err := bootstrap.NewConfig()
+		if err != nil {
+			logger.Errorf("build bootstrap config error :%s", err.Error())
+			return nil, err
+		}
+		certProvider, err := buildProvider(config.CertProviderConfigs, "default")
+
+		if err != nil {
+			logger.Errorf("get cert provider error :%s", err.Error())
+			return nil, err
+		}
+		manager.provider = certProvider
+		return manager, nil
+	} else {
+		manager := &CACertManager{}
+		manager.rootPath = RootCertPath
+		return manager, nil
+	}
+
+}
+
+// AgentCertManager work in istio agent mode
+type AgentCertManager struct {
+	provider certprovider.Provider
+}
+
+//GetRootCertificate return certificate of ca
+func (c *AgentCertManager) GetRootCertificate() (*x509.CertPool, error) {
+	material, err := c.provider.KeyMaterial(context.Background())
+	if err != nil {
+		return nil, err
+	}
+	return material.Roots, nil
+}
+
+//GetCertificate return certificate of application
+func (c *AgentCertManager) GetCertificate() ([]tls.Certificate, error) {
+	material, err := c.provider.KeyMaterial(context.Background())
+	if err != nil {
+		return nil, err
+	}
+	return material.Certs, nil
+}
+
+// buildProvider build cert provider from config
+func buildProvider(configs map[string]*certprovider.BuildableConfig, instanceName string) (certprovider.Provider, error) {
+	cfg, ok := configs[instanceName]
+	if !ok {
+		return nil, fmt.Errorf("certificate provider instance %q not found in bootstrap file", instanceName)
+	}
+	provider, err := cfg.Build(certprovider.BuildOptions{
+		CertName:     "ca",
+		WantIdentity: true,
+		WantRoot:     true,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("xds: failed to get security plugin instance (%+v): %v", cfg, err)
+	}
+	return provider, nil
+}
+
+// CACertManager work in no agent mode, fetch cert form CA
+type CACertManager struct {
+	// Certs contains a slice of cert/key pairs used to prove local identity.
+	Certs []tls.Certificate
+	// Roots contains the set of trusted roots to validate the peer's identity.
+	Roots *x509.CertPool
+
+	NoAfter time.Time
+
+	RootNoAfter time.Time
+
+	rootPath string
+}
+
+//GetCertificate return certificate of application
+func (c *CACertManager) GetCertificate() ([]tls.Certificate, error) {
+	//cert expired
+	if time.Now().After(c.NoAfter) {
+		if err := c.UpdateCert(); err != nil {
+			return nil, err
+		}
+	}
+	return c.Certs, nil
+}
+
+//GetRootCertificate return certificate of ca
+func (c *CACertManager) GetRootCertificate() (*x509.CertPool, error) {
+	//root expired
+	if time.Now().After(c.RootNoAfter) {
+		if err := c.UpdateRoot(); err != nil {
+			return nil, err
+		}
+	}
+	return c.Roots, nil
+}
+
+// UpdateRoot update root cert
+func (c *CACertManager) UpdateRoot() error {
+	rootFileContents, err := ioutil.ReadFile(c.rootPath)
+	if err != nil {
+		return err
+	}
+	trustPool := x509.NewCertPool()
+	if !trustPool.AppendCertsFromPEM(rootFileContents) {
+		logger.Warn("failed to parse root certificate")
+	}
+	c.Roots = trustPool
+	block, _ := pem.Decode(rootFileContents)
+	if block == nil {
+		return fmt.Errorf("failed to decode certificate")
+	}
+	cert, err := x509.ParseCertificate(block.Bytes)
+	if err != nil {
+		return err
+	}
+	c.RootNoAfter = cert.NotAfter
+	return nil
+}
+
+// UpdateCert update cert
+func (c *CACertManager) UpdateCert() error {
+	tokenProvider, err := NewSaTokenProvider(ServiceAccountPath)
+	if err != nil {
+		return err
+	}
+
+	trustRoot, err := c.GetRootCertificate()
+	if err != nil {
+		return err
+	}
+	citadelClient, err := remote.NewCitadelClient(&remote.Options{
+		CAEndpoint:    IstioCAEndpoint,
+		TrustedRoots:  trustRoot,
+		TokenProvider: tokenProvider,
+		CertSigner:    certSigner,
+		ClusterID:     clusterID,
+	})
+	if err != nil {
+		return err
+	}
+	host := URIPrefix + Domain + "/ns/" + PodNamespace + "/sa/" + ServiceAccountName
+	ttl, err := strconv.ParseInt(CertTTL, 10, 64)
+	if err != nil {
+		return err
+	}
+
+	options := certgenerate.CertOptions{
+		Host:       host,
+		RSAKeySize: 2048,
+		PKCS8Key:   true,
+		TTL:        time.Duration(ttl),
+	}
+
+	// Generate the cert/key, send CSR to CA.
+	csrPEM, keyPEM, err := certgenerate.GenCSR(options)
+	if err != nil {
+		logger.Errorf("failed to generate key and certificate for CSR: %v", err)
+		return err
+	}
+	sign, err := citadelClient.CSRSign(csrPEM, ttl)
+
+	if err != nil {
+		return err
+	}
+
+	cert, err := c.parseCert(concatCerts(sign), keyPEM)
+	if err != nil {
+		return err
+	}
+	c.Certs = []tls.Certificate{*cert}
+	return nil
+}
+
+func (c *CACertManager) parseCert(certByte []byte, keyByte []byte) (*tls.Certificate, error) {
+	block, _ := pem.Decode(certByte)
+	if block == nil {
+		return nil, fmt.Errorf("failed to decode certificate")
+	}
+	cert, err := x509.ParseCertificate(block.Bytes)
+	if err != nil {
+		return nil, err
+	}
+	expired := cert.NotAfter
+	logger.Infof("cert expired after:" + expired.String())
+	c.NoAfter = expired
+	pair, err := tls.X509KeyPair(certByte, keyByte)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse certificate: %v", err)
+	}
+	return &pair, nil
+}
+
+func concatCerts(certsPEM []string) []byte {
+	if len(certsPEM) == 0 {
+		return []byte{}
+	}
+	var certChain bytes.Buffer
+	for i, c := range certsPEM {
+		certChain.WriteString(c)
+		if i < len(certsPEM)-1 && !strings.HasSuffix(c, "\n") {
+			certChain.WriteString("\n")
+		}
+	}
+	return certChain.Bytes()
+}
diff --git a/xds/credentials/certgenerate/crypto.go b/xds/credentials/certgenerate/crypto.go
new file mode 100644
index 000000000..00ab1b10d
--- /dev/null
+++ b/xds/credentials/certgenerate/crypto.go
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/*
+ *
+ * Copyright Istio Authors
+ *
+ */
+
+package certgenerate
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/rsa"
+	"crypto/x509"
+	"encoding/pem"
+	"fmt"
+	"reflect"
+	"strings"
+)
+
+const (
+	blockTypeECPrivateKey    = "EC PRIVATE KEY"
+	blockTypeRSAPrivateKey   = "RSA PRIVATE KEY" // PKCS#1 private key
+	blockTypePKCS8PrivateKey = "PRIVATE KEY"     // PKCS#8 plain private key
+)
+
+// ParsePemEncodedCertificate constructs a `x509.Certificate` object using the
+// given a PEM-encoded certificate.
+func ParsePemEncodedCertificate(certBytes []byte) (*x509.Certificate, error) {
+	cb, _ := pem.Decode(certBytes)
+	if cb == nil {
+		return nil, fmt.Errorf("invalid PEM encoded certificate")
+	}
+
+	cert, err := x509.ParseCertificate(cb.Bytes)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse X.509 certificate")
+	}
+
+	return cert, nil
+}
+
+// ParsePemEncodedCertificateChain constructs a slice of `x509.Certificate`
+// objects using the given a PEM-encoded certificate chain.
+func ParsePemEncodedCertificateChain(certBytes []byte) ([]*x509.Certificate, error) {
+	var (
+		certs []*x509.Certificate
+		cb    *pem.Block
+	)
+	for {
+		cb, certBytes = pem.Decode(certBytes)
+		if cb == nil {
+			break
+		}
+		cert, err := x509.ParseCertificate(cb.Bytes)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse X.509 certificate")
+		}
+		certs = append(certs, cert)
+	}
+	if len(certs) == 0 {
+		return nil, fmt.Errorf("no PEM encoded X.509 certificates parsed")
+	}
+	return certs, nil
+}
+
+// ParsePemEncodedCSR constructs a `x509.CertificateRequest` object using the
+// given PEM-encoded certificate signing request.
+func ParsePemEncodedCSR(csrBytes []byte) (*x509.CertificateRequest, error) {
+	block, _ := pem.Decode(csrBytes)
+	if block == nil {
+		return nil, fmt.Errorf("certificate signing request is not properly encoded")
+	}
+	csr, err := x509.ParseCertificateRequest(block.Bytes)
+	if err != nil {
+		return nil, fmt.Errorf("failed to parse X.509 certificate signing request")
+	}
+	return csr, nil
+}
+
+// ParsePemEncodedKey takes a PEM-encoded key and parsed the bytes into a `crypto.PrivateKey`.
+func ParsePemEncodedKey(keyBytes []byte) (crypto.PrivateKey, error) {
+	kb, _ := pem.Decode(keyBytes)
+	if kb == nil {
+		return nil, fmt.Errorf("invalid PEM-encoded key")
+	}
+
+	switch kb.Type {
+	case blockTypeECPrivateKey:
+		key, err := x509.ParseECPrivateKey(kb.Bytes)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse the ECDSA private key")
+		}
+		return key, nil
+	case blockTypeRSAPrivateKey:
+		key, err := x509.ParsePKCS1PrivateKey(kb.Bytes)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse the RSA private key")
+		}
+		return key, nil
+	case blockTypePKCS8PrivateKey:
+		key, err := x509.ParsePKCS8PrivateKey(kb.Bytes)
+		if err != nil {
+			return nil, fmt.Errorf("failed to parse the PKCS8 private key")
+		}
+		return key, nil
+	default:
+		return nil, fmt.Errorf("unsupported PEM block type for a private key: %s", kb.Type)
+	}
+}
+
+// GetRSAKeySize returns the size if it is RSA key, otherwise it returns an error.
+func GetRSAKeySize(privKey crypto.PrivateKey) (int, error) {
+	if t := reflect.TypeOf(privKey); t != reflect.TypeOf(&rsa.PrivateKey{}) {
+		return 0, fmt.Errorf("key type is not RSA: %v", t)
+	}
+	pkey := privKey.(*rsa.PrivateKey)
+	return pkey.N.BitLen(), nil
+}
+
+// IsSupportedECPrivateKey is a predicate returning true if the private key is EC based
+func IsSupportedECPrivateKey(privKey *crypto.PrivateKey) bool {
+	switch (*privKey).(type) {
+	// this should agree with var SupportedECSignatureAlgorithms
+	case *ecdsa.PrivateKey:
+		return true
+	default:
+		return false
+	}
+}
+
+// PemCertBytestoString: takes an array of PEM certs in bytes and returns a string array in the same order with
+// trailing newline characters removed
+func PemCertBytestoString(caCerts []byte) []string {
+	certs := []string{}
+	var cert string
+	pemBlock := caCerts
+	for block, rest := pem.Decode(pemBlock); block != nil && len(block.Bytes) != 0; block, rest = pem.Decode(pemBlock) {
+		if len(rest) == 0 {
+			cert = strings.TrimPrefix(strings.TrimSuffix(string(pemBlock), "\n"), "\n")
+			certs = append(certs, cert)
+			break
+		}
+		cert = string(pemBlock[0 : len(pemBlock)-len(rest)])
+		cert = strings.TrimPrefix(strings.TrimSuffix(cert, "\n"), "\n")
+		certs = append(certs, cert)
+		pemBlock = rest
+	}
+	return certs
+}
diff --git a/xds/credentials/certgenerate/dual_use.go b/xds/credentials/certgenerate/dual_use.go
new file mode 100644
index 000000000..86cb16ceb
--- /dev/null
+++ b/xds/credentials/certgenerate/dual_use.go
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/*
+ *
+ * Copyright Istio Authors
+ *
+ */
+
+package certgenerate
+
+import (
+	"fmt"
+	"strings"
+)
+
+// DualUseCommonName extracts a valid CommonName from a comma-delimited host string
+// for dual-use certificates.
+func DualUseCommonName(host string) (string, error) {
+	// cn uses one hostname, drop the rest
+	first := strings.SplitN(host, ",", 2)[0]
+
+	// cn max length is 64 (ub-common-name @ https://tools.ietf.org/html/rfc5280)
+	if l := len(first); l > 64 {
+		return "", fmt.Errorf("certificate CN upper bound exceeded (%v>64): %s", l, first)
+	}
+
+	return first, nil
+}
diff --git a/xds/credentials/certgenerate/generate_cert.go b/xds/credentials/certgenerate/generate_cert.go
new file mode 100644
index 000000000..8f590be33
--- /dev/null
+++ b/xds/credentials/certgenerate/generate_cert.go
@@ -0,0 +1,440 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/*
+ *
+ * Copyright Istio Authors
+ *
+ */
+
+package certgenerate
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/ed25519"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"math/big"
+	"strings"
+	"time"
+)
+
+import (
+	"github.com/dubbogo/gost/log/logger"
+)
+
+// SupportedECSignatureAlgorithms are the types of EC Signature Algorithms
+// to be used in key generation (e.g. ECDSA or ED2551)
+type SupportedECSignatureAlgorithms string
+
+const (
+	// only ECDSA using P256 is currently supported
+	EcdsaSigAlg SupportedECSignatureAlgorithms = "ECDSA"
+)
+
+// CertOptions contains options for generating a new certificate.
+type CertOptions struct {
+	// Comma-separated hostnames and IPs to generate a certificate for.
+	// This can also be set to the identity running the workload,
+	// like kubernetes service account.
+	Host string
+
+	// The NotBefore field of the issued certificate.
+	NotBefore time.Time
+
+	// TTL of the certificate. NotAfter - NotBefore.
+	TTL time.Duration
+
+	// Signer certificate.
+	SignerCert *x509.Certificate
+
+	// Signer private key.
+	SignerPriv crypto.PrivateKey
+
+	// Signer private key (PEM encoded).
+	SignerPrivPem []byte
+
+	// Organization for this certificate.
+	Org string
+
+	// The size of RSA private key to be generated.
+	RSAKeySize int
+
+	// Whether this certificate is used as signing cert for CA.
+	IsCA bool
+
+	// Whether this certificate is self-signed.
+	IsSelfSigned bool
+
+	// Whether this certificate is for a client.
+	IsClient bool
+
+	// Whether this certificate is for a server.
+	IsServer bool
+
+	// Whether this certificate is for dual-use clients (SAN+CN).
+	IsDualUse bool
+
+	// If true, the private key is encoded with PKCS#8.
+	PKCS8Key bool
+
+	// The type of Elliptical Signature algorithm to use
+	// when generating private keys. Currently only ECDSA is supported.
+	// If empty, RSA is used, otherwise ECC is used.
+	ECSigAlg SupportedECSignatureAlgorithms
+
+	// Subjective Alternative Name values.
+	DNSNames string
+}
+
+// GenCertKeyFromOptions generates a X.509 certificate and a private key with the given options.
+func GenCertKeyFromOptions(options CertOptions) (pemCert []byte, pemKey []byte, err error) {
+	// Generate the appropriate private&public key pair based on options.
+	// The public key will be bound to the certificate generated below. The
+	// private key will be used to sign this certificate in the self-signed
+	// case, otherwise the certificate is signed by the signer private key
+	// as specified in the CertOptions.
+	if options.ECSigAlg != "" {
+		var ecPriv *ecdsa.PrivateKey
+
+		switch options.ECSigAlg {
+		case EcdsaSigAlg:
+			ecPriv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+			if err != nil {
+				return nil, nil, fmt.Errorf("cert generation fails at EC key generation (%v)", err)
+			}
+		default:
+			return nil, nil, errors.New("cert generation fails due to unsupported EC signature algorithm")
+		}
+		return genCert(options, ecPriv, &ecPriv.PublicKey)
+	}
+
+	if options.RSAKeySize < minimumRsaKeySize {
+		return nil, nil, fmt.Errorf("requested key size does not meet the minimum requied size of %d (requested: %d)", minimumRsaKeySize, options.RSAKeySize)
+	}
+	rsaPriv, err := rsa.GenerateKey(rand.Reader, options.RSAKeySize)
+	if err != nil {
+		return nil, nil, fmt.Errorf("cert generation fails at RSA key generation (%v)", err)
+	}
+	return genCert(options, rsaPriv, &rsaPriv.PublicKey)
+}
+
+func genCert(options CertOptions, priv interface{}, key interface{}) ([]byte, []byte, error) {
+	template, err := genCertTemplateFromOptions(options)
+	if err != nil {
+		return nil, nil, fmt.Errorf("cert generation fails at cert template creation (%v)", err)
+	}
+	signerCert, signerKey := template, crypto.PrivateKey(priv)
+	if !options.IsSelfSigned {
+		signerCert, signerKey = options.SignerCert, options.SignerPriv
+	}
+	certBytes, err := x509.CreateCertificate(rand.Reader, template, signerCert, key, signerKey)
+	if err != nil {
+		return nil, nil, fmt.Errorf("cert generation fails at X509 cert creation (%v)", err)
+	}
+
+	pemCert, pemKey, err := encodePem(false, certBytes, priv, options.PKCS8Key)
+	return pemCert, pemKey, err
+}
+
+func publicKey(priv interface{}) interface{} {
+	switch k := priv.(type) {
+	case *rsa.PrivateKey:
+		return &k.PublicKey
+	case *ecdsa.PrivateKey:
+		return &k.PublicKey
+	case ed25519.PrivateKey:
+		return k.Public().(ed25519.PublicKey)
+	default:
+		return nil
+	}
+}
+
+// GenRootCertFromExistingKey generates a X.509 certificate using existing
+// CA private key. Only called by a self-signed Citadel.
+func GenRootCertFromExistingKey(options CertOptions) (pemCert []byte, pemKey []byte, err error) {
+	if !options.IsSelfSigned || len(options.SignerPrivPem) == 0 {
+		return nil, nil, fmt.Errorf("skip cert " +
+			"generation. Citadel is not in self-signed mode or CA private key is not " +
+			"available")
+	}
+
+	template, err := genCertTemplateFromOptions(options)
+	if err != nil {
+		return nil, nil, fmt.Errorf("cert generation fails at cert template creation (%v)", err)
+	}
+	caPrivateKey, err := ParsePemEncodedKey(options.SignerPrivPem)
+	if err != nil {
+		return nil, nil, fmt.Errorf("unrecogniazed CA "+
+			"private key, skip root cert rotation: %s", err.Error())
+	}
+	certBytes, err := x509.CreateCertificate(rand.Reader, template, template, publicKey(caPrivateKey), caPrivateKey)
+	if err != nil {
+		return nil, nil, fmt.Errorf("cert generation fails at X509 cert creation (%v)", err)
+	}
+
+	pemCert = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
+	return pemCert, options.SignerPrivPem, nil
+}
+
+// GetCertOptionsFromExistingCert parses cert and generates a CertOptions
+// that contains information about the cert. This is the reverse operation of
+// genCertTemplateFromOptions(), and only called by a self-signed Citadel.
+func GetCertOptionsFromExistingCert(certBytes []byte) (opts CertOptions, err error) {
+	cert, certErr := ParsePemEncodedCertificate(certBytes)
+	if certErr != nil {
+		return opts, certErr
+	}
+
+	orgs := cert.Subject.Organization
+	if len(orgs) > 0 {
+		opts.Org = orgs[0]
+	}
+	// TODO(JimmyCYJ): parse other fields from certificate, e.g. CommonName.
+	return opts, nil
+}
+
+// MergeCertOptions merges deltaOpts into defaultOpts and returns the merged
+// CertOptions. Only called by a self-signed Citadel.
+func MergeCertOptions(defaultOpts, deltaOpts CertOptions) CertOptions {
+	if len(deltaOpts.Org) > 0 {
+		defaultOpts.Org = deltaOpts.Org
+	}
+	// TODO(JimmyCYJ): merge other fields, e.g. Host, IsDualUse, etc.
+	return defaultOpts
+}
+
+// GenCertFromCSR generates a X.509 certificate with the given CSR.
+func GenCertFromCSR(csr *x509.CertificateRequest, signingCert *x509.Certificate, publicKey interface{},
+	signingKey crypto.PrivateKey, subjectIDs []string, ttl time.Duration, isCA bool,
+) (cert []byte, err error) {
+	tmpl, err := genCertTemplateFromCSR(csr, subjectIDs, ttl, isCA)
+	if err != nil {
+		return nil, err
+	}
+	return x509.CreateCertificate(rand.Reader, tmpl, signingCert, publicKey, signingKey)
+}
+
+// LoadSignerCredsFromFiles loads the signer cert&key from the given files.
+//
+//	signerCertFile: cert file name
+//	signerPrivFile: private key file name
+func LoadSignerCredsFromFiles(signerCertFile string, signerPrivFile string) (*x509.Certificate, crypto.PrivateKey, error) {
+	signerCertBytes, err := ioutil.ReadFile(signerCertFile)
+	if err != nil {
+		return nil, nil, fmt.Errorf("certificate file reading failure (%v)", err)
+	}
+
+	signerPrivBytes, err := ioutil.ReadFile(signerPrivFile)
+	if err != nil {
+		return nil, nil, fmt.Errorf("private key file reading failure (%v)", err)
+	}
+
+	cert, err := ParsePemEncodedCertificate(signerCertBytes)
+	if err != nil {
+		return nil, nil, fmt.Errorf("pem encoded cert parsing failure (%v)", err)
+	}
+
+	key, err := ParsePemEncodedKey(signerPrivBytes)
+	if err != nil {
+		return nil, nil, fmt.Errorf("pem encoded key parsing failure (%v)", err)
+	}
+
+	return cert, key, nil
+}
+
+// ClockSkewGracePeriod defines the period of time a certificate will be valid before its creation.
+// This is meant to handle cases where we have clock skew between the CA and workloads.
+const ClockSkewGracePeriod = time.Minute * 2
+
+// genCertTemplateFromCSR generates a certificate template with the given CSR.
+// The NotBefore value of the cert is set to current time.
+func genCertTemplateFromCSR(csr *x509.CertificateRequest, subjectIDs []string, ttl time.Duration, isCA bool) (
+	*x509.Certificate, error,
+) {
+	subjectIDsInString := strings.Join(subjectIDs, ",")
+	var keyUsage x509.KeyUsage
+	extKeyUsages := []x509.ExtKeyUsage{}
+	if isCA {
+		// If the cert is a CA cert, the private key is allowed to sign other certificates.
+		keyUsage = x509.KeyUsageCertSign
+	} else {
+		// Otherwise the private key is allowed for digital signature and key encipherment.
+		keyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
+		// For now, we do not differentiate non-CA certs to be used on client auth or server auth.
+		extKeyUsages = append(extKeyUsages, x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth)
+	}
+
+	// Build cert extensions with the subjectIDs.
+	ext, err := BuildSubjectAltNameExtension(subjectIDsInString)
+	if err != nil {
+		return nil, err
+	}
+	exts := []pkix.Extension{*ext}
+
+	subject := pkix.Name{}
+	// Dual use mode if common name in CSR is not empty.
+	// In this case, set CN as determined by DualUseCommonName(subjectIDsInString).
+	if len(csr.Subject.CommonName) != 0 {
+		var cn string
+		if cn, err = DualUseCommonName(subjectIDsInString); err != nil {
+			// logger and continue
+			logger.Errorf("dual-use failed for cert template - omitting CN (%v)", err)
+		} else {
+			subject.CommonName = cn
+		}
+	}
+
+	now := time.Now()
+
+	serialNum, err := genSerialNum()
+	if err != nil {
+		return nil, err
+	}
+	// SignatureAlgorithm will use the default algorithm.
+	// See https://golang.org/src/crypto/x509/x509.go?s=5131:5158#L1965 .
+	return &x509.Certificate{
+		SerialNumber:          serialNum,
+		Subject:               subject,
+		NotBefore:             now.Add(-ClockSkewGracePeriod),
+		NotAfter:              now.Add(ttl),
+		KeyUsage:              keyUsage,
+		ExtKeyUsage:           extKeyUsages,
+		IsCA:                  isCA,
+		BasicConstraintsValid: true,
+		ExtraExtensions:       exts,
+	}, nil
+}
+
+// genCertTemplateFromoptions generates a certificate template with the given options.
+func genCertTemplateFromOptions(options CertOptions) (*x509.Certificate, error) {
+	var keyUsage x509.KeyUsage
+	if options.IsCA {
+		// If the cert is a CA cert, the private key is allowed to sign other certificates.
+		keyUsage = x509.KeyUsageCertSign
+	} else {
+		// Otherwise the private key is allowed for digital signature and key encipherment.
+		keyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
+	}
+
+	extKeyUsages := []x509.ExtKeyUsage{}
+	if options.IsServer {
+		extKeyUsages = append(extKeyUsages, x509.ExtKeyUsageServerAuth)
+	}
+	if options.IsClient {
+		extKeyUsages = append(extKeyUsages, x509.ExtKeyUsageClientAuth)
+	}
+
+	notBefore := time.Now()
+	if !options.NotBefore.IsZero() {
+		notBefore = options.NotBefore
+	}
+
+	serialNum, err := genSerialNum()
+	if err != nil {
+		return nil, err
+	}
+
+	subject := pkix.Name{
+		Organization: []string{options.Org},
+	}
+
+	exts := []pkix.Extension{}
+	if h := options.Host; len(h) > 0 {
+		s, err := BuildSubjectAltNameExtension(h)
+		if err != nil {
+			return nil, err
+		}
+		if options.IsDualUse {
+			cn, err := DualUseCommonName(h)
+			if err != nil {
+				// logger and continue
+				logger.Errorf("dual-use failed for cert template - omitting CN (%v)", err)
+			} else {
+				subject.CommonName = cn
+			}
+		}
+		exts = []pkix.Extension{*s}
+	}
+
+	dnsNames := strings.Split(options.DNSNames, ",")
+	if len(dnsNames[0]) == 0 {
+		dnsNames = nil
+	}
+
+	return &x509.Certificate{
+		SerialNumber:          serialNum,
+		Subject:               subject,
+		NotBefore:             notBefore,
+		NotAfter:              notBefore.Add(options.TTL),
+		KeyUsage:              keyUsage,
+		ExtKeyUsage:           extKeyUsages,
+		IsCA:                  options.IsCA,
+		BasicConstraintsValid: true,
+		ExtraExtensions:       exts,
+		DNSNames:              dnsNames,
+	}, nil
+}
+
+func genSerialNum() (*big.Int, error) {
+	serialNumLimit := new(big.Int).Lsh(big.NewInt(1), 128)
+	serialNum, err := rand.Int(rand.Reader, serialNumLimit)
+	if err != nil {
+		return nil, fmt.Errorf("serial number generation failure (%v)", err)
+	}
+	return serialNum, nil
+}
+
+func encodePem(isCSR bool, csrOrCert []byte, priv interface{}, pkcs8 bool) (
+	csrOrCertPem []byte, privPem []byte, err error,
+) {
+	encodeMsg := "CERTIFICATE"
+	if isCSR {
+		encodeMsg = "CERTIFICATE REQUEST"
+	}
+	csrOrCertPem = pem.EncodeToMemory(&pem.Block{Type: encodeMsg, Bytes: csrOrCert})
+
+	var encodedKey []byte
+	if pkcs8 {
+		if encodedKey, err = x509.MarshalPKCS8PrivateKey(priv); err != nil {
+			return nil, nil, err
+		}
+		privPem = pem.EncodeToMemory(&pem.Block{Type: blockTypePKCS8PrivateKey, Bytes: encodedKey})
+	} else {
+		switch k := priv.(type) {
+		case *rsa.PrivateKey:
+			encodedKey = x509.MarshalPKCS1PrivateKey(k)
+			privPem = pem.EncodeToMemory(&pem.Block{Type: blockTypeRSAPrivateKey, Bytes: encodedKey})
+		case *ecdsa.PrivateKey:
+			encodedKey, err = x509.MarshalECPrivateKey(k)
+			if err != nil {
+				return nil, nil, err
+			}
+			privPem = pem.EncodeToMemory(&pem.Block{Type: blockTypeECPrivateKey, Bytes: encodedKey})
+		}
+	}
+	err = nil
+	return
+}
diff --git a/xds/credentials/certgenerate/generate_csr.go b/xds/credentials/certgenerate/generate_csr.go
new file mode 100644
index 000000000..7e1edf0af
--- /dev/null
+++ b/xds/credentials/certgenerate/generate_csr.go
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/*
+ *
+ * Copyright Istio Authors
+ *
+ */
+
+package certgenerate
+
+import (
+	"crypto"
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/x509"
+	"crypto/x509/pkix"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"strings"
+)
+
+import (
+	"github.com/dubbogo/gost/log/logger"
+)
+
+// minimumRsaKeySize is the minimum RSA key size to generate certificates
+// to ensure proper security
+const minimumRsaKeySize = 2048
+
+// GenCSR generates a X.509 certificate sign request and private key with the given options.
+func GenCSR(options CertOptions) ([]byte, []byte, error) {
+	var priv interface{}
+	var err error
+	if options.ECSigAlg != "" {
+		switch options.ECSigAlg {
+		case EcdsaSigAlg:
+			priv, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+			if err != nil {
+				return nil, nil, fmt.Errorf("EC key generation failed (%v)", err)
+			}
+		default:
+			return nil, nil, errors.New("csr cert generation fails due to unsupported EC signature algorithm")
+		}
+	} else {
+		if options.RSAKeySize < minimumRsaKeySize {
+			return nil, nil, fmt.Errorf("requested key size does not meet the minimum requied size of %d (requested: %d)", minimumRsaKeySize, options.RSAKeySize)
+		}
+
+		priv, err = rsa.GenerateKey(rand.Reader, options.RSAKeySize)
+		if err != nil {
+			return nil, nil, fmt.Errorf("RSA key generation failed (%v)", err)
+		}
+	}
+	template, err := GenCSRTemplate(options)
+	if err != nil {
+		return nil, nil, fmt.Errorf("CSR template creation failed (%v)", err)
+	}
+
+	csrBytes, err := x509.CreateCertificateRequest(rand.Reader, template, crypto.PrivateKey(priv))
+	if err != nil {
+		return nil, nil, fmt.Errorf("CSR creation failed (%v)", err)
+	}
+
+	csr, privKey, err := encodePem(true, csrBytes, priv, options.PKCS8Key)
+	return csr, privKey, err
+}
+
+// GenCSRTemplate generates a certificateRequest template with the given options.
+func GenCSRTemplate(options CertOptions) (*x509.CertificateRequest, error) {
+	template := &x509.CertificateRequest{
+		Subject: pkix.Name{
+			Organization: []string{options.Org},
+		},
+	}
+
+	if h := options.Host; len(h) > 0 {
+		s, err := BuildSubjectAltNameExtension(h)
+		if err != nil {
+			return nil, err
+		}
+		if options.IsDualUse {
+			cn, err := DualUseCommonName(h)
+			if err != nil {
+				// logger and continue
+				//logger.Errorf("dual-use failed for CSR template - omitting CN (%v)", err)
+			} else {
+				template.Subject.CommonName = cn
+			}
+		}
+		template.ExtraExtensions = []pkix.Extension{*s}
+	}
+
+	return template, nil
+}
+
+// AppendRootCerts appends root certificates in RootCertFile to the input certificate.
+func AppendRootCerts(pemCert []byte, rootCertFile string) ([]byte, error) {
+	rootCerts := pemCert
+	if len(rootCertFile) > 0 {
+		logger.Debugf("append root certificates from %v", rootCertFile)
+		certBytes, err := ioutil.ReadFile(rootCertFile)
+		if err != nil {
+			return rootCerts, fmt.Errorf("failed to read root certificates (%v)", err)
+		}
+		rootCerts = AppendCertByte(pemCert, certBytes)
+	}
+	return rootCerts, nil
+}
+
+// AppendCertByte: Append x.509 rootCert in bytes to existing certificate chain (in bytes)
+func AppendCertByte(pemCert []byte, rootCert []byte) []byte {
+	rootCerts := []byte{}
+	if len(pemCert) > 0 {
+		// Copy the input certificate
+		rootCerts = []byte(strings.TrimSuffix(string(pemCert), "\n") + "\n")
+	}
+	rootCerts = append(rootCerts, rootCert...)
+	return rootCerts
+}
diff --git a/xds/credentials/certgenerate/san.go b/xds/credentials/certgenerate/san.go
new file mode 100644
index 000000000..772868141
--- /dev/null
+++ b/xds/credentials/certgenerate/san.go
@@ -0,0 +1,213 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/*
+ *
+ * Copyright Istio Authors
+ *
+ */
+
+package certgenerate
+
+import (
+	"crypto/x509/pkix"
+	"encoding/asn1"
+	"fmt"
+	"net"
+	"strings"
+)
+
+// IdentityType represents type of an identity. This is used to properly encode
+// an identity into a SAN extension.
+type IdentityType int
+
+const (
+	// TypeDNS represents a DNS name.
+	TypeDNS IdentityType = iota
+	// TypeIP represents an IP address.
+	TypeIP
+	// TypeURI represents a universal resource identifier.
+	TypeURI
+
+	Scheme       = "spiffe"
+	URIPrefix    = Scheme + "://"
+	URIPrefixLen = len(URIPrefix)
+)
+
+var (
+	// Mapping from the type of an identity to the OID tag value for the X.509
+	// SAN field (see https://tools.ietf.org/html/rfc5280#appendix-A.2)
+	//
+	// SubjectAltName ::= GeneralNames
+	//
+	// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
+	//
+	// GeneralName ::= CHOICE {
+	//      dNSName                         [2]     IA5String,
+	//      uniformResourceIdentifier       [6]     IA5String,
+	//      iPAddress                       [7]     OCTET STRING,
+	// }
+	oidTagMap = map[IdentityType]int{
+		TypeDNS: 2,
+		TypeURI: 6,
+		TypeIP:  7,
+	}
+
+	// A reversed map that maps from an OID tag to the corresponding identity
+	// type.
+	identityTypeMap = generateReversedMap(oidTagMap)
+
+	// The OID for the SAN extension (See
+	// http://www.alvestrand.no/objectid/2.5.29.17.html).
+	oidSubjectAlternativeName = asn1.ObjectIdentifier{2, 5, 29, 17}
+)
+
+// Identity is an object holding both the encoded identifier bytes as well as
+// the type of the identity.
+type Identity struct {
+	Type  IdentityType
+	Value []byte
+}
+
+// BuildSubjectAltNameExtension builds the SAN extension for the certificate.
+func BuildSubjectAltNameExtension(hosts string) (*pkix.Extension, error) {
+	ids := []Identity{}
+	for _, host := range strings.Split(hosts, ",") {
+		if ip := net.ParseIP(host); ip != nil {
+			// Use the 4-byte representation of the IP address when possible.
+			if eip := ip.To4(); eip != nil {
+				ip = eip
+			}
+			ids = append(ids, Identity{Type: TypeIP, Value: ip})
+		} else if strings.HasPrefix(host, URIPrefix) {
+			ids = append(ids, Identity{Type: TypeURI, Value: []byte(host)})
+		} else {
+			ids = append(ids, Identity{Type: TypeDNS, Value: []byte(host)})
+		}
+	}
+
+	san, err := BuildSANExtension(ids)
+	if err != nil {
+		return nil, fmt.Errorf("SAN extension building failure (%v)", err)
+	}
+
+	return san, nil
+}
+
+// BuildSANExtension builds a `pkix.Extension` of type "Subject
+// Alternative Name" based on the given identities.
+func BuildSANExtension(identites []Identity) (*pkix.Extension, error) {
+	rawValues := []asn1.RawValue{}
+	for _, i := range identites {
+		tag, ok := oidTagMap[i.Type]
+		if !ok {
+			return nil, fmt.Errorf("unsupported identity type: %v", i.Type)
+		}
+
+		rawValues = append(rawValues, asn1.RawValue{
+			Bytes: i.Value,
+			Class: asn1.ClassContextSpecific,
+			Tag:   tag,
+		})
+	}
+
+	bs, err := asn1.Marshal(rawValues)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal the raw values for SAN field (err: %s)", err)
+	}
+
+	// SAN is Critical because the subject is empty. This is compliant with X.509 and SPIFFE standards.
+	return &pkix.Extension{Id: oidSubjectAlternativeName, Critical: true, Value: bs}, nil
+}
+
+// ExtractIDsFromSAN takes a SAN extension and extracts the identities.
+// The logic is mostly borrowed from
+// https://github.com/golang/go/blob/master/src/crypto/x509/x509.go, with the
+// addition of supporting extracting URIs.
+func ExtractIDsFromSAN(sanExt *pkix.Extension) ([]Identity, error) {
+	if !sanExt.Id.Equal(oidSubjectAlternativeName) {
+		return nil, fmt.Errorf("the input is not a SAN extension")
+	}
+
+	var sequence asn1.RawValue
+	if rest, err := asn1.Unmarshal(sanExt.Value, &sequence); err != nil {
+		return nil, err
+	} else if len(rest) != 0 {
+		return nil, fmt.Errorf("the SAN extension is incorrectly encoded")
+	}
+
+	// Check the rawValue is a sequence.
+	if !sequence.IsCompound || sequence.Tag != asn1.TagSequence || sequence.Class != asn1.ClassUniversal {
+		return nil, fmt.Errorf("the SAN extension is incorrectly encoded")
+	}
+
+	ids := []Identity{}
+	for bytes := sequence.Bytes; len(bytes) > 0; {
+		var rawValue asn1.RawValue
+		var err error
+
+		bytes, err = asn1.Unmarshal(bytes, &rawValue)
+		if err != nil {
+			return nil, err
+		}
+		ids = append(ids, Identity{Type: identityTypeMap[rawValue.Tag], Value: rawValue.Bytes})
+	}
+
+	return ids, nil
+}
+
+// ExtractSANExtension extracts the "Subject Alternative Name" externsion from
+// the given PKIX extension set.
+func ExtractSANExtension(exts []pkix.Extension) *pkix.Extension {
+	for _, ext := range exts {
+		if ext.Id.Equal(oidSubjectAlternativeName) {
+			// We don't need to examine other extensions anymore since a certificate
+			// must not include more than one instance of a particular extension. See
+			// https://tools.ietf.org/html/rfc5280#section-4.2.
+			return &ext
+		}
+	}
+	return nil
+}
+
+// ExtractIDs first finds the SAN extension from the given extension set, then
+// extract identities from the SAN extension.
+func ExtractIDs(exts []pkix.Extension) ([]string, error) {
+	sanExt := ExtractSANExtension(exts)
+	if sanExt == nil {
+		return nil, fmt.Errorf("the SAN extension does not exist")
+	}
+
+	idsWithType, err := ExtractIDsFromSAN(sanExt)
+	if err != nil {
+		return nil, fmt.Errorf("failed to extract identities from SAN extension (error %v)", err)
+	}
+
+	ids := []string{}
+	for _, id := range idsWithType {
+		ids = append(ids, string(id.Value))
+	}
+	return ids, nil
+}
+
+func generateReversedMap(m map[IdentityType]int) map[int]IdentityType {
+	reversed := make(map[int]IdentityType)
+	for key, value := range m {
+		reversed[value] = key
+	}
+	return reversed
+}
diff --git a/xds/credentials/certprovider/distributor.go b/xds/credentials/certprovider/distributor.go
new file mode 100644
index 000000000..0393d9a0b
--- /dev/null
+++ b/xds/credentials/certprovider/distributor.go
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/*
+ *
+ * Copyright 2020 gRPC authors.
+ *
+ */
+
+package certprovider
+
+import (
+	"context"
+	"sync"
+)
+
+import (
+	"dubbo.apache.org/dubbo-go/v3/xds/utils/grpcsync"
+)
+
+// Distributor makes it easy for provider implementations to furnish new key
+// materials by handling synchronization between the producer and consumers of
+// the key material.
+//
+// Provider implementations which choose to use a Distributor should do the
+// following:
+// - create a new Distributor using the NewDistributor() function.
+// - invoke the Set() method whenever they have new key material or errors to
+//   report.
+// - delegate to the distributor when handing calls to KeyMaterial().
+// - invoke the Stop() method when they are done using the distributor.
+type Distributor struct {
+	// mu protects the underlying key material.
+	mu   sync.Mutex
+	km   *KeyMaterial
+	pErr error
+
+	// ready channel to unblock KeyMaterial() invocations blocked on
+	// availability of key material.
+	ready *grpcsync.Event
+	// done channel to notify provider implementations and unblock any
+	// KeyMaterial() calls, once the Distributor is closed.
+	closed *grpcsync.Event
+}
+
+// NewDistributor returns a new Distributor.
+func NewDistributor() *Distributor {
+	return &Distributor{
+		ready:  grpcsync.NewEvent(),
+		closed: grpcsync.NewEvent(),
+	}
+}
+
+// Set updates the key material in the distributor with km.
+//
+// Provider implementations which use the distributor must not modify the
+// contents of the KeyMaterial struct pointed to by km.
+//
+// A non-nil err value indicates the error that the provider implementation ran
+// into when trying to fetch key material, and makes it possible to surface the
+// error to the user. A non-nil error value passed here causes distributor's
+// KeyMaterial() method to return nil key material.
+func (d *Distributor) Set(km *KeyMaterial, err error) {
+	d.mu.Lock()
+	d.km = km
+	d.pErr = err
+	if err != nil {
+		// If a non-nil err is passed, we ignore the key material being passed.
+		d.km = nil
+	}
+	d.ready.Fire()
+	d.mu.Unlock()
+}
+
+// KeyMaterial returns the most recent key material provided to the Distributor.
+// If no key material was provided at the time of this call, it will block until
+// the deadline on the context expires or fresh key material arrives.
+func (d *Distributor) KeyMaterial(ctx context.Context) (*KeyMaterial, error) {
+	if d.closed.HasFired() {
+		return nil, errProviderClosed
+	}
+
+	if d.ready.HasFired() {
+		return d.keyMaterial()
+	}
+
+	select {
+	case <-ctx.Done():
+		return nil, ctx.Err()
+	case <-d.closed.Done():
+		return nil, errProviderClosed
+	case <-d.ready.Done():
+		return d.keyMaterial()
+	}
+}
+
+func (d *Distributor) keyMaterial() (*KeyMaterial, error) {
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	return d.km, d.pErr
+}
+
+// Stop turns down the distributor, releases allocated resources and fails any
+// active KeyMaterial() call waiting for new key material.
+func (d *Distributor) Stop() {
+	d.closed.Fire()
+}
diff --git a/xds/credentials/certprovider/pemfile/builder.go b/xds/credentials/certprovider/pemfile/builder.go
new file mode 100644
index 000000000..7860d5efa
--- /dev/null
+++ b/xds/credentials/certprovider/pemfile/builder.go
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/*
+ *
+ * Copyright 2020 gRPC authors.
+ *
+ */
+
+package pemfile
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+)
+
+import (
+	"google.golang.org/protobuf/encoding/protojson"
+
+	"google.golang.org/protobuf/types/known/durationpb"
+)
+
+import (
+	"dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider"
+)
+
+const (
+	pluginName             = "file_watcher"
+	defaultRefreshInterval = 10 * time.Minute
+)
+
+type PluginBuilder struct{}
+
+func (p *PluginBuilder) ParseConfig(c interface{}) (*certprovider.BuildableConfig, error) {
+	data, ok := c.(json.RawMessage)
+	if !ok {
+		return nil, fmt.Errorf("meshca: unsupported config type: %T", c)
+	}
+	opts, err := pluginConfigFromJSON(data)
+	if err != nil {
+		return nil, err
+	}
+	return certprovider.NewBuildableConfig(pluginName, opts.canonical(), func(certprovider.BuildOptions) certprovider.Provider {
+		return newProvider(opts)
+	}), nil
+}
+
+func (p *PluginBuilder) Name() string {
+	return pluginName
+}
+
+func pluginConfigFromJSON(jd json.RawMessage) (Options, error) {
+	// The only difference between this anonymous struct and the Options struct
+	// is that the refresh_interval is represented here as a duration proto,
+	// while in the latter a time.Duration is used.
+	cfg := &struct {
+		CertificateFile   string          `json:"certificate_file,omitempty"`
+		PrivateKeyFile    string          `json:"private_key_file,omitempty"`
+		CACertificateFile string          `json:"ca_certificate_file,omitempty"`
+		RefreshInterval   json.RawMessage `json:"refresh_interval,omitempty"`
+	}{}
+	if err := json.Unmarshal(jd, cfg); err != nil {
+		return Options{}, fmt.Errorf("pemfile: json.Unmarshal(%s) failed: %v", string(jd), err)
+	}
+
+	opts := Options{
+		CertFile: cfg.CertificateFile,
+		KeyFile:  cfg.PrivateKeyFile,
+		RootFile: cfg.CACertificateFile,
+		// Refresh interval is the only field in the configuration for which we
+		// support a default value. We cannot possibly have valid defaults for
+		// file paths to watch. Also, it is valid to specify an empty path for
+		// some of those fields if the user does not want to watch them.
+		RefreshDuration: defaultRefreshInterval,
+	}
+	if cfg.RefreshInterval != nil {
+		dur := &durationpb.Duration{}
+		if err := protojson.Unmarshal(cfg.RefreshInterval, dur); err != nil {
+			return Options{}, fmt.Errorf("pemfile: protojson.Unmarshal(%+v) failed: %v", cfg.RefreshInterval, err)
+		}
+		opts.RefreshDuration = dur.AsDuration()
+	}
+	fmt.Println("pluginConfigFromJSON")
+	fmt.Println(opts)
+
+	if err := opts.validate(); err != nil {
+		return Options{}, err
+	}
+	return opts, nil
+}
diff --git a/xds/credentials/certprovider/pemfile/watcher.go b/xds/credentials/certprovider/pemfile/watcher.go
new file mode 100644
index 000000000..0b1540a8e
--- /dev/null
+++ b/xds/credentials/certprovider/pemfile/watcher.go
@@ -0,0 +1,265 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/*
+ *
+ * Copyright 2020 gRPC authors.
+ *
+ */
+
+// Package pemfile provides a file watching certificate provider plugin
+// implementation which works for files with PEM contents.
+package pemfile
+
+import (
+	"bytes"
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"path/filepath"
+	"time"
+)
+
+import (
+	"github.com/dubbogo/grpc-go/grpclog"
+)
+
+import (
+	"dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider"
+)
+
+const defaultCertRefreshDuration = 1 * time.Hour
+
+var (
+	// For overriding from unit tests.
+	newDistributor = func() distributor { return certprovider.NewDistributor() }
+
+	logger = grpclog.Component("pemfile")
+)
+
+// Options configures a certificate provider plugin that watches a specified set
+// of files that contain certificates and keys in PEM format.
+type Options struct {
+	// CertFile is the file that holds the identity certificate.
+	// Optional. If this is set, KeyFile must also be set.
+	CertFile string
+	// KeyFile is the file that holds identity private key.
+	// Optional. If this is set, CertFile must also be set.
+	KeyFile string
+	// RootFile is the file that holds trusted root certificate(s).
+	// Optional.
+	RootFile string
+	// RefreshDuration is the amount of time the plugin waits before checking
+	// for updates in the specified files.
+	// Optional. If not set, a default value (1 hour) will be used.
+	RefreshDuration time.Duration
+}
+
+func (o Options) canonical() []byte {
+	return []byte(fmt.Sprintf("%s:%s:%s:%s", o.CertFile, o.KeyFile, o.RootFile, o.RefreshDuration))
+}
+
+func (o Options) validate() error {
+	if o.CertFile == "" && o.KeyFile == "" && o.RootFile == "" {
+		return fmt.Errorf("pemfile: at least one credential file needs to be specified")
+	}
+	if keySpecified, certSpecified := o.KeyFile != "", o.CertFile != ""; keySpecified != certSpecified {
+		return fmt.Errorf("pemfile: private key file and identity cert file should be both specified or not specified")
+	}
+	// C-core has a limitation that they cannot verify that a certificate file
+	// matches a key file. So, the only way to get around this is to make sure
+	// that both files are in the same directory and that they do an atomic
+	// read. Even though Java/Go do not have this limitation, we want the
+	// overall plugin behavior to be consistent across languages.
+	if certDir, keyDir := filepath.Dir(o.CertFile), filepath.Dir(o.KeyFile); certDir != keyDir {
+		return errors.New("pemfile: certificate and key file must be in the same directory")
+	}
+	return nil
+}
+
+// NewProvider returns a new certificate provider plugin that is configured to
+// watch the PEM files specified in the passed in options.
+func NewProvider(o Options) (certprovider.Provider, error) {
+	if err := o.validate(); err != nil {
+		return nil, err
+	}
+	return newProvider(o), nil
+}
+
+// newProvider is used to create a new certificate provider plugin after
+// validating the options, and hence does not return an error.
+func newProvider(o Options) certprovider.Provider {
+	if o.RefreshDuration == 0 {
+		o.RefreshDuration = defaultCertRefreshDuration
+	}
+
+	provider := &watcher{opts: o}
+	if o.CertFile != "" && o.KeyFile != "" {
+		provider.identityDistributor = newDistributor()
+	}
+	if o.RootFile != "" {
+		provider.rootDistributor = newDistributor()
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	provider.cancel = cancel
+	go provider.run(ctx)
+	return provider
+}
+
+// watcher is a certificate provider plugin that implements the
+// certprovider.Provider interface. It watches a set of certificate and key
+// files and provides the most up-to-date key material for consumption by
+// credentials implementation.
+type watcher struct {
+	identityDistributor distributor
+	rootDistributor     distributor
+	opts                Options
+	certFileContents    []byte
+	keyFileContents     []byte
+	rootFileContents    []byte
+	cancel              context.CancelFunc
+}
+
+// distributor wraps the methods on certprovider.Distributor which are used by
+// the plugin. This is very useful in tests which need to know exactly when the
+// plugin updates its key material.
+type distributor interface {
+	KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error)
+	Set(km *certprovider.KeyMaterial, err error)
+	Stop()
+}
+
+// updateIdentityDistributor checks if the cert/key files that the plugin is
+// watching have changed, and if so, reads the new contents and updates the
+// identityDistributor with the new key material.
+//
+// Skips updates when file reading or parsing fails.
+// TODO(easwars): Retry with limit (on the number of retries or the amount of
+// time) upon failures.
+func (w *watcher) updateIdentityDistributor() {
+	if w.identityDistributor == nil {
+		return
+	}
+
+	certFileContents, err := ioutil.ReadFile(w.opts.CertFile)
+	if err != nil {
+		logger.Warningf("certFile (%s) read failed: %v", w.opts.CertFile, err)
+		return
+	}
+	keyFileContents, err := ioutil.ReadFile(w.opts.KeyFile)
+	if err != nil {
+		logger.Warningf("keyFile (%s) read failed: %v", w.opts.KeyFile, err)
+		return
+	}
+	// If the file contents have not changed, skip updating the distributor.
+	if bytes.Equal(w.certFileContents, certFileContents) && bytes.Equal(w.keyFileContents, keyFileContents) {
+		return
+	}
+
+	cert, err := tls.X509KeyPair(certFileContents, keyFileContents)
+	if err != nil {
+		logger.Warningf("tls.X509KeyPair(%q, %q) failed: %v", certFileContents, keyFileContents, err)
+		return
+	}
+	w.certFileContents = certFileContents
+	w.keyFileContents = keyFileContents
+	w.identityDistributor.Set(&certprovider.KeyMaterial{Certs: []tls.Certificate{cert}}, nil)
+}
+
+// updateRootDistributor checks if the root cert file that the plugin is
+// watching hs changed, and if so, updates the rootDistributor with the new key
+// material.
+//
+// Skips updates when root cert reading or parsing fails.
+// TODO(easwars): Retry with limit (on the number of retries or the amount of
+// time) upon failures.
+func (w *watcher) updateRootDistributor() {
+	if w.rootDistributor == nil {
+		return
+	}
+
+	rootFileContents, err := ioutil.ReadFile(w.opts.RootFile)
+	if err != nil {
+		logger.Warningf("rootFile (%s) read failed: %v", w.opts.RootFile, err)
+		return
+	}
+	trustPool := x509.NewCertPool()
+	if !trustPool.AppendCertsFromPEM(rootFileContents) {
+		logger.Warning("failed to parse root certificate")
+		return
+	}
+	// If the file contents have not changed, skip updating the distributor.
+	if bytes.Equal(w.rootFileContents, rootFileContents) {
+		return
+	}
+
+	w.rootFileContents = rootFileContents
+	w.rootDistributor.Set(&certprovider.KeyMaterial{Roots: trustPool}, nil)
+}
+
+// run is a long running goroutine which watches the configured files for
+// changes, and pushes new key material into the appropriate distributors which
+// is returned from calls to KeyMaterial().
+func (w *watcher) run(ctx context.Context) {
+	ticker := time.NewTicker(w.opts.RefreshDuration)
+	for {
+		w.updateIdentityDistributor()
+		w.updateRootDistributor()
+		select {
+		case <-ctx.Done():
+			ticker.Stop()
+			if w.identityDistributor != nil {
+				w.identityDistributor.Stop()
+			}
+			if w.rootDistributor != nil {
+				w.rootDistributor.Stop()
+			}
+			return
+		case <-ticker.C:
+		}
+	}
+}
+
+// KeyMaterial returns the key material sourced by the watcher.
+// Callers are expected to use the returned value as read-only.
+func (w *watcher) KeyMaterial(ctx context.Context) (*certprovider.KeyMaterial, error) {
+	km := &certprovider.KeyMaterial{}
+	if w.identityDistributor != nil {
+		identityKM, err := w.identityDistributor.KeyMaterial(ctx)
+		if err != nil {
+			return nil, err
+		}
+		km.Certs = identityKM.Certs
+	}
+	if w.rootDistributor != nil {
+		rootKM, err := w.rootDistributor.KeyMaterial(ctx)
+		if err != nil {
+			return nil, err
+		}
+		km.Roots = rootKM.Roots
+	}
+	return km, nil
+}
+
+// Close cleans up resources allocated by the watcher.
+func (w *watcher) Close() {
+	w.cancel()
+}
diff --git a/xds/credentials/certprovider/provider.go b/xds/credentials/certprovider/provider.go
new file mode 100644
index 000000000..4f6f80513
--- /dev/null
+++ b/xds/credentials/certprovider/provider.go
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/*
+ *
+ * Copyright 2020 gRPC authors.
+ *
+ */
+
+// Package certprovider defines APIs for Certificate Providers in gRPC.
+
+package certprovider
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"errors"
+)
+
+var (
+	// errProviderClosed is returned by Distributor.KeyMaterial when it is
+	// closed.
+	errProviderClosed = errors.New("provider instance is closed")
+
+	// m is a map from name to Provider builder.
+	m = make(map[string]Builder)
+)
+
+// Register registers the Provider builder, whose name as returned by its Name()
+// method will be used as the name registered with this builder. Registered
+// Builders are used by the Store to create Providers.
+func Register(b Builder) {
+	m[b.Name()] = b
+}
+
+// GetBuilder returns the Provider builder registered with the given name.
+// If no builder is registered with the provided name, nil will be returned.
+func GetBuilder(name string) Builder {
+	if b, ok := m[name]; ok {
+		return b
+	}
+	return nil
+}
+
+// Builder creates a Provider.
+type Builder interface {
+	// ParseConfig parses the given config, which is in a format specific to individual
+	// implementations, and returns a BuildableConfig on success.
+	ParseConfig(interface{}) (*BuildableConfig, error)
+
+	// Name returns the name of providers built by this builder.
+	Name() string
+}
+
+// Provider makes it possible to keep channel credential implementations up to
+// date with secrets that they rely on to secure communications on the
+// underlying channel.
+//
+// Provider implementations are free to rely on local or remote sources to fetch
+// the latest secrets, and free to share any state between different
+// instantiations as they deem fit.
+type Provider interface {
+	// KeyMaterial returns the key material sourced by the Provider.
+	// Callers are expected to use the returned value as read-only.
+	KeyMaterial(ctx context.Context) (*KeyMaterial, error)
+
+	// Close cleans up resources allocated by the Provider.
+	Close()
+}
+
+// KeyMaterial wraps the certificates and keys returned by a Provider instance.
+type KeyMaterial struct {
+	// Certs contains a slice of cert/key pairs used to prove local identity.
+	Certs []tls.Certificate
+	// Roots contains the set of trusted roots to validate the peer's identity.
+	Roots *x509.CertPool
+}
+
+// BuildOptions contains parameters passed to a Provider at build time.
+type BuildOptions struct {
+	// CertName holds the certificate name, whose key material is of interest to
+	// the caller.
+	CertName string
+	// WantRoot indicates if the caller is interested in the root certificate.
+	WantRoot bool
+	// WantIdentity indicates if the caller is interested in the identity
+	// certificate.
+	WantIdentity bool
+}
diff --git a/xds/credentials/certprovider/remote/istioca_client.go b/xds/credentials/certprovider/remote/istioca_client.go
new file mode 100644
index 000000000..8bd18fb8b
--- /dev/null
+++ b/xds/credentials/certprovider/remote/istioca_client.go
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+// Copyright Istio Authors
+
+package remote
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"errors"
+	"fmt"
+	"log"
+	"path/filepath"
+	"strings"
+	"time"
+)
+
+import (
+	structpb "github.com/golang/protobuf/ptypes/struct"
+
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials"
+	"google.golang.org/grpc/metadata"
+)
+
+import (
+	v1alpha1 "dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider/remote/v1alpha1"
+)
+
+const (
+	// CertSigner info
+	CertSigner = "CertSigner"
+)
+
+type Options struct {
+	CAEndpoint    string
+	CAEndpointSAN string
+
+	TokenProvider credentials.PerRPCCredentials
+	GRPCOptions   []grpc.DialOption
+
+	CertSigner string
+	ClusterID  string
+
+	TrustedRoots *x509.CertPool
+
+	// ProvCert contains a long-lived 'provider' certificate that will be
+	// exchanged with the workload certificate.
+	// It is a cert signed by same CA (or a CA trusted by Istiod).
+	// It is still exchanged because Istiod may add info to the cert.
+	ProvCert string
+}
+
+type CitadelClient struct {
+	enableTLS bool
+	client    v1alpha1.IstioCertificateServiceClient
+	conn      *grpc.ClientConn
+	opts      *Options
+}
+
+// NewCitadelClient create a CA client for Citadel.
+func NewCitadelClient(opts *Options) (*CitadelClient, error) {
+	c := &CitadelClient{
+		enableTLS: true,
+		opts:      opts,
+	}
+
+	conn, err := c.buildConnection()
+
+	if err != nil {
+		log.Printf("Failed to connect to endpoint %s: %v", opts.CAEndpoint, err)
+		return nil, fmt.Errorf("failed to connect to endpoint %s", opts.CAEndpoint)
+	}
+	c.conn = conn
+	c.client = v1alpha1.NewIstioCertificateServiceClient(conn)
+	return c, nil
+}
+
+func (c *CitadelClient) Close() {
+	if c.conn != nil {
+		c.conn.Close()
+	}
+}
+
+// CSR Sign calls Citadel to sign a CSR.
+func (c *CitadelClient) CSRSign(csrPEM []byte, certValidTTLInSec int64) ([]string, error) {
+	crMetaStruct := &structpb.Struct{
+		Fields: map[string]*structpb.Value{
+			CertSigner: {
+				Kind: &structpb.Value_StringValue{StringValue: c.opts.CertSigner},
+			},
+		},
+	}
+	req := &v1alpha1.IstioCertificateRequest{
+		Csr:              string(csrPEM),
+		ValidityDuration: certValidTTLInSec,
+		Metadata:         crMetaStruct,
+	}
+	ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("ClusterID", c.opts.ClusterID))
+	resp, err := c.client.CreateCertificate(ctx, req)
+	if err != nil {
+		return nil, fmt.Errorf("create certificate: %v", err)
+	}
+
+	if len(resp.CertChain) <= 1 {
+		return nil, errors.New("invalid empty CertChain")
+	}
+
+	return resp.CertChain, nil
+}
+
+func (c *CitadelClient) getTLSDialOption() (grpc.DialOption, error) {
+	// Load the TLS root certificate from the specified file.
+	// Create a certificate pool
+	var certPool *x509.CertPool
+	var err error
+	if c.opts.TrustedRoots == nil {
+		// No explicit certificate - assume the citadel-compatible server uses a public cert
+		certPool, err = x509.SystemCertPool()
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		certPool = c.opts.TrustedRoots
+	}
+	var certificate tls.Certificate
+	config := tls.Config{
+		GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
+			if c.opts.ProvCert != "" {
+				// Load the certificate from disk
+				certificate, err = tls.LoadX509KeyPair(
+					filepath.Join(c.opts.ProvCert, "cert-chain.pem"),
+					filepath.Join(c.opts.ProvCert, "key.pem"))
+
+				if err != nil {
+					// we will return an empty cert so that when user sets the Prov cert path
+					// but not have such cert in the file path we use the token to provide verification
+					// instead of just broken the workflow
+					log.Printf("cannot load key pair, using token instead: %v", err)
+					return &certificate, nil
+				}
+				if certificate.Leaf.NotAfter.Before(time.Now()) {
+					log.Printf("cannot parse the cert chain, using token instead: %v", err)
+					return &tls.Certificate{}, nil
+				}
+			}
+			return &certificate, nil
+		},
+	}
+	config.RootCAs = certPool
+
+	// For debugging on localhost (with port forward)
+	// TODO: remove once istiod is stable and we have a way to validate JWTs locally
+	if strings.Contains(c.opts.CAEndpoint, "localhost") {
+		config.ServerName = "istiod.istio-system.svc"
+	}
+	if c.opts.CAEndpointSAN != "" {
+		config.ServerName = c.opts.CAEndpointSAN
+	}
+
+	transportCreds := credentials.NewTLS(&config)
+	return grpc.WithTransportCredentials(transportCreds), nil
+}
+
+func (c *CitadelClient) buildConnection() (*grpc.ClientConn, error) {
+	var ol []grpc.DialOption
+	var opts grpc.DialOption
+	var err error
+	if c.enableTLS {
+		opts, err = c.getTLSDialOption()
+		if err != nil {
+			return nil, err
+		}
+		ol = append(ol, opts)
+	} else {
+		opts = grpc.WithInsecure()
+		ol = append(ol, opts)
+	}
+	ol = append(ol, grpc.WithPerRPCCredentials(c.opts.TokenProvider))
+	ol = append(ol, c.opts.GRPCOptions...)
+
+	conn, err := grpc.Dial(c.opts.CAEndpoint, ol...)
+	if err != nil {
+		log.Printf("Failed to connect to endpoint %s: %v", c.opts.CAEndpoint, err)
+		return nil, fmt.Errorf("failed to connect to endpoint %s", c.opts.CAEndpoint)
+	}
+
+	return conn, nil
+}
+
+// GetRootCertBundle: Citadel (Istiod) CA doesn't publish any endpoint to retrieve CA certs
+func (c *CitadelClient) GetRootCertBundle() ([]string, error) {
+	return []string{}, nil
+}
diff --git a/xds/credentials/certprovider/remote/v1alpha1/ca.pb.go b/xds/credentials/certprovider/remote/v1alpha1/ca.pb.go
new file mode 100644
index 000000000..54ce78e97
--- /dev/null
+++ b/xds/credentials/certprovider/remote/v1alpha1/ca.pb.go
@@ -0,0 +1,287 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+// Copyright Istio Authors
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.27.1
+// 	protoc        (unknown)
+// source: security/v1alpha1/ca.proto
+
+// Keep this package for backward compatibility.
+
+package v1alpha1
+
+import (
+	reflect "reflect"
+	sync "sync"
+)
+
+import (
+	_struct "github.com/golang/protobuf/ptypes/struct"
+
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// Certificate request message. The authentication should be based on:
+// 1. Bearer tokens carried in the side channel;
+// 2. Client-side certificate via Mutual TLS handshake.
+// Note: the service implementation is REQUIRED to verify the authenticated caller is authorize to
+// all SANs in the CSR. The server side may overwrite any requested certificate field based on its
+// policies.
+type IstioCertificateRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// PEM-encoded certificate request.
+	// The public key in the CSR is used to generate the certificate,
+	// and other fields in the generated certificate may be overwritten by the CA.
+	Csr string `protobuf:"bytes,1,opt,name=csr,proto3" json:"csr,omitempty"`
+	// Optional: requested certificate validity period, in seconds.
+	ValidityDuration int64 `protobuf:"varint,3,opt,name=validity_duration,json=validityDuration,proto3" json:"validity_duration,omitempty"`
+	// $hide_from_docs
+	// Optional: Opaque metadata provided by the XDS node to Istio.
+	// Supported metadata: WorkloadName, WorkloadIP, ClusterID
+	Metadata *_struct.Struct `protobuf:"bytes,4,opt,name=metadata,proto3" json:"metadata,omitempty"`
+}
+
+func (x *IstioCertificateRequest) Reset() {
+	*x = IstioCertificateRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_security_v1alpha1_ca_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *IstioCertificateRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*IstioCertificateRequest) ProtoMessage() {}
+
+func (x *IstioCertificateRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_security_v1alpha1_ca_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use IstioCertificateRequest.ProtoReflect.Descriptor instead.
+func (*IstioCertificateRequest) Descriptor() ([]byte, []int) {
+	return file_security_v1alpha1_ca_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *IstioCertificateRequest) GetCsr() string {
+	if x != nil {
+		return x.Csr
+	}
+	return ""
+}
+
+func (x *IstioCertificateRequest) GetValidityDuration() int64 {
+	if x != nil {
+		return x.ValidityDuration
+	}
+	return 0
+}
+
+func (x *IstioCertificateRequest) GetMetadata() *_struct.Struct {
+	if x != nil {
+		return x.Metadata
+	}
+	return nil
+}
+
+// Certificate response message.
+type IstioCertificateResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// PEM-encoded certificate chain.
+	// The leaf cert is the first element, and the root cert is the last element.
+	CertChain []string `protobuf:"bytes,1,rep,name=cert_chain,json=certChain,proto3" json:"cert_chain,omitempty"`
+}
+
+func (x *IstioCertificateResponse) Reset() {
+	*x = IstioCertificateResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_security_v1alpha1_ca_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *IstioCertificateResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*IstioCertificateResponse) ProtoMessage() {}
+
+func (x *IstioCertificateResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_security_v1alpha1_ca_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use IstioCertificateResponse.ProtoReflect.Descriptor instead.
+func (*IstioCertificateResponse) Descriptor() ([]byte, []int) {
+	return file_security_v1alpha1_ca_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *IstioCertificateResponse) GetCertChain() []string {
+	if x != nil {
+		return x.CertChain
+	}
+	return nil
+}
+
+var File_security_v1alpha1_ca_proto protoreflect.FileDescriptor
+
+var file_security_v1alpha1_ca_proto_rawDesc = []byte{
+	0x0a, 0x1a, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70,
+	0x68, 0x61, 0x31, 0x2f, 0x63, 0x61, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x69, 0x73,
+	0x74, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x1a, 0x1c, 0x67, 0x6f, 0x6f,
+	0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72,
+	0x75, 0x63, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x8d, 0x01, 0x0a, 0x17, 0x49, 0x73,
+	0x74, 0x69, 0x6f, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x73, 0x72, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x03, 0x63, 0x73, 0x72, 0x12, 0x2b, 0x0a, 0x11, 0x76, 0x61, 0x6c, 0x69, 0x64,
+	0x69, 0x74, 0x79, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x03, 0x52, 0x10, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x69, 0x74, 0x79, 0x44, 0x75, 0x72, 0x61,
+	0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
+	0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52,
+	0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x39, 0x0a, 0x18, 0x49, 0x73, 0x74,
+	0x69, 0x6f, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73,
+	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x65, 0x72, 0x74, 0x5f, 0x63, 0x68,
+	0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x63, 0x65, 0x72, 0x74, 0x43,
+	0x68, 0x61, 0x69, 0x6e, 0x32, 0x81, 0x01, 0x0a, 0x17, 0x49, 0x73, 0x74, 0x69, 0x6f, 0x43, 0x65,
+	0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
+	0x12, 0x66, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66,
+	0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x26, 0x2e, 0x69, 0x73, 0x74, 0x69, 0x6f, 0x2e, 0x76, 0x31,
+	0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x49, 0x73, 0x74, 0x69, 0x6f, 0x43, 0x65, 0x72, 0x74, 0x69,
+	0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e,
+	0x69, 0x73, 0x74, 0x69, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x49, 0x73,
+	0x74, 0x69, 0x6f, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x20, 0x5a, 0x1e, 0x69, 0x73, 0x74, 0x69,
+	0x6f, 0x2e, 0x69, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74,
+	0x79, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x33,
+}
+
+var (
+	file_security_v1alpha1_ca_proto_rawDescOnce sync.Once
+	file_security_v1alpha1_ca_proto_rawDescData = file_security_v1alpha1_ca_proto_rawDesc
+)
+
+func file_security_v1alpha1_ca_proto_rawDescGZIP() []byte {
+	file_security_v1alpha1_ca_proto_rawDescOnce.Do(func() {
+		file_security_v1alpha1_ca_proto_rawDescData = protoimpl.X.CompressGZIP(file_security_v1alpha1_ca_proto_rawDescData)
+	})
+	return file_security_v1alpha1_ca_proto_rawDescData
+}
+
+var file_security_v1alpha1_ca_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_security_v1alpha1_ca_proto_goTypes = []interface{}{
+	(*IstioCertificateRequest)(nil),  // 0: istio.v1.auth.IstioCertificateRequest
+	(*IstioCertificateResponse)(nil), // 1: istio.v1.auth.IstioCertificateResponse
+	(*_struct.Struct)(nil),           // 2: google.protobuf.Struct
+}
+var file_security_v1alpha1_ca_proto_depIdxs = []int32{
+	2, // 0: istio.v1.auth.IstioCertificateRequest.metadata:type_name -> google.protobuf.Struct
+	0, // 1: istio.v1.auth.IstioCertificateService.CreateCertificate:input_type -> istio.v1.auth.IstioCertificateRequest
+	1, // 2: istio.v1.auth.IstioCertificateService.CreateCertificate:output_type -> istio.v1.auth.IstioCertificateResponse
+	2, // [2:3] is the sub-list for method output_type
+	1, // [1:2] is the sub-list for method input_type
+	1, // [1:1] is the sub-list for extension type_name
+	1, // [1:1] is the sub-list for extension extendee
+	0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_security_v1alpha1_ca_proto_init() }
+func file_security_v1alpha1_ca_proto_init() {
+	if File_security_v1alpha1_ca_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_security_v1alpha1_ca_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*IstioCertificateRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_security_v1alpha1_ca_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*IstioCertificateResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_security_v1alpha1_ca_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_security_v1alpha1_ca_proto_goTypes,
+		DependencyIndexes: file_security_v1alpha1_ca_proto_depIdxs,
+		MessageInfos:      file_security_v1alpha1_ca_proto_msgTypes,
+	}.Build()
+	File_security_v1alpha1_ca_proto = out.File
+	file_security_v1alpha1_ca_proto_rawDesc = nil
+	file_security_v1alpha1_ca_proto_goTypes = nil
+	file_security_v1alpha1_ca_proto_depIdxs = nil
+}
diff --git a/xds/credentials/certprovider/remote/v1alpha1/ca_grpc.pb.go b/xds/credentials/certprovider/remote/v1alpha1/ca_grpc.pb.go
new file mode 100644
index 000000000..22a6334b5
--- /dev/null
+++ b/xds/credentials/certprovider/remote/v1alpha1/ca_grpc.pb.go
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.2.0
+// - protoc             (unknown)
+// source: security/v1alpha1/ca.proto
+
+package v1alpha1
+
+import (
+	context "context"
+)
+
+import (
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+// IstioCertificateServiceClient is the client API for IstioCertificateService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type IstioCertificateServiceClient interface {
+	// Using provided CSR, returns a signed certificate.
+	CreateCertificate(ctx context.Context, in *IstioCertificateRequest, opts ...grpc.CallOption) (*IstioCertificateResponse, error)
+}
+
+type istioCertificateServiceClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewIstioCertificateServiceClient(cc grpc.ClientConnInterface) IstioCertificateServiceClient {
+	return &istioCertificateServiceClient{cc}
+}
+
+func (c *istioCertificateServiceClient) CreateCertificate(ctx context.Context, in *IstioCertificateRequest, opts ...grpc.CallOption) (*IstioCertificateResponse, error) {
+	out := new(IstioCertificateResponse)
+	err := c.cc.Invoke(ctx, "/istio.v1.auth.IstioCertificateService/CreateCertificate", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// IstioCertificateServiceServer is the server API for IstioCertificateService service.
+// All implementations must embed UnimplementedIstioCertificateServiceServer
+// for forward compatibility
+type IstioCertificateServiceServer interface {
+	// Using provided CSR, returns a signed certificate.
+	CreateCertificate(context.Context, *IstioCertificateRequest) (*IstioCertificateResponse, error)
+	mustEmbedUnimplementedIstioCertificateServiceServer()
+}
+
+// UnimplementedIstioCertificateServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedIstioCertificateServiceServer struct {
+}
+
+func (UnimplementedIstioCertificateServiceServer) CreateCertificate(context.Context, *IstioCertificateRequest) (*IstioCertificateResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method CreateCertificate not implemented")
+}
+func (UnimplementedIstioCertificateServiceServer) mustEmbedUnimplementedIstioCertificateServiceServer() {
+}
+
+// UnsafeIstioCertificateServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to IstioCertificateServiceServer will
+// result in compilation errors.
+type UnsafeIstioCertificateServiceServer interface {
+	mustEmbedUnimplementedIstioCertificateServiceServer()
+}
+
+func RegisterIstioCertificateServiceServer(s grpc.ServiceRegistrar, srv IstioCertificateServiceServer) {
+	s.RegisterService(&IstioCertificateService_ServiceDesc, srv)
+}
+
+func _IstioCertificateService_CreateCertificate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(IstioCertificateRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(IstioCertificateServiceServer).CreateCertificate(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/istio.v1.auth.IstioCertificateService/CreateCertificate",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(IstioCertificateServiceServer).CreateCertificate(ctx, req.(*IstioCertificateRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// IstioCertificateService_ServiceDesc is the grpc.ServiceDesc for IstioCertificateService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var IstioCertificateService_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "istio.v1.auth.IstioCertificateService",
+	HandlerType: (*IstioCertificateServiceServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "CreateCertificate",
+			Handler:    _IstioCertificateService_CreateCertificate_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "security/v1alpha1/ca.proto",
+}
diff --git a/xds/credentials/certprovider/store.go b/xds/credentials/certprovider/store.go
new file mode 100644
index 000000000..c5709cbf9
--- /dev/null
+++ b/xds/credentials/certprovider/store.go
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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.
+ */
+
+/*
+ *
+ * Copyright 2020 gRPC authors.
+ *
+ */
+
+package certprovider
+
+import (
+	"fmt"
+	"sync"
+)
+
+// provStore is the global singleton certificate provider store.
+var provStore = &store{
+	providers: make(map[storeKey]*wrappedProvider),
+}
+
+// storeKey acts as the key to the map of providers maintained by the store. A
+// combination of provider name and configuration is used to uniquely identify
+// every provider instance in the store. Go maps need to be indexed by
+// comparable types, so the provider configuration is converted from
+// `interface{}` to string using the ParseConfig method while creating this key.
+type storeKey struct {
+	// name of the certificate provider.
+	name string
+	// configuration of the certificate provider in string form.
+	config string
+	// opts contains the certificate name and other keyMaterial options.
+	opts BuildOptions
+}
+
+// wrappedProvider wraps a provider instance with a reference count.
+type wrappedProvider struct {
+	Provider
+	refCount int
+
+	// A reference to the key and store are also kept here to override the
+	// Close method on the provider.
+	storeKey storeKey
+	store    *store
+}
+
+// store is a collection of provider instances, safe for concurrent access.
+type store struct {
+	mu        sync.Mutex
+	providers map[storeKey]*wrappedProvider
+}
+
+// Close overrides the Close method of the embedded provider. It releases the
+// reference held by the caller on the underlying provider and if the
+// provider's reference count reaches zero, it is removed from the store, and
+// its Close method is also invoked.
+func (wp *wrappedProvider) Close() {
+	ps := wp.store
+	ps.mu.Lock()
+	defer ps.mu.Unlock()
+
+	wp.refCount--
+	if wp.refCount == 0 {
+		wp.Provider.Close()
+		delete(ps.providers, wp.storeKey)
+	}
+}
+
+// BuildableConfig wraps parsed provider configuration and functionality to
+// instantiate provider instances.
+type BuildableConfig struct {
+	name    string
+	config  []byte
+	starter func(BuildOptions) Provider
+	pStore  *store
+}
+
+// NewBuildableConfig creates a new BuildableConfig with the given arguments.
+// Provider implementations are expected to invoke this function after parsing
+// the given configuration as part of their ParseConfig() method.
+// Equivalent configurations are expected to invoke this function with the same
+// config argument.
+func NewBuildableConfig(name string, config []byte, starter func(BuildOptions) Provider) *BuildableConfig {
+	return &BuildableConfig{
+		name:    name,
+		config:  config,
+		starter: starter,
+		pStore:  provStore,
+	}
+}
+
+// Build kicks off a provider instance with the wrapped configuration. Multiple
+// invocations of this method with the same opts will result in provider
+// instances being reused.
+func (bc *BuildableConfig) Build(opts BuildOptions) (Provider, error) {
+	provStore.mu.Lock()
+	defer provStore.mu.Unlock()
+
+	sk := storeKey{
+		name:   bc.name,
+		config: string(bc.config),
+		opts:   opts,
+	}
+	if wp, ok := provStore.providers[sk]; ok {
+		wp.refCount++
+		return wp, nil
+	}
+
+	provider := bc.starter(opts)
+	if provider == nil {
+		return nil, fmt.Errorf("provider(%q, %q).Build(%v) failed", sk.name, sk.config, opts)
+	}
+	wp := &wrappedProvider{
+		Provider: provider,
+		refCount: 1,
+		storeKey: sk,
+		store:    provStore,
+	}
+	provStore.providers[sk] = wp
+	return wp, nil
+}
+
+// String returns the provider name and config as a colon separated string.
+func (bc *BuildableConfig) String() string {
+	return fmt.Sprintf("%s:%s", bc.name, string(bc.config))
+}
+
+// ParseConfig is a convenience function to create a BuildableConfig given a
+// provider name and configuration. Returns an error if there is no registered
+// builder for the given name or if the config parsing fails.
+func ParseConfig(name string, config interface{}) (*BuildableConfig, error) {
+	parser := GetBuilder(name)
+	if parser == nil {
+		return nil, fmt.Errorf("no certificate provider builder found for %q", name)
+	}
+	return parser.ParseConfig(config)
+}
+
+// GetProvider is a convenience function to create a provider given the name,
+// config and build options.
+func GetProvider(name string, config interface{}, opts BuildOptions) (Provider, error) {
+	bc, err := ParseConfig(name, config)
+	if err != nil {
+		return nil, err
+	}
+	return bc.Build(opts)
+}
diff --git a/xds/credentials/env.go b/xds/credentials/env.go
new file mode 100644
index 000000000..f7c86b597
--- /dev/null
+++ b/xds/credentials/env.go
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 credentials
+
+import (
+	"os"
+)
+
+const (
+	DefaultKubernetesServiceAccountPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
+	DefaultIsitoCaServiceAccountPath    = "/var/run/secrets/tokens/istio-token"
+	DefaultRootCertPath                 = "/var/run/secrets/istio/root-cert.pem"
+	DefaultIstioCAEndpoint              = "istiod.istio-system.svc:15012"
+	DefaultCertSigner                   = "kubernetes.default.svc"
+	DefaultClusterID                    = "Kubernetes"
+)
+
+var (
+	PodNamespace       = f(os.Getenv("POD_NAMESPACE"), "default")
+	CertTTL            = f(os.Getenv("CERT_TTL"), "31536000")
+	RootCertPath       = f(os.Getenv("ROOT_CERT_PATH"), DefaultRootCertPath)
+	IstioCAEndpoint    = f(os.Getenv("ISTIO_CA_ENDPOINT"), DefaultIstioCAEndpoint)
+	ServiceAccountPath = f(os.Getenv("SERVICE_ACCOUNT_PATH"), DefaultIsitoCaServiceAccountPath)
+	certSigner         = f(os.Getenv("CERT_SIGNER"), DefaultCertSigner)
+	clusterID          = f(os.Getenv("CLUSTER_ID"), DefaultClusterID)
+	URIPrefix          = f(os.Getenv("URI_PREFIX"), "spiffe://")
+	Domain             = f(os.Getenv("DOMAIN"), "cluster.local")
+	ServiceAccountName = f(os.Getenv("SERVICE_ACCOUNT_NAME"), "default")
+)
+
+var f = func(a, b string) string {
+	if "" == a {
+		return b
+	}
+	return a
+}
diff --git a/xds/credentials/token_provider.go b/xds/credentials/token_provider.go
new file mode 100644
index 000000000..0faadf4b1
--- /dev/null
+++ b/xds/credentials/token_provider.go
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 credentials
+
+import (
+	"context"
+	"io/ioutil"
+)
+
+//provide k8s service account
+type saTokenProvider struct {
+	Token     string
+	tokenPath string
+}
+
+// NewSaTokenProvider return a provider
+func NewSaTokenProvider(tokenPath string) (*saTokenProvider, error) {
+	sa, err := ioutil.ReadFile(tokenPath)
+	if err != nil {
+		return nil, err
+	}
+	return &saTokenProvider{
+		tokenPath: tokenPath,
+		Token:     string(sa),
+	}, nil
+}
+
+// GetRequestMetadata return meta of authorization
+func (s *saTokenProvider) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
+
+	meta := make(map[string]string)
+
+	meta["authorization"] = "Bearer " + s.Token
+	return meta, nil
+}
+
+// RequireTransportSecurity always false
+func (s *saTokenProvider) RequireTransportSecurity() bool {
+	return false
+}
diff --git a/xds/server/conn_wrapper.go b/xds/server/conn_wrapper.go
index 421a1bdd2..7f8d42e0a 100644
--- a/xds/server/conn_wrapper.go
+++ b/xds/server/conn_wrapper.go
@@ -31,12 +31,9 @@ import (
 	"time"
 )
 
-import (
-	"google.golang.org/grpc/credentials/tls/certprovider"
-)
-
 import (
 	"dubbo.apache.org/dubbo-go/v3/xds/client/resource"
+	"dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider"
 	xdsinternal "dubbo.apache.org/dubbo-go/v3/xds/utils/credentials/xds"
 )
 
diff --git a/xds/utils/credentials/xds/handshake_info.go b/xds/utils/credentials/xds/handshake_info.go
index a7d62ab5f..a7213a567 100644
--- a/xds/utils/credentials/xds/handshake_info.go
+++ b/xds/utils/credentials/xds/handshake_info.go
@@ -37,12 +37,11 @@ import (
 import (
 	"google.golang.org/grpc/attributes"
 
-	"google.golang.org/grpc/credentials/tls/certprovider"
-
 	"google.golang.org/grpc/resolver"
 )
 
 import (
+	"dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider"
 	"dubbo.apache.org/dubbo-go/v3/xds/utils/matcher"
 )