1package certificates
2
3import (
4	"bytes"
5	"crypto/rand"
6	"crypto/rsa"
7	"crypto/x509"
8	"crypto/x509/pkix"
9	"encoding/pem"
10	"errors"
11	"fmt"
12	"io/ioutil"
13	"math/big"
14	"net"
15	"os"
16	"time"
17
18	"github.com/hashicorp/go-multierror"
19	"github.com/hashicorp/go-uuid"
20)
21
22// Generate is a convenience method for testing. It creates a group of test certificates with the
23// client certificate reflecting the given values. Close() should be called when done to immediately
24// delete the three temporary files it has created.
25//
26// Usage:
27//
28// testCerts, err := certificates.Generate(...)
29// if err != nil {
30// 		...
31// }
32// defer func(){
33// 		if err := testCerts.Close(); err != nil {
34//			...
35// 		}
36// }()
37//
38func Generate(instanceID, orgID, spaceID, appID, ipAddress string) (*TestCertificates, error) {
39	caCert, instanceCert, instanceKey, err := generate(instanceID, orgID, spaceID, appID, ipAddress)
40	if err != nil {
41		return nil, err
42	}
43
44	// Keep a list of paths we've created so that if we fail along the way,
45	// we can attempt to clean them up.
46	var paths []string
47	pathToCACertificate, err := makePathTo(caCert)
48	if err != nil {
49		// No path was successfully created, so we don't need to cleanup here.
50		return nil, err
51	}
52	paths = append(paths, pathToCACertificate)
53
54	pathToInstanceCertificate, err := makePathTo(instanceCert)
55	if err != nil {
56		if cleanupErr := cleanup(paths); cleanupErr != nil {
57			return nil, multierror.Append(err, cleanupErr)
58		}
59		return nil, err
60	}
61	paths = append(paths, pathToInstanceCertificate)
62
63	pathToInstanceKey, err := makePathTo(instanceKey)
64	if err != nil {
65		if cleanupErr := cleanup(paths); cleanupErr != nil {
66			return nil, multierror.Append(err, cleanupErr)
67		}
68		return nil, err
69	}
70	paths = append(paths, pathToInstanceKey)
71
72	// Provide a function to be called at the end cleaning up our temporary files.
73	cleanup := func() error {
74		return cleanup(paths)
75	}
76
77	return &TestCertificates{
78		CACertificate:             caCert,
79		InstanceCertificate:       instanceCert,
80		InstanceKey:               instanceKey,
81		PathToCACertificate:       pathToCACertificate,
82		PathToInstanceCertificate: pathToInstanceCertificate,
83		PathToInstanceKey:         pathToInstanceKey,
84		cleanup:                   cleanup,
85	}, nil
86}
87
88type TestCertificates struct {
89	CACertificate       string
90	InstanceCertificate string
91	InstanceKey         string
92
93	PathToCACertificate       string
94	PathToInstanceCertificate string
95	PathToInstanceKey         string
96
97	// cleanup contains a function that has a path to all the temporary files we made,
98	// and deletes them. They're all in the /tmp folder so they'll disappear on the next
99	// system restart anyways, but in case of repeated tests, it's best to leave nothing
100	// behind if possible.
101	cleanup func() error
102}
103
104func (e *TestCertificates) Close() error {
105	return e.cleanup()
106}
107
108func generate(instanceID, orgID, spaceID, appID, ipAddress string) (caCert, instanceCert, instanceKey string, err error) {
109	caCert, caPriv, err := generateCA("", nil)
110	if err != nil {
111		return "", "", "", err
112	}
113
114	intermediateCert, intermediatePriv, err := generateCA(caCert, caPriv)
115	if err != nil {
116		return "", "", "", err
117	}
118
119	identityCert, identityPriv, err := generateIdentity(intermediateCert, intermediatePriv, instanceID, orgID, spaceID, appID, ipAddress)
120	if err != nil {
121		return "", "", "", err
122	}
123
124	// Convert the identity key to something appropriate for a file body.
125	out := &bytes.Buffer{}
126	pem.Encode(out, pemBlockForKey(identityPriv))
127	instanceKey = out.String()
128	return caCert, fmt.Sprintf("%s%s", intermediateCert, identityCert), instanceKey, nil
129}
130
131func generateCA(caCert string, caPriv *rsa.PrivateKey) (string, *rsa.PrivateKey, error) {
132	template := x509.Certificate{
133		SerialNumber: big.NewInt(1),
134		Subject: pkix.Name{
135			Country:      []string{"US"},
136			Province:     []string{"CA"},
137			Organization: []string{"Testing, Inc."},
138			CommonName:   "test-CA",
139		},
140		NotBefore:             time.Now(),
141		NotAfter:              time.Now().Add(time.Hour * 24 * 365 * 100),
142		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
143		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
144		BasicConstraintsValid: true,
145		IsCA:                  true,
146	}
147
148	// Default to self-signing certificates by listing using itself as a parent.
149	parent := &template
150
151	// If a cert is provided, use it as the parent.
152	if caCert != "" {
153		block, certBytes := pem.Decode([]byte(caCert))
154		if block == nil {
155			return "", nil, errors.New("block shouldn't be nil")
156		}
157		if len(certBytes) > 0 {
158			return "", nil, errors.New("there shouldn't be more bytes")
159		}
160		ca509cert, err := x509.ParseCertificate(block.Bytes)
161		if err != nil {
162			return "", nil, err
163		}
164		parent = ca509cert
165	}
166	// If a private key isn't provided, make a new one.
167	priv := caPriv
168	if priv == nil {
169		newPriv, err := rsa.GenerateKey(rand.Reader, 2048)
170		if err != nil {
171			return "", nil, err
172		}
173		priv = newPriv
174	}
175
176	derBytes, err := x509.CreateCertificate(rand.Reader, &template, parent, publicKey(priv), priv)
177	if err != nil {
178		return "", nil, err
179	}
180
181	out := &bytes.Buffer{}
182	pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
183	cert := out.String()
184	return cert, priv, nil
185}
186
187func generateIdentity(caCert string, caPriv *rsa.PrivateKey, instanceID, orgID, spaceID, appID, ipAddress string) (string, *rsa.PrivateKey, error) {
188	block, certBytes := pem.Decode([]byte(caCert))
189	if block == nil {
190		return "", nil, errors.New("block shouldn't be nil")
191	}
192	if len(certBytes) > 0 {
193		return "", nil, errors.New("there shouldn't be more bytes")
194	}
195	ca509cert, err := x509.ParseCertificate(block.Bytes)
196	if err != nil {
197		return "", nil, err
198	}
199
200	template := x509.Certificate{
201		SerialNumber: big.NewInt(1),
202		Subject: pkix.Name{
203			Country:      []string{"US"},
204			Province:     []string{"CA"},
205			Organization: []string{"Cloud Foundry"},
206			OrganizationalUnit: []string{
207				fmt.Sprintf("organization:%s", orgID),
208				fmt.Sprintf("space:%s", spaceID),
209				fmt.Sprintf("app:%s", appID),
210			},
211			CommonName: instanceID,
212		},
213		NotBefore:             time.Now(),
214		NotAfter:              time.Now().Add(time.Hour * 24 * 365 * 100),
215		KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
216		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
217		BasicConstraintsValid: true,
218		IsCA:                  false,
219		IPAddresses:           []net.IP{net.ParseIP(ipAddress)},
220	}
221
222	priv, err := rsa.GenerateKey(rand.Reader, 2048)
223	if err != nil {
224		return "", nil, err
225	}
226
227	derBytes, err := x509.CreateCertificate(rand.Reader, &template, ca509cert, publicKey(priv), caPriv)
228	if err != nil {
229		return "", nil, err
230	}
231
232	out := &bytes.Buffer{}
233	pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
234	cert := out.String()
235	return cert, priv, nil
236}
237
238func makePathTo(certOrKey string) (string, error) {
239	u, err := uuid.GenerateUUID()
240	if err != nil {
241		return "", err
242	}
243	tmpFile, err := ioutil.TempFile("", u)
244	if err != nil {
245		return "", err
246	}
247	if _, err := tmpFile.Write([]byte(certOrKey)); err != nil {
248		return "", err
249	}
250	if err := tmpFile.Close(); err != nil {
251		return "", err
252	}
253	return tmpFile.Name(), nil
254}
255
256func publicKey(priv interface{}) interface{} {
257	switch k := priv.(type) {
258	case *rsa.PrivateKey:
259		return &k.PublicKey
260	default:
261		return nil
262	}
263}
264
265func pemBlockForKey(priv interface{}) *pem.Block {
266	switch k := priv.(type) {
267	case *rsa.PrivateKey:
268		return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
269	default:
270		return nil
271	}
272}
273
274func cleanup(paths []string) error {
275	var result error
276	for i := 0; i < len(paths); i++ {
277		if err := os.Remove(paths[i]); err != nil {
278			result = multierror.Append(result, err)
279		}
280	}
281	return result
282}
283