...

Source file src/sigs.k8s.io/controller-runtime/pkg/internal/testing/certs/tinyca.go

Documentation: sigs.k8s.io/controller-runtime/pkg/internal/testing/certs

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package certs
    18  
    19  // NB(directxman12): nothing has verified that this has good settings.  In fact,
    20  // the setting generated here are probably terrible, but they're fine for integration
    21  // tests.  These ABSOLUTELY SHOULD NOT ever be exposed in the public API.  They're
    22  // ONLY for use with envtest's ability to configure webhook testing.
    23  // If I didn't otherwise not want to add a dependency on cfssl, I'd just use that.
    24  
    25  import (
    26  	"crypto"
    27  	"crypto/ecdsa"
    28  	"crypto/elliptic"
    29  	crand "crypto/rand"
    30  	"crypto/x509"
    31  	"crypto/x509/pkix"
    32  	"encoding/pem"
    33  	"fmt"
    34  	"math/big"
    35  	"net"
    36  	"time"
    37  
    38  	certutil "k8s.io/client-go/util/cert"
    39  )
    40  
    41  var (
    42  	ellipticCurve = elliptic.P256()
    43  	bigOne        = big.NewInt(1)
    44  )
    45  
    46  // CertPair is a private key and certificate for use for client auth, as a CA, or serving.
    47  type CertPair struct {
    48  	Key  crypto.Signer
    49  	Cert *x509.Certificate
    50  }
    51  
    52  // CertBytes returns the PEM-encoded version of the certificate for this pair.
    53  func (k CertPair) CertBytes() []byte {
    54  	return pem.EncodeToMemory(&pem.Block{
    55  		Type:  "CERTIFICATE",
    56  		Bytes: k.Cert.Raw,
    57  	})
    58  }
    59  
    60  // AsBytes encodes keypair in the appropriate formats for on-disk storage (PEM and
    61  // PKCS8, respectively).
    62  func (k CertPair) AsBytes() (cert []byte, key []byte, err error) {
    63  	cert = k.CertBytes()
    64  
    65  	rawKeyData, err := x509.MarshalPKCS8PrivateKey(k.Key)
    66  	if err != nil {
    67  		return nil, nil, fmt.Errorf("unable to encode private key: %w", err)
    68  	}
    69  
    70  	key = pem.EncodeToMemory(&pem.Block{
    71  		Type:  "PRIVATE KEY",
    72  		Bytes: rawKeyData,
    73  	})
    74  
    75  	return cert, key, nil
    76  }
    77  
    78  // TinyCA supports signing serving certs and client-certs,
    79  // and can be used as an auth mechanism with envtest.
    80  type TinyCA struct {
    81  	CA      CertPair
    82  	orgName string
    83  
    84  	nextSerial *big.Int
    85  }
    86  
    87  // newPrivateKey generates a new private key of a relatively sane size (see
    88  // rsaKeySize).
    89  func newPrivateKey() (crypto.Signer, error) {
    90  	return ecdsa.GenerateKey(ellipticCurve, crand.Reader)
    91  }
    92  
    93  // NewTinyCA creates a new a tiny CA utility for provisioning serving certs and client certs FOR TESTING ONLY.
    94  // Don't use this for anything else!
    95  func NewTinyCA() (*TinyCA, error) {
    96  	caPrivateKey, err := newPrivateKey()
    97  	if err != nil {
    98  		return nil, fmt.Errorf("unable to generate private key for CA: %w", err)
    99  	}
   100  	caCfg := certutil.Config{CommonName: "envtest-environment", Organization: []string{"envtest"}}
   101  	caCert, err := certutil.NewSelfSignedCACert(caCfg, caPrivateKey)
   102  	if err != nil {
   103  		return nil, fmt.Errorf("unable to generate certificate for CA: %w", err)
   104  	}
   105  
   106  	return &TinyCA{
   107  		CA:         CertPair{Key: caPrivateKey, Cert: caCert},
   108  		orgName:    "envtest",
   109  		nextSerial: big.NewInt(1),
   110  	}, nil
   111  }
   112  
   113  func (c *TinyCA) makeCert(cfg certutil.Config) (CertPair, error) {
   114  	now := time.Now()
   115  
   116  	key, err := newPrivateKey()
   117  	if err != nil {
   118  		return CertPair{}, fmt.Errorf("unable to create private key: %w", err)
   119  	}
   120  
   121  	serial := new(big.Int).Set(c.nextSerial)
   122  	c.nextSerial.Add(c.nextSerial, bigOne)
   123  
   124  	template := x509.Certificate{
   125  		Subject:      pkix.Name{CommonName: cfg.CommonName, Organization: cfg.Organization},
   126  		DNSNames:     cfg.AltNames.DNSNames,
   127  		IPAddresses:  cfg.AltNames.IPs,
   128  		SerialNumber: serial,
   129  
   130  		KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
   131  		ExtKeyUsage: cfg.Usages,
   132  
   133  		// technically not necessary for testing, but let's set anyway just in case.
   134  		NotBefore: now.UTC(),
   135  		// 1 week -- the default for cfssl, and just long enough for a
   136  		// long-term test, but not too long that anyone would try to use this
   137  		// seriously.
   138  		NotAfter: now.Add(168 * time.Hour).UTC(),
   139  	}
   140  
   141  	certRaw, err := x509.CreateCertificate(crand.Reader, &template, c.CA.Cert, key.Public(), c.CA.Key)
   142  	if err != nil {
   143  		return CertPair{}, fmt.Errorf("unable to create certificate: %w", err)
   144  	}
   145  
   146  	cert, err := x509.ParseCertificate(certRaw)
   147  	if err != nil {
   148  		return CertPair{}, fmt.Errorf("generated invalid certificate, could not parse: %w", err)
   149  	}
   150  
   151  	return CertPair{
   152  		Key:  key,
   153  		Cert: cert,
   154  	}, nil
   155  }
   156  
   157  // NewServingCert returns a new CertPair for a serving HTTPS on localhost (or other specified names).
   158  func (c *TinyCA) NewServingCert(names ...string) (CertPair, error) {
   159  	if len(names) == 0 {
   160  		names = []string{"localhost"}
   161  	}
   162  	dnsNames, ips, err := resolveNames(names)
   163  	if err != nil {
   164  		return CertPair{}, err
   165  	}
   166  
   167  	return c.makeCert(certutil.Config{
   168  		CommonName:   "localhost",
   169  		Organization: []string{c.orgName},
   170  		AltNames: certutil.AltNames{
   171  			DNSNames: dnsNames,
   172  			IPs:      ips,
   173  		},
   174  		Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
   175  	})
   176  }
   177  
   178  // ClientInfo describes some Kubernetes user for the purposes of creating
   179  // client certificates.
   180  type ClientInfo struct {
   181  	// Name is the user name (embedded as the cert's CommonName)
   182  	Name string
   183  	// Groups are the groups to which this user belongs (embedded as the cert's
   184  	// Organization)
   185  	Groups []string
   186  }
   187  
   188  // NewClientCert produces a new CertPair suitable for use with Kubernetes
   189  // client cert auth with an API server validating based on this CA.
   190  func (c *TinyCA) NewClientCert(user ClientInfo) (CertPair, error) {
   191  	return c.makeCert(certutil.Config{
   192  		CommonName:   user.Name,
   193  		Organization: user.Groups,
   194  		Usages:       []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
   195  	})
   196  }
   197  
   198  func resolveNames(names []string) ([]string, []net.IP, error) {
   199  	dnsNames := []string{}
   200  	ips := []net.IP{}
   201  	for _, name := range names {
   202  		if name == "" {
   203  			continue
   204  		}
   205  		ip := net.ParseIP(name)
   206  		if ip == nil {
   207  			dnsNames = append(dnsNames, name)
   208  			// Also resolve to IPs.
   209  			nameIPs, err := net.LookupHost(name)
   210  			if err != nil {
   211  				return nil, nil, err
   212  			}
   213  			for _, nameIP := range nameIPs {
   214  				ip = net.ParseIP(nameIP)
   215  				if ip != nil {
   216  					ips = append(ips, ip)
   217  				}
   218  			}
   219  		} else {
   220  			ips = append(ips, ip)
   221  		}
   222  	}
   223  	return dnsNames, ips, nil
   224  }
   225  

View as plain text