istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/istio/ca.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package istio
    16  
    17  import (
    18  	"context"
    19  	"crypto/tls"
    20  	"crypto/x509"
    21  	"fmt"
    22  	"sync"
    23  	"time"
    24  
    25  	"google.golang.org/grpc"
    26  	"google.golang.org/grpc/credentials"
    27  	"google.golang.org/grpc/metadata"
    28  	authenticationv1 "k8s.io/api/authentication/v1"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/client-go/kubernetes"
    31  
    32  	pb "istio.io/api/security/v1alpha1"
    33  	"istio.io/istio/pkg/config/constants"
    34  	"istio.io/istio/pkg/test/framework"
    35  	pkiutil "istio.io/istio/security/pkg/pki/util"
    36  )
    37  
    38  type Cert struct {
    39  	ClientCert, Key, RootCert []byte
    40  }
    41  
    42  func CreateCertificate(t framework.TestContext, i Instance, serviceAccount, namespace string) (Cert, error) {
    43  	c := t.Clusters().Default()
    44  	rootCert, err := FetchRootCert(c.Kube())
    45  	if err != nil {
    46  		return Cert{}, fmt.Errorf("failed to fetch root cert: %v", err)
    47  	}
    48  
    49  	token, err := GetServiceAccountToken(c.Kube(), "istio-ca", namespace, serviceAccount)
    50  	if err != nil {
    51  		return Cert{}, err
    52  	}
    53  
    54  	san := fmt.Sprintf("spiffe://%s/ns/%s/sa/%s", "cluster.local", namespace, serviceAccount)
    55  	options := pkiutil.CertOptions{
    56  		Host:       san,
    57  		RSAKeySize: 2048,
    58  	}
    59  	// Generate the cert/key, send CSR to CA.
    60  	csrPEM, keyPEM, err := pkiutil.GenCSR(options)
    61  	if err != nil {
    62  		return Cert{}, err
    63  	}
    64  	a, err := i.InternalDiscoveryAddressFor(c)
    65  	if err != nil {
    66  		return Cert{}, err
    67  	}
    68  	client, err := newCitadelClient(a, []byte(rootCert))
    69  	if err != nil {
    70  		return Cert{}, fmt.Errorf("creating citadel client: %v", err)
    71  	}
    72  	req := &pb.IstioCertificateRequest{
    73  		Csr:              string(csrPEM),
    74  		ValidityDuration: int64((time.Hour * 24 * 7).Seconds()),
    75  	}
    76  	rctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("Authorization", "Bearer "+token, "ClusterID", constants.DefaultClusterName))
    77  	resp, err := client.CreateCertificate(rctx, req)
    78  	if err != nil {
    79  		return Cert{}, fmt.Errorf("send CSR: %v", err)
    80  	}
    81  	certChain := []byte{}
    82  	for _, c := range resp.CertChain {
    83  		certChain = append(certChain, []byte(c)...)
    84  	}
    85  	return Cert{certChain, keyPEM, []byte(rootCert)}, nil
    86  }
    87  
    88  // 7 days
    89  var saTokenExpiration int64 = 60 * 60 * 24 * 7
    90  
    91  func GetServiceAccountToken(c kubernetes.Interface, aud, ns, sa string) (string, error) {
    92  	san := san(ns, sa)
    93  
    94  	if got, f := cachedTokens.Load(san); f {
    95  		t := got.(token)
    96  		if t.expiration.After(time.Now().Add(time.Minute)) {
    97  			return t.token, nil
    98  		}
    99  		// Otherwise, its expired, load a new one
   100  	}
   101  	rt, err := c.CoreV1().ServiceAccounts(ns).CreateToken(context.Background(), sa,
   102  		&authenticationv1.TokenRequest{
   103  			Spec: authenticationv1.TokenRequestSpec{
   104  				Audiences:         []string{aud},
   105  				ExpirationSeconds: &saTokenExpiration,
   106  			},
   107  		}, metav1.CreateOptions{})
   108  	if err != nil {
   109  		return "", err
   110  	}
   111  	exp := rt.Status.ExpirationTimestamp.Time
   112  	cachedTokens.Store(san, token{rt.Status.Token, exp})
   113  	return rt.Status.Token, nil
   114  }
   115  
   116  // map of SAN to jwt token. Used to avoid repetitive calls
   117  var cachedTokens sync.Map
   118  
   119  type token struct {
   120  	token      string
   121  	expiration time.Time
   122  }
   123  
   124  // NewCitadelClient create a CA client for Citadel.
   125  func newCitadelClient(endpoint string, rootCert []byte) (pb.IstioCertificateServiceClient, error) {
   126  	certPool := x509.NewCertPool()
   127  	ok := certPool.AppendCertsFromPEM(rootCert)
   128  	if !ok {
   129  		return nil, fmt.Errorf("failed to append certificates")
   130  	}
   131  	config := tls.Config{
   132  		RootCAs:            certPool,
   133  		InsecureSkipVerify: true, // nolint: gosec // test only code
   134  	}
   135  	transportCreds := credentials.NewTLS(&config)
   136  
   137  	conn, err := grpc.Dial(endpoint, grpc.WithTransportCredentials(transportCreds))
   138  	if err != nil {
   139  		return nil, fmt.Errorf("failed to connect to endpoint %s", endpoint)
   140  	}
   141  
   142  	client := pb.NewIstioCertificateServiceClient(conn)
   143  	return client, nil
   144  }
   145  
   146  func san(ns, sa string) string {
   147  	return fmt.Sprintf("spiffe://%s/ns/%s/sa/%s", "cluster.local", ns, sa)
   148  }
   149  
   150  func FetchRootCert(c kubernetes.Interface) (string, error) {
   151  	cm, err := c.CoreV1().ConfigMaps("istio-system").Get(context.TODO(), "istio-ca-root-cert", metav1.GetOptions{})
   152  	if err != nil {
   153  		return "", err
   154  	}
   155  	return cm.Data["root-cert.pem"], nil
   156  }