302 lines
7.8 KiB
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"`
|
|
}
|