1package internal
2
3// NB(directxman12): nothing has verified that this has good settings.  In fact,
4// the setting generated here are probably terrible, but they're fine for integration
5// tests.  These ABSOLUTELY SHOULD NOT ever be exposed in the public API.  They're
6// ONLY for use with envtest's ability to configure webhook testing.
7// If I didn't otherwise not want to add a dependency on cfssl, I'd just use that.
8
9import (
10	"crypto"
11	crand "crypto/rand"
12	"crypto/rsa"
13	"crypto/x509"
14	"crypto/x509/pkix"
15	"encoding/pem"
16	"fmt"
17	"math/big"
18	"net"
19	"time"
20
21	certutil "k8s.io/client-go/util/cert"
22)
23
24var (
25	rsaKeySize = 2048 // a decent number, as of 2019
26	bigOne     = big.NewInt(1)
27)
28
29// CertPair is a private key and certificate for use for client auth, as a CA, or serving.
30type CertPair struct {
31	Key  crypto.Signer
32	Cert *x509.Certificate
33}
34
35// CertBytes returns the PEM-encoded version of the certificate for this pair.
36func (k CertPair) CertBytes() []byte {
37	return pem.EncodeToMemory(&pem.Block{
38		Type:  "CERTIFICATE",
39		Bytes: k.Cert.Raw,
40	})
41}
42
43// AsBytes encodes keypair in the appropriate formats for on-disk storage (PEM and
44// PKCS8, respectively).
45func (k CertPair) AsBytes() (cert []byte, key []byte, err error) {
46	cert = k.CertBytes()
47
48	rawKeyData, err := x509.MarshalPKCS8PrivateKey(k.Key)
49	if err != nil {
50		return nil, nil, fmt.Errorf("unable to encode private key: %v", err)
51	}
52
53	key = pem.EncodeToMemory(&pem.Block{
54		Type:  "PRIVATE KEY",
55		Bytes: rawKeyData,
56	})
57
58	return cert, key, nil
59}
60
61// TinyCA supports signing serving certs and client-certs,
62// and can be used as an auth mechanism with envtest.
63type TinyCA struct {
64	CA      CertPair
65	orgName string
66
67	nextSerial *big.Int
68}
69
70// newPrivateKey generates a new private key of a relatively sane size (see
71// rsaKeySize).
72func newPrivateKey() (crypto.Signer, error) {
73	return rsa.GenerateKey(crand.Reader, rsaKeySize)
74}
75
76func NewTinyCA() (*TinyCA, error) {
77	caPrivateKey, err := newPrivateKey()
78	if err != nil {
79		return nil, fmt.Errorf("unable to generate private key for CA: %v", err)
80	}
81	caCfg := certutil.Config{CommonName: "envtest-environment", Organization: []string{"envtest"}}
82	caCert, err := certutil.NewSelfSignedCACert(caCfg, caPrivateKey)
83	if err != nil {
84		return nil, fmt.Errorf("unable to generate certificate for CA: %v", err)
85	}
86
87	return &TinyCA{
88		CA:         CertPair{Key: caPrivateKey, Cert: caCert},
89		orgName:    "envtest",
90		nextSerial: big.NewInt(1),
91	}, nil
92}
93
94func (c *TinyCA) makeCert(cfg certutil.Config) (CertPair, error) {
95	now := time.Now()
96
97	key, err := newPrivateKey()
98	if err != nil {
99		return CertPair{}, fmt.Errorf("unable to create private key: %v", err)
100	}
101
102	serial := new(big.Int).Set(c.nextSerial)
103	c.nextSerial.Add(c.nextSerial, bigOne)
104
105	template := x509.Certificate{
106		Subject:      pkix.Name{CommonName: cfg.CommonName, Organization: cfg.Organization},
107		DNSNames:     cfg.AltNames.DNSNames,
108		IPAddresses:  cfg.AltNames.IPs,
109		SerialNumber: serial,
110
111		KeyUsage:    x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
112		ExtKeyUsage: cfg.Usages,
113
114		// technically not necessary for testing, but let's set anyway just in case.
115		NotBefore: now.UTC(),
116		// 1 week -- the default for cfssl, and just long enough for a
117		// long-term test, but not too long that anyone would try to use this
118		// seriously.
119		NotAfter: now.Add(168 * time.Hour).UTC(),
120	}
121
122	certRaw, err := x509.CreateCertificate(crand.Reader, &template, c.CA.Cert, key.Public(), c.CA.Key)
123	if err != nil {
124		return CertPair{}, fmt.Errorf("unable to create certificate: %v", err)
125	}
126
127	cert, err := x509.ParseCertificate(certRaw)
128	if err != nil {
129		return CertPair{}, fmt.Errorf("generated invalid certificate, could not parse: %v", err)
130	}
131
132	return CertPair{
133		Key:  key,
134		Cert: cert,
135	}, nil
136}
137
138// NewServingCert returns a new CertPair for a serving HTTPS on localhost.
139func (c *TinyCA) NewServingCert() (CertPair, error) {
140	return c.makeCert(certutil.Config{
141		CommonName:   "localhost",
142		Organization: []string{c.orgName},
143		AltNames: certutil.AltNames{
144			DNSNames: []string{"localhost"},
145			IPs:      []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback},
146		},
147		Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
148	})
149}
150