package hashvault import ( "bytes" "crypto/rand" "crypto/x509" "crypto/x509/pkix" b64 "encoding/base64" "encoding/json" "encoding/pem" "io" "net/http" "sync" "time" "github.com/fiskerinc/cloud-services/pkg/common" "github.com/fiskerinc/cloud-services/pkg/httpclient" "github.com/fiskerinc/cloud-services/pkg/logger" "github.com/fiskerinc/cloud-services/pkg/utils/envtool" "github.com/fiskerinc/cloud-services/pkg/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"` }