2020-08-14 21:36:50 +00:00
|
|
|
package pki
|
|
|
|
|
|
|
|
import (
|
2020-09-20 05:53:01 +00:00
|
|
|
"crypto/x509"
|
|
|
|
"crypto/x509/pkix"
|
|
|
|
"encoding/pem"
|
|
|
|
"fmt"
|
|
|
|
"io/ioutil"
|
2020-11-04 07:24:31 +00:00
|
|
|
"log"
|
2020-09-20 05:53:01 +00:00
|
|
|
"os"
|
2020-11-05 02:13:37 +00:00
|
|
|
"path"
|
2020-09-20 05:53:01 +00:00
|
|
|
"path/filepath"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"git.sequentialread.com/forest/easypki.git/pkg/certificate"
|
|
|
|
"git.sequentialread.com/forest/easypki.git/pkg/easypki"
|
|
|
|
"git.sequentialread.com/forest/easypki.git/pkg/store"
|
2020-10-21 18:36:28 +00:00
|
|
|
errors "git.sequentialread.com/forest/pkg-errors"
|
2020-09-20 05:53:01 +00:00
|
|
|
"git.sequentialread.com/forest/rootsystem/configuration"
|
|
|
|
"git.sequentialread.com/forest/rootsystem/objectStorage"
|
2020-08-14 21:36:50 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func BuildTLSCertsForThreshold(
|
2020-09-20 05:53:01 +00:00
|
|
|
workingDirectory string,
|
|
|
|
domain string,
|
|
|
|
clientId string,
|
|
|
|
storage objectStorage.ObjectStorager,
|
2020-08-14 21:36:50 +00:00
|
|
|
) error {
|
|
|
|
|
2020-09-20 05:53:01 +00:00
|
|
|
inMemoryStore := &store.InMemory{}
|
|
|
|
pki := &easypki.EasyPKI{Store: inMemoryStore}
|
|
|
|
|
|
|
|
//pkiBytes, err := json.MarshalIndent(inMemoryStore.CAs, "", " ")
|
|
|
|
|
|
|
|
clientCA := fmt.Sprintf("%s_CA", clientId)
|
|
|
|
domainCA := fmt.Sprintf("%s_CA", domain)
|
2020-11-05 02:13:37 +00:00
|
|
|
certsBackupLocation := configuration.THRESHOLD_DATA
|
2020-09-20 05:53:01 +00:00
|
|
|
thresholdServerConfigRole := filepath.Join(
|
|
|
|
workingDirectory,
|
|
|
|
configuration.ANSIBLE_ROLES,
|
|
|
|
"threshold-server-config/files",
|
|
|
|
)
|
2020-11-01 21:08:55 +00:00
|
|
|
thresholdClientConfigMount := filepath.Join(
|
2020-09-20 05:53:01 +00:00
|
|
|
workingDirectory,
|
2020-11-01 21:08:55 +00:00
|
|
|
configuration.APPLICATION_MODULES_PATH,
|
|
|
|
"servergarden-ingress/threshold",
|
2020-09-20 05:53:01 +00:00
|
|
|
)
|
|
|
|
thresholdRegisterClientWithServerConfigRole := filepath.Join(
|
|
|
|
workingDirectory,
|
|
|
|
configuration.ANSIBLE_ROLES,
|
|
|
|
"threshold-register-client-with-server/files",
|
|
|
|
)
|
|
|
|
|
|
|
|
for _, path := range []string{
|
2020-11-05 02:13:37 +00:00
|
|
|
certsBackupLocation,
|
2020-09-20 05:53:01 +00:00
|
|
|
thresholdServerConfigRole,
|
2020-11-01 21:08:55 +00:00
|
|
|
thresholdClientConfigMount,
|
2020-09-20 05:53:01 +00:00
|
|
|
thresholdRegisterClientWithServerConfigRole,
|
|
|
|
} {
|
|
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
2020-11-05 02:13:37 +00:00
|
|
|
err = os.MkdirAll(path, 0600)
|
2020-09-20 05:53:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed trying to ensure files folder exists in ansible role")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-04 07:24:31 +00:00
|
|
|
objectStorageKey := fmt.Sprintf("threshold/%s.crt", domainCA)
|
|
|
|
|
|
|
|
domainCACertFile, notFound, err := storage.Get(objectStorageKey)
|
2020-09-20 05:53:01 +00:00
|
|
|
if err != nil && !notFound {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed trying to get domain CA from object storage:")
|
|
|
|
}
|
2020-11-01 21:08:55 +00:00
|
|
|
domainCACertLocalPath := filepath.Join(thresholdClientConfigMount, fmt.Sprintf("%s.crt", domainCA))
|
2020-09-20 05:53:01 +00:00
|
|
|
domainKeyLocalPath := filepath.Join(thresholdServerConfigRole, fmt.Sprintf("%s.key", domain))
|
|
|
|
domainCertLocalPath := filepath.Join(thresholdServerConfigRole, fmt.Sprintf("%s.crt", domain))
|
|
|
|
|
2020-11-04 07:24:31 +00:00
|
|
|
log.Printf("BuildTLSCertsForThreshold(): object storage file '%s' exists: %t\n", objectStorageKey, !notFound)
|
|
|
|
|
2020-09-20 05:53:01 +00:00
|
|
|
// if the file was not already uploaded to object storage, that must mean
|
|
|
|
// we are the first node to run -- therefore we must create and upload it
|
|
|
|
if notFound {
|
2020-11-05 02:13:37 +00:00
|
|
|
log.Println("BuildTLSCertsForThreshold(): creating threshold server CA / key pair ")
|
2020-11-04 07:24:31 +00:00
|
|
|
|
2020-09-20 05:53:01 +00:00
|
|
|
// Create a CA for the server's key/cert
|
|
|
|
err = pki.Sign(
|
|
|
|
nil,
|
|
|
|
&easypki.Request{
|
|
|
|
Name: domainCA,
|
|
|
|
Template: &x509.Certificate{
|
|
|
|
NotAfter: time.Now().Add(time.Hour * 24 * 720),
|
|
|
|
IsCA: true,
|
|
|
|
MaxPathLen: -1,
|
|
|
|
Subject: getSubject(domainCA),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed creating CA")
|
|
|
|
}
|
|
|
|
|
2020-11-01 21:08:55 +00:00
|
|
|
err = saveBundle(pki, domainCA, domainCA, false, thresholdClientConfigMount)
|
2020-09-20 05:53:01 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): saveBundle():")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Upload the server CA certificate to the object storage so the other client(s) can use it
|
|
|
|
bytes, err := ioutil.ReadFile(domainCACertLocalPath)
|
|
|
|
if err != nil {
|
2020-11-01 21:08:55 +00:00
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): cant read domainCA file from thresholdClientConfigMount:")
|
2020-09-20 05:53:01 +00:00
|
|
|
}
|
|
|
|
err = storage.Put(fmt.Sprintf("threshold/%s.crt", domainCA), bytes)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): uploading domainCA file to object storage failed:")
|
|
|
|
}
|
|
|
|
|
|
|
|
caSigner, err := pki.GetCA(domainCA)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): signer named \"CA\" was not found")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create Threshold server certificate
|
|
|
|
err = pki.Sign(
|
|
|
|
caSigner,
|
|
|
|
&easypki.Request{
|
|
|
|
Name: domain,
|
|
|
|
Template: &x509.Certificate{
|
|
|
|
NotAfter: time.Now().Add(time.Hour * 24 * 720),
|
|
|
|
IsCA: false,
|
|
|
|
Subject: getSubject(domain),
|
|
|
|
DNSNames: []string{domain},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed creating Threshold server certificate")
|
|
|
|
}
|
|
|
|
|
|
|
|
err = saveBundle(pki, domainCA, domain, true, thresholdServerConfigRole)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): saveBundle():")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Upload the server key/cert to the object storage so the other client(s) can use it
|
|
|
|
bytes, err = ioutil.ReadFile(domainKeyLocalPath)
|
|
|
|
if err != nil {
|
2020-11-01 21:08:55 +00:00
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): cant read domainCA file from thresholdClientConfigMount:")
|
2020-09-20 05:53:01 +00:00
|
|
|
}
|
|
|
|
err = storage.Put(fmt.Sprintf("threshold/%s.key", domain), bytes)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): uploading domainCA file to object storage failed:")
|
|
|
|
}
|
|
|
|
|
|
|
|
bytes, err = ioutil.ReadFile(domainCertLocalPath)
|
|
|
|
if err != nil {
|
2020-11-01 21:08:55 +00:00
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): cant read domainCA file from thresholdClientConfigMount:")
|
2020-09-20 05:53:01 +00:00
|
|
|
}
|
|
|
|
err = storage.Put(fmt.Sprintf("threshold/%s.crt", domain), bytes)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): uploading domainCA file to object storage failed:")
|
|
|
|
}
|
|
|
|
|
|
|
|
// else, if the domainCACertFile does exist in object storage, that means another node already created and uploaded it.
|
|
|
|
// just in case this node wants to run the global terraform project, we should download the cert/key too.
|
|
|
|
// storing them in the object storage should be fine security wise because its not a very high security cert
|
|
|
|
// for example, we are uploading it to a cloud provider anyway, and we don't fully trust the threshold server anyways
|
|
|
|
// since it runs on cloud, on someone elses computer
|
|
|
|
|
|
|
|
} else {
|
2020-11-05 02:13:37 +00:00
|
|
|
log.Println("BuildTLSCertsForThreshold(): using threshold server CA / key pair from object storage")
|
2020-11-04 07:24:31 +00:00
|
|
|
|
2020-09-20 05:53:01 +00:00
|
|
|
err = ioutil.WriteFile(domainCACertLocalPath, domainCACertFile.Content, 0600)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): writing domainCA file to ansible role failed")
|
|
|
|
}
|
|
|
|
|
|
|
|
domainCertFile, notFound, err := storage.Get(fmt.Sprintf("threshold/%s.crt", domain))
|
|
|
|
if err != nil || notFound {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed trying to get domainCertFile from object storage:")
|
|
|
|
}
|
|
|
|
domainKeyFile, notFound, err := storage.Get(fmt.Sprintf("threshold/%s.key", domain))
|
|
|
|
if err != nil || notFound {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed trying to get domainCertFile from object storage:")
|
|
|
|
}
|
|
|
|
|
|
|
|
err = ioutil.WriteFile(domainCertLocalPath, domainCertFile.Content, 0600)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): writing domainCertFile to ansible role failed")
|
|
|
|
}
|
|
|
|
err = ioutil.WriteFile(domainKeyLocalPath, domainKeyFile.Content, 0600)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): writing domainCertFile to ansible role failed")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-04 07:24:31 +00:00
|
|
|
// Create A CA for this client if it doesn't already exist
|
2020-11-05 02:13:37 +00:00
|
|
|
clientEmail := fmt.Sprintf("%s@%s", clientId, domain)
|
|
|
|
clientCAFilename := fmt.Sprintf("%s.crt", clientCA)
|
|
|
|
clientCertFilename := fmt.Sprintf("%s.crt", clientEmail)
|
|
|
|
clientKeyFilename := fmt.Sprintf("%s.key", clientEmail)
|
|
|
|
|
|
|
|
clientCAFilePath := filepath.Join(certsBackupLocation, clientCAFilename)
|
2020-11-04 07:24:31 +00:00
|
|
|
_, statErr := os.Stat(clientCAFilePath)
|
|
|
|
|
|
|
|
log.Printf("BuildTLSCertsForThreshold(): file '%s' exists: %t\n", clientCAFilePath, !os.IsNotExist(statErr))
|
|
|
|
|
|
|
|
if os.IsNotExist(statErr) {
|
|
|
|
log.Println("BuildTLSCertsForThreshold(): creating threshold client CA")
|
|
|
|
err = pki.Sign(
|
|
|
|
nil,
|
|
|
|
&easypki.Request{
|
|
|
|
Name: clientCA,
|
|
|
|
Template: &x509.Certificate{
|
|
|
|
NotAfter: time.Now().Add(time.Hour * 24 * 720),
|
|
|
|
IsCA: true,
|
|
|
|
MaxPathLen: -1,
|
|
|
|
Subject: getSubject(clientCA),
|
|
|
|
},
|
2020-09-20 05:53:01 +00:00
|
|
|
},
|
2020-11-04 07:24:31 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed creating CA")
|
|
|
|
}
|
2020-09-20 05:53:01 +00:00
|
|
|
|
2020-11-05 02:13:37 +00:00
|
|
|
err = saveBundle(pki, clientCA, clientCA, false, certsBackupLocation)
|
2020-11-04 07:24:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): saveBundle():")
|
|
|
|
}
|
2020-09-20 05:53:01 +00:00
|
|
|
|
2020-11-04 07:24:31 +00:00
|
|
|
clientCASigner, err := pki.GetCA(clientCA)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): signer named \"CA\" was not found")
|
|
|
|
}
|
2020-09-20 05:53:01 +00:00
|
|
|
|
2020-11-04 07:24:31 +00:00
|
|
|
// Create Threshold client certificates
|
|
|
|
err = pki.Sign(
|
|
|
|
clientCASigner,
|
|
|
|
&easypki.Request{
|
|
|
|
Name: clientEmail,
|
|
|
|
Template: &x509.Certificate{
|
|
|
|
NotAfter: time.Now().Add(time.Hour * 24 * 720),
|
|
|
|
IsCA: false,
|
|
|
|
Subject: getSubject(clientEmail),
|
|
|
|
EmailAddresses: []string{clientEmail},
|
|
|
|
},
|
2020-09-20 05:53:01 +00:00
|
|
|
},
|
2020-11-04 07:24:31 +00:00
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed creating Threshold client certificate")
|
|
|
|
}
|
2020-09-20 05:53:01 +00:00
|
|
|
|
2020-11-05 02:13:37 +00:00
|
|
|
err = saveBundle(pki, clientCA, clientEmail, true, certsBackupLocation)
|
2020-11-04 07:24:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): saveBundle():")
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log.Println("BuildTLSCertsForThreshold(): using existing threshold client CA")
|
2020-09-20 05:53:01 +00:00
|
|
|
}
|
|
|
|
|
2020-11-05 02:13:37 +00:00
|
|
|
err = copyFile(path.Join(thresholdClientConfigMount, clientCertFilename), path.Join(certsBackupLocation, clientCertFilename))
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed copying cert for threshold client")
|
|
|
|
}
|
|
|
|
err = copyFile(path.Join(thresholdClientConfigMount, clientKeyFilename), path.Join(certsBackupLocation, clientKeyFilename))
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed copying key for threshold client")
|
|
|
|
}
|
|
|
|
|
|
|
|
err = copyFile(path.Join(thresholdRegisterClientWithServerConfigRole, clientCAFilename), path.Join(certsBackupLocation, clientCAFilename))
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed copying client CA cert for threshold server")
|
|
|
|
}
|
|
|
|
|
2020-09-20 05:53:01 +00:00
|
|
|
return nil
|
2020-08-14 21:36:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func saveBundle(pki *easypki.EasyPKI, caName, bundleName string, savePrivateKey bool, saveInDirectory string) error {
|
2020-09-20 05:53:01 +00:00
|
|
|
var bundle *certificate.Bundle
|
|
|
|
if caName == "" {
|
|
|
|
caName = bundleName
|
|
|
|
}
|
|
|
|
|
|
|
|
bundle, err := pki.GetBundle(caName, bundleName)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "Failed getting bundle %v within CA %v: %v", bundleName, caName)
|
|
|
|
}
|
|
|
|
leaf := bundle
|
|
|
|
chain := []*certificate.Bundle{bundle}
|
|
|
|
//if fullChain {
|
|
|
|
for {
|
|
|
|
if leaf.Cert.Issuer.CommonName == leaf.Cert.Subject.CommonName {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
ca, err := pki.GetCA(leaf.Cert.Issuer.CommonName)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrapf(err, "Failed getting signing CA %v: %v", leaf.Cert.Issuer.CommonName)
|
|
|
|
}
|
|
|
|
chain = append(chain, ca)
|
|
|
|
leaf = ca
|
|
|
|
}
|
|
|
|
//}
|
|
|
|
|
|
|
|
if savePrivateKey {
|
|
|
|
key, err := os.Create(filepath.Join(saveInDirectory, bundleName+".key"))
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "Failed creating key output file")
|
|
|
|
}
|
|
|
|
if err := pem.Encode(key, &pem.Block{
|
|
|
|
Bytes: x509.MarshalPKCS1PrivateKey(bundle.Key),
|
|
|
|
Type: "RSA PRIVATE KEY",
|
|
|
|
}); err != nil {
|
|
|
|
return errors.Wrap(err, "Failed ecoding private key")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
crtName := bundleName + ".crt"
|
|
|
|
cert, err := os.Create(filepath.Join(saveInDirectory, crtName))
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "Failed creating chain output file")
|
|
|
|
}
|
|
|
|
for _, c := range chain {
|
|
|
|
if err := pem.Encode(cert, &pem.Block{
|
|
|
|
Bytes: c.Cert.Raw,
|
|
|
|
Type: "CERTIFICATE",
|
|
|
|
}); err != nil {
|
|
|
|
return errors.Wrapf(err, "Failed ecoding %v certificate: %v", c.Name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
2020-08-14 21:36:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO add certificate metadata option to seedpacket?
|
|
|
|
func getSubject(commonName string) pkix.Name {
|
2020-09-20 05:53:01 +00:00
|
|
|
return pkix.Name{
|
|
|
|
Organization: []string{"user left blank"},
|
|
|
|
OrganizationalUnit: []string{"user left blank"},
|
|
|
|
Locality: []string{"user left blank"},
|
|
|
|
Country: []string{"user left blank"},
|
|
|
|
Province: []string{"user left blank"},
|
|
|
|
CommonName: commonName,
|
|
|
|
}
|
2020-08-14 21:36:50 +00:00
|
|
|
}
|
2020-11-05 02:13:37 +00:00
|
|
|
|
|
|
|
func copyFile(dst, src string) error {
|
|
|
|
bytes, err := ioutil.ReadFile(src)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return ioutil.WriteFile(dst, bytes, 0600)
|
|
|
|
}
|