sigs.k8s.io/controller-runtime@v0.18.2/pkg/internal/testing/certs/tinyca.go (about) 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 }