dubbo.apache.org/dubbo-go/v3@v3.1.1/xds/credentials/certprovider/remote/istioca_client.go (about) 1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 // Copyright Istio Authors 19 20 package remote 21 22 import ( 23 "context" 24 "crypto/tls" 25 "crypto/x509" 26 "errors" 27 "fmt" 28 "log" 29 "path/filepath" 30 "strings" 31 "time" 32 ) 33 34 import ( 35 structpb "github.com/golang/protobuf/ptypes/struct" 36 37 "google.golang.org/grpc" 38 "google.golang.org/grpc/credentials" 39 "google.golang.org/grpc/metadata" 40 ) 41 42 import ( 43 v1alpha1 "dubbo.apache.org/dubbo-go/v3/xds/credentials/certprovider/remote/v1alpha1" 44 ) 45 46 const ( 47 // CertSigner info 48 CertSigner = "CertSigner" 49 ) 50 51 type Options struct { 52 CAEndpoint string 53 CAEndpointSAN string 54 55 TokenProvider credentials.PerRPCCredentials 56 GRPCOptions []grpc.DialOption 57 58 CertSigner string 59 ClusterID string 60 61 TrustedRoots *x509.CertPool 62 63 // ProvCert contains a long-lived 'provider' certificate that will be 64 // exchanged with the workload certificate. 65 // It is a cert signed by same CA (or a CA trusted by Istiod). 66 // It is still exchanged because Istiod may add info to the cert. 67 ProvCert string 68 } 69 70 type CitadelClient struct { 71 enableTLS bool 72 client v1alpha1.IstioCertificateServiceClient 73 conn *grpc.ClientConn 74 opts *Options 75 } 76 77 // NewCitadelClient create a CA client for Citadel. 78 func NewCitadelClient(opts *Options) (*CitadelClient, error) { 79 c := &CitadelClient{ 80 enableTLS: true, 81 opts: opts, 82 } 83 84 conn, err := c.buildConnection() 85 86 if err != nil { 87 log.Printf("Failed to connect to endpoint %s: %v", opts.CAEndpoint, err) 88 return nil, fmt.Errorf("failed to connect to endpoint %s", opts.CAEndpoint) 89 } 90 c.conn = conn 91 c.client = v1alpha1.NewIstioCertificateServiceClient(conn) 92 return c, nil 93 } 94 95 func (c *CitadelClient) Close() { 96 if c.conn != nil { 97 c.conn.Close() 98 } 99 } 100 101 // CSR Sign calls Citadel to sign a CSR. 102 func (c *CitadelClient) CSRSign(csrPEM []byte, certValidTTLInSec int64) ([]string, error) { 103 crMetaStruct := &structpb.Struct{ 104 Fields: map[string]*structpb.Value{ 105 CertSigner: { 106 Kind: &structpb.Value_StringValue{StringValue: c.opts.CertSigner}, 107 }, 108 }, 109 } 110 req := &v1alpha1.IstioCertificateRequest{ 111 Csr: string(csrPEM), 112 ValidityDuration: certValidTTLInSec, 113 Metadata: crMetaStruct, 114 } 115 ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("ClusterID", c.opts.ClusterID)) 116 resp, err := c.client.CreateCertificate(ctx, req) 117 if err != nil { 118 return nil, fmt.Errorf("create certificate: %v", err) 119 } 120 121 if len(resp.CertChain) <= 1 { 122 return nil, errors.New("invalid empty CertChain") 123 } 124 125 return resp.CertChain, nil 126 } 127 128 func (c *CitadelClient) getTLSDialOption() (grpc.DialOption, error) { 129 // Load the TLS root certificate from the specified file. 130 // Create a certificate pool 131 var certPool *x509.CertPool 132 var err error 133 if c.opts.TrustedRoots == nil { 134 // No explicit certificate - assume the citadel-compatible server uses a public cert 135 certPool, err = x509.SystemCertPool() 136 if err != nil { 137 return nil, err 138 } 139 } else { 140 certPool = c.opts.TrustedRoots 141 } 142 var certificate tls.Certificate 143 config := tls.Config{ 144 GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { 145 if c.opts.ProvCert != "" { 146 // Load the certificate from disk 147 certificate, err = tls.LoadX509KeyPair( 148 filepath.Join(c.opts.ProvCert, "cert-chain.pem"), 149 filepath.Join(c.opts.ProvCert, "key.pem")) 150 151 if err != nil { 152 // we will return an empty cert so that when user sets the Prov cert path 153 // but not have such cert in the file path we use the token to provide verification 154 // instead of just broken the workflow 155 log.Printf("cannot load key pair, using token instead: %v", err) 156 return &certificate, nil 157 } 158 if certificate.Leaf.NotAfter.Before(time.Now()) { 159 log.Printf("cannot parse the cert chain, using token instead: %v", err) 160 return &tls.Certificate{}, nil 161 } 162 } 163 return &certificate, nil 164 }, 165 } 166 config.RootCAs = certPool 167 168 // For debugging on localhost (with port forward) 169 // TODO: remove once istiod is stable and we have a way to validate JWTs locally 170 if strings.Contains(c.opts.CAEndpoint, "localhost") { 171 config.ServerName = "istiod.istio-system.svc" 172 } 173 if c.opts.CAEndpointSAN != "" { 174 config.ServerName = c.opts.CAEndpointSAN 175 } 176 177 transportCreds := credentials.NewTLS(&config) 178 return grpc.WithTransportCredentials(transportCreds), nil 179 } 180 181 func (c *CitadelClient) buildConnection() (*grpc.ClientConn, error) { 182 var ol []grpc.DialOption 183 var opts grpc.DialOption 184 var err error 185 if c.enableTLS { 186 opts, err = c.getTLSDialOption() 187 if err != nil { 188 return nil, err 189 } 190 ol = append(ol, opts) 191 } else { 192 opts = grpc.WithInsecure() 193 ol = append(ol, opts) 194 } 195 ol = append(ol, grpc.WithPerRPCCredentials(c.opts.TokenProvider)) 196 ol = append(ol, c.opts.GRPCOptions...) 197 198 conn, err := grpc.Dial(c.opts.CAEndpoint, ol...) 199 if err != nil { 200 log.Printf("Failed to connect to endpoint %s: %v", c.opts.CAEndpoint, err) 201 return nil, fmt.Errorf("failed to connect to endpoint %s", c.opts.CAEndpoint) 202 } 203 204 return conn, nil 205 } 206 207 // GetRootCertBundle: Citadel (Istiod) CA doesn't publish any endpoint to retrieve CA certs 208 func (c *CitadelClient) GetRootCertBundle() ([]string, error) { 209 return []string{}, nil 210 }