server.garden privileged automation agent (mirror of https://git.sequentialread.com/forest/rootsystem)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

303 lines
9.8 KiB

package pki
import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"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"
"git.sequentialread.com/forest/rootsystem/configuration"
"git.sequentialread.com/forest/rootsystem/objectStorage"
"github.com/pkg/errors"
)
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)
thresholdServerConfigRole := filepath.Join(
workingDirectory,
configuration.ANSIBLE_ROLES,
"threshold-server-config/files",
)
thresholdClientConfigRole := filepath.Join(
workingDirectory,
configuration.ANSIBLE_ROLES,
"threshold-client-config/files",
)
thresholdRegisterClientWithServerConfigRole := filepath.Join(
workingDirectory,
configuration.ANSIBLE_ROLES,
"threshold-register-client-with-server/files",
)
for _, path := range []string{
thresholdServerConfigRole,
thresholdClientConfigRole,
thresholdRegisterClientWithServerConfigRole,
} {
if _, err := os.Stat(path); os.IsNotExist(err) {
err = os.Mkdir(path, 0600)
if err != nil {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed trying to ensure files folder exists in ansible role")
}
}
}
domainCACertFile, notFound, err := storage.Get(fmt.Sprintf("threshold/%s.crt", domainCA))
if err != nil && !notFound {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed trying to get domain CA from object storage:")
}
domainCACertLocalPath := filepath.Join(thresholdClientConfigRole, fmt.Sprintf("%s.crt", domainCA))
domainKeyLocalPath := filepath.Join(thresholdServerConfigRole, fmt.Sprintf("%s.key", domain))
domainCertLocalPath := filepath.Join(thresholdServerConfigRole, fmt.Sprintf("%s.crt", domain))
// 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 {
// 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, thresholdClientConfigRole)
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 thresholdClientConfigRole:")
}
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 thresholdClientConfigRole:")
}
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 thresholdClientConfigRole:")
}
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 {
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")
}
}
// Create A CA for this client
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),
},
},
)
if err != nil {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed creating CA")
}
err = saveBundle(pki, clientCA, clientCA, false, thresholdRegisterClientWithServerConfigRole)
if err != nil {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): saveBundle():")
}
clientCASigner, err := pki.GetCA(clientCA)
if err != nil {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): signer named \"CA\" was not found")
}
// Create Threshold client certificates
clientEmail := fmt.Sprintf("%s@%s", clientId, domain)
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},
},
},
)
if err != nil {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): failed creating Threshold client certificate")
}
err = saveBundle(pki, clientCA, clientEmail, true, thresholdClientConfigRole)
if err != nil {
return errors.Wrap(err, "BuildTLSCertsForThreshold(): saveBundle():")
}
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,
}
}