Initial cloud-services repo - gateway service + pkg modules
This commit is contained in:
301
pkg/hashvault/vault.go
Normal file
301
pkg/hashvault/vault.go
Normal file
@@ -0,0 +1,301 @@
|
||||
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"`
|
||||
}
|
||||
Reference in New Issue
Block a user