~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
107 lines · 3.9 KB Raw History
package vaultcert

import (
	"bytes"
	"crypto/ecdsa"
	"crypto/tls"
	"crypto/x509"
	"encoding/pem"
	"fmt"
	"os"
	"path/filepath"
	"time"

	"github.com/johnandersen777/socialweb-computer-kcp/internal/rootvault"
)

const (
	OpenBaoClientsCACommonName    = "socialweb-openbao-clients-ca"
	OpenBaoClientCommonNamePrefix = "socialweb-openbao-client-"
	openBaoClientDirMode          = 0o700
	openBaoClientMaterialFileMode = 0o600
)

type OpenBaoClientsCA struct {
	CAPEM      []byte
	BundlePath string
	key        *ecdsa.PrivateKey
	cert       *x509.Certificate
}

type OpenBaoClientTLS struct {
	CommonName string
	CertFile   string
	KeyFile    string
}

func OpenBaoClientCommonName(name string) string { return OpenBaoClientCommonNamePrefix + name }

func EnsureOpenBaoClientsCA(stateDir string, rootCAPEM []byte) (OpenBaoClientsCA, error) {
	if stateDir == "" {
		return OpenBaoClientsCA{}, ErrNoRootVaultTLSStateDir
	}
	caPath, keyPath := rootvault.ClientsCAPath(stateDir), rootvault.ClientsCAKeyPath(stateDir)
	ca := OpenBaoClientsCA{BundlePath: rootvault.ClientCABundlePath(stateDir)}
	caPEM, cert, ok := vaultTLSCAOnDisk(VaultTLS{CAPath: caPath})
	var key *ecdsa.PrivateKey
	if ok {
		key, ok = vaultTLSCAKeyOnDisk(cert, VaultTLS{CAKeyPath: keyPath})
	}
	if !ok {
		var der []byte
		var err error
		key, cert, der, err = newNamedCA(time.Now(), OpenBaoClientsCACommonName)
		if err != nil {
			return OpenBaoClientsCA{}, err
		}
		caPEM = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
		keyDER, err := x509.MarshalPKCS8PrivateKey(key)
		if err != nil {
			return OpenBaoClientsCA{}, fmt.Errorf("vaultcert: marshalling the OpenBao clients CA key: %w", err)
		}
		if err := os.MkdirAll(filepath.Dir(caPath), openBaoClientDirMode); err != nil {
			return OpenBaoClientsCA{}, err
		}
		if err := os.WriteFile(keyPath, pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER}), openBaoClientMaterialFileMode); err != nil {
			return OpenBaoClientsCA{}, fmt.Errorf("vaultcert: writing %s: %w", keyPath, err)
		}
		if err := os.WriteFile(caPath, caPEM, openBaoClientMaterialFileMode); err != nil {
			return OpenBaoClientsCA{}, fmt.Errorf("vaultcert: writing %s: %w", caPath, err)
		}
	}
	bundle := append(append(append([]byte{}, bytes.TrimSpace(rootCAPEM)...), '\n'), caPEM...)
	if cur, err := os.ReadFile(ca.BundlePath); err != nil || !bytes.Equal(cur, bundle) {
		if err := os.WriteFile(ca.BundlePath, bundle, openBaoClientMaterialFileMode); err != nil {
			return OpenBaoClientsCA{}, fmt.Errorf("vaultcert: writing %s: %w", ca.BundlePath, err)
		}
	}
	ca.CAPEM, ca.key, ca.cert = caPEM, key, cert
	return ca, nil
}

func (ca OpenBaoClientsCA) EnsureClient(stateDir, name string) (OpenBaoClientTLS, error) {
	out := OpenBaoClientTLS{CommonName: OpenBaoClientCommonName(name), CertFile: rootvault.AppClientCertPath(stateDir, name),
		KeyFile: rootvault.AppClientKeyPath(stateDir, name)}
	pool := x509.NewCertPool()
	pool.AddCert(ca.cert)
	if leaf, ok := usableLeaf(out.CertFile, out.KeyFile, pool, x509.ExtKeyUsageClientAuth); ok && leaf.Subject.CommonName == out.CommonName {
		return out, nil
	}
	certPEM, keyPEM, err := signLeaf(time.Now(), ca.key, ca.cert, out.CommonName, x509.ExtKeyUsageClientAuth, nil, nil)
	if err != nil {
		return OpenBaoClientTLS{}, err
	}
	if err := os.MkdirAll(filepath.Dir(out.CertFile), openBaoClientDirMode); err != nil {
		return OpenBaoClientTLS{}, err
	}
	if err := os.WriteFile(out.KeyFile, keyPEM, openBaoClientMaterialFileMode); err != nil {
		return OpenBaoClientTLS{}, fmt.Errorf("vaultcert: writing %s: %w", out.KeyFile, err)
	}
	if err := os.WriteFile(out.CertFile, append(append([]byte{}, certPEM...), ca.CAPEM...), openBaoClientMaterialFileMode); err != nil {
		return OpenBaoClientTLS{}, fmt.Errorf("vaultcert: writing %s: %w", out.CertFile, err)
	}
	if _, err := tls.LoadX509KeyPair(out.CertFile, out.KeyFile); err != nil {
		return OpenBaoClientTLS{}, fmt.Errorf("vaultcert: the client pair just written at %s: %w", out.CertFile, err)
	}
	return out, nil
}