Files
cloud-services/pkg/hashvault/vault.go

302 lines
7.8 KiB
Go

package hashvault
import (
"bytes"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
b64 "encoding/base64"
"encoding/json"
"encoding/pem"
"io"
"net/http"
"sync"
"time"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/utils/envtool"
"fiskerinc.com/modules/validator"
"github.com/pkg/errors"
)
var (
vaultToken = envtool.GetEnv("VAULT_TOKEN", "REPLACE_ME")
vaultURL = envtool.GetEnv("VAULT_URL", "REPLACE_ME")
vaultIssueEndpath = envtool.GetEnv("ISSUE_PATH", "REPLACE_ME")
vaultRevokeEndpath = envtool.GetEnv("REVOKE_PATH", "REPLACE_ME")
vaultRenewEndpath = envtool.GetEnv("RENEW_PATH", "REPLACE_ME")
vaultPki = envtool.GetEnv("VAULT_PKI", "/pki")
organization = "Fisker, Inc"
organization_unit = "Edge Compute"
vaultClientOnce sync.Once
vaultClient VaultInterface
)
const (
vaultHeader = "X-Vault-Token"
cert_format = "pem"
)
type VaultInterface interface {
CreateCertificate(commonName string, certificateType string, isEU bool) (*common.Certificate, error)
CreatePKICertificate(commonName string) (*common.Certificate, error)
RevokeCertificate(serial string, certType string) (*common.Certificate, error)
RenewCertificate(commonName string, privateKey string, certType string) (*common.Certificate, error)
}
func GetVaultClient() VaultInterface {
vaultClientOnce.Do((func() {
if vaultClient == nil {
vaultClient = &Vault{}
}
}))
return vaultClient
}
func SetVaultClient(client VaultInterface) {
vaultClient = client
}
type VaultCreateCert struct {
Common string `json:"common_name" validate:"required,max=64"`
Format string `json:"format"`
}
type Vault struct {
}
func (vault *Vault) CreateCertificate(commonName string, certificateType string, isEU bool) (*common.Certificate, error) {
url := vaultURLPath(certificateType) + getIssuePath(commonName, isEU)
postBody := VaultCreateCert{
Common: commonName,
Format: cert_format,
}
err := validator.ValidateStruct(postBody)
if err != nil {
return nil, errors.WithStack(err)
}
return vault.createCertificate(url, commonName, certificateType, postBody)
}
func (vault *Vault) CreatePKICertificate(commonName string) (*common.Certificate, error) {
url := vaultURL + "/pki-manufacture/issue/fisker"
postBody := VaultCreateCert{
Common: commonName,
Format: cert_format,
}
err := validator.ValidateStruct(postBody)
if err != nil {
return nil, errors.WithStack(err)
}
// TODO: clarify with John Wu if cert type is properly used.
return vault.createCertificate(url, commonName, "rsa", postBody)
}
func (vault *Vault) createCertificate(vaultURL, commonName, certType string, postBody any) (*common.Certificate, error) {
postHeader := http.Header{}
postHeader.Set(vaultHeader, vaultToken)
client := &http.Client{Timeout: 60 * time.Second}
jsonBytes, err := json.Marshal(postBody)
if err != nil {
return nil, errors.WithStack(err)
}
request, err := http.NewRequest(http.MethodPost, vaultURL, bytes.NewReader(jsonBytes))
if err != nil {
return nil, errors.WithStack(err)
}
request.Header = postHeader
resp, err := client.Do(request)
if err != nil {
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
if resp != nil && resp.StatusCode != 200 {
return nil, ErrorCertificateCreation(resp)
}
var vaultResponse VaultResponse
err = json.NewDecoder(resp.Body).Decode(&vaultResponse)
if err != nil {
return nil, errors.WithStack(err)
}
var certificateData = vaultResponse.CertificateData
response := common.Certificate{
PublicKey: certificateData.Certificate,
CommonName: commonName,
PrivateKey: certificateData.PrivateKey,
SerialNumber: certificateData.SerialNumber,
EncryptedKey: []byte(certificateData.PrivateKey),
Type: certType,
Valid: true,
}
return &response, nil
}
func (vault *Vault) RevokeCertificate(serial string, certType string) (*common.Certificate, error) {
url := vaultURLPath(certType)
postBody := struct {
Serial string `json:"serial_number"`
}{
Serial: serial,
}
postHeader := http.Header{}
postHeader.Set(vaultHeader, vaultToken)
resp, err := httpclient.Post(url+vaultRevokeEndpath, postBody, postHeader)
if err != nil {
return nil, errors.WithStack(err)
}
if resp != nil && resp.StatusCode != 200 {
return nil, ErrorCertificateCreation(resp)
}
response := common.Certificate{
SerialNumber: serial,
Valid: false,
}
return &response, nil
}
func (vault *Vault) RenewCertificate(commonName string, privateKey string, certType string) (*common.Certificate, error) {
url := vaultURLPath(certType)
csrBytes, err := GenerateCsr(commonName, privateKey)
if err != nil {
return nil, errors.WithStack(err)
}
postBody := struct {
CSR string `json:"csr"`
Common string `json:"common_name"`
Format string `json:"format"`
}{
CSR: FormatCsr(csrBytes),
Common: commonName,
Format: cert_format,
}
postHeader := http.Header{}
postHeader.Set(vaultHeader, vaultToken)
resp, err := httpclient.Post(url+vaultRenewEndpath, postBody, postHeader)
if err != nil {
return nil, errors.WithStack(err)
}
defer resp.Body.Close()
if resp != nil && resp.StatusCode != 200 {
return nil, ErrorCertificateCreation(resp)
}
var vaultResponse VaultResponse
var certificateData CertificateData
err = json.NewDecoder(resp.Body).Decode(&vaultResponse)
certificateData = vaultResponse.CertificateData
response := common.Certificate{
PublicKey: certificateData.Certificate,
CommonName: commonName,
SerialNumber: certificateData.SerialNumber,
Valid: true,
Type: certType,
}
return &response, err
}
func vaultURLPath(certType string) string {
url := vaultURL
switch certType {
case common.CertAftersales:
return url + "/pki-aftersales"
default:
return url + vaultPki
}
}
func getIssuePath(vin string, isEU bool) string {
if isEU {
return vaultIssueEndpath + "-eu"
}
err := validator.GetValidator().Var(vin, "vin,vincheck")
if err != nil {
return vaultIssueEndpath
}
switch vin[6:7] {
case "E":
return vaultIssueEndpath + "-eu"
default:
return vaultIssueEndpath
}
}
func FormatCsr(csrBytes []byte) string {
csr := b64.StdEncoding.EncodeToString([]byte(csrBytes))
for i := 0; i < len(csr); i += 64 {
csr = csr[:i] + "\n" + csr[i:]
}
return "-----BEGIN CERTIFICATE REQUEST-----" + csr + "\n-----END CERTIFICATE REQUEST-----"
}
func GenerateCsr(vin string, privateKey string) ([]byte, error) {
subj := pkix.Name{
CommonName: vin,
Organization: []string{organization},
OrganizationalUnit: []string{organization_unit},
}
template := x509.CertificateRequest{
Subject: subj,
SignatureAlgorithm: x509.SHA256WithRSA,
}
block, _ := pem.Decode([]byte(privateKey))
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
csrBytes, _ := x509.CreateCertificateRequest(rand.Reader, &template, priv)
return csrBytes, err
}
func readRespString(resp *http.Response) string {
if resp == nil {
return ""
}
data, err := io.ReadAll(resp.Body)
if err != nil {
logger.Warn().Err(err).Send()
return ""
}
return string(data)
}
func ErrorCertificateCreation(resp *http.Response) error {
err := readRespString(resp)
return errors.Errorf("Vault unable to create certificate, status code: %d %s", resp.StatusCode, err)
}
type VaultResponse struct {
RequestID string `json:"request_id"`
LeaseID string `json:"lease_id"`
Renewable bool `json:"renewable"`
LeaseDuration int `json:"lease_duration"`
CertificateData CertificateData `json:"data"`
}
type CertificateData struct {
Certificate string `json:"certificate"`
Expiration int `json:"expiration"`
IssuingCA string `json:"issuing_ca"`
PrivateKey string `json:"private_key"`
PrivateKeyType string `json:"private_key_type"`
SerialNumber string `json:"serial_number"`
}