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  }