rootsystem/pki/pki.go

352 lines
12 KiB
Go
Raw Permalink Normal View History

package pki
import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io/ioutil"
2020-11-04 07:24:31 +00:00
"log"
"os"
2020-11-05 02:13:37 +00:00
"path"
"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"
errors "git.sequentialread.com/forest/pkg-errors"
"git.sequentialread.com/forest/rootsystem/configuration"
"git.sequentialread.com/forest/rootsystem/objectStorage"
)
func BuildTLSCertsForThreshold(
workingDirectory string,
domain string,
clientId string,
storage objectStorage.ObjectStorager,
) error {
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
thresholdServerConfigRole := filepath.Join(
workingDirectory,
configuration.ANSIBLE_ROLES,
"threshold-server-config/files",
)
thresholdClientConfigMount := filepath.Join(
workingDirectory,
configuration.APPLICATION_MODULES_PATH,
"servergarden-ingress/threshold",
)
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,
thresholdServerConfigRole,
thresholdClientConfigMount,
thresholdRegisterClientWithServerConfigRole,
} {
if _, err := os.Stat(path); os.IsNotExist(err) {
2020-11-05 02:13:37 +00:00
err = os.MkdirAll(path, 0600)
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)
if err != nil && !notFound {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed trying to get domain CA from object storage:")
}
domainCACertLocalPath := filepath.Join(thresholdClientConfigMount, fmt.Sprintf("%s.crt", domainCA))
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)
// 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
// 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")
}
err = saveBundle(pki, domainCA, domainCA, false, thresholdClientConfigMount)
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 {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): cant read domainCA file from thresholdClientConfigMount:")
}
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 {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): cant read domainCA file from thresholdClientConfigMount:")
}
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 {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): cant read domainCA file from thresholdClientConfigMount:")
}
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
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-11-04 07:24:31 +00:00
)
if err != nil {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed creating CA")
}
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-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-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-11-04 07:24:31 +00:00
)
if err != nil {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed creating Threshold client certificate")
}
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-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")
}
return nil
}
func saveBundle(pki *easypki.EasyPKI, caName, bundleName string, savePrivateKey bool, saveInDirectory string) error {
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
}
// TODO add certificate metadata option to seedpacket?
func getSubject(commonName string) pkix.Name {
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-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)
}