diff --git a/Makefile b/Makefile index 72cdd09..38d432f 100644 --- a/Makefile +++ b/Makefile @@ -25,59 +25,72 @@ LDFLAGS_ARGS += -X 'main.BUILD_DATE=$(shell date '+%Y-%m-%dT%H:%M:%S%Z%z')' endif ifneq ($(DEFAULT_OIDC_CLIENT_ID),) LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/oidc.DEFAULT_OIDC_CLIENT_ID=$(DEFAULT_OIDC_CLIENT_ID)' -else -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/oidc.DEFAULT_OIDC_CLIENT_ID=athenz-user-cert' endif ifneq ($(DEFAULT_OIDC_CLIENT_SECRET),) LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/oidc.DEFAULT_OIDC_CLIENT_SECRET=$(DEFAULT_OIDC_CLIENT_SECRET)' -else -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/oidc.DEFAULT_OIDC_CLIENT_SECRET=athenz-user-cert' endif ifneq ($(DEFAULT_OIDC_ISSUER),) LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/oidc.DEFAULT_OIDC_ISSUER=$(DEFAULT_OIDC_ISSUER)' -else -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/oidc.DEFAULT_OIDC_ISSUER=http://127.0.0.1:5556/dex' endif ifneq ($(DEFAULT_OIDC_LISTEN_ADDRESS),) LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/oidc.DEFAULT_OIDC_LISTEN_ADDRESS=$(DEFAULT_OIDC_LISTEN_ADDRESS)' -else -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/oidc.DEFAULT_OIDC_LISTEN_ADDRESS=":8080"' endif ifneq ($(DEFAULT_OIDC_SCOPES),) LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/oidc.DEFAULT_OIDC_SCOPES=$(DEFAULT_OIDC_SCOPES)' -else -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/oidc.DEFAULT_OIDC_SCOPES="openid\ email\ profile"' endif ifneq ($(DEFAULT_OIDC_ACCESS_TOKEN_PATH),) LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/oidc.DEFAULT_OIDC_ACCESS_TOKEN_PATH=$(DEFAULT_OIDC_ACCESS_TOKEN_PATH)' -else -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/oidc.DEFAULT_OIDC_ACCESS_TOKEN_PATH=.athenz/.accesstoken' endif -ifneq ($(DEFAULT_CRYPKI_VALIDITY),) -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_CRYPKI_VALIDITY=$(DEFAULT_CRYPKI_VALIDITY)' -else -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_CRYPKI_VALIDITY=2592000' # 30 * 24 * 60 * 60 seconds +ifneq ($(DEFAULT_SIGNER_CRYPKI_SIGN_URL),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_CRYPKI_SIGN_URL=$(DEFAULT_SIGNER_CRYPKI_SIGN_URL)' endif -ifneq ($(DEFAULT_CRYPKI_IDENTIFIER),) -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_CRYPKI_IDENTIFIER=$(DEFAULT_CRYPKI_IDENTIFIER)' -else -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_CRYPKI_IDENTIFIER=athenz' +ifneq ($(DEFAULT_SIGNER_CRYPKI_CA_URL),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_CRYPKI_CA_URL=$(DEFAULT_SIGNER_CRYPKI_CA_URL)' endif -ifneq ($(DEFAULT_CRYPKI_TIMEOUT),) -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_CRYPKI_TIMEOUT=$(DEFAULT_CRYPKI_TIMEOUT)' -else -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_CRYPKI_TIMEOUT=10' # seconds +ifneq ($(.DEFAULT_SIGNER_CRYPKI_VALIDITY),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_CRYPKI_VALIDITY=$(.DEFAULT_SIGNER_CRYPKI_VALIDITY)' endif -ifneq ($(DEFAULT_CRYPKI_ALGORITHM),) -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_CRYPKI_ALGORITHM=$(DEFAULT_CRYPKI_ALGORITHM)' -else -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_CRYPKI_ALGORITHM=RSA' +ifneq ($(.DEFAULT_SIGNER_CRYPKI_IDENTIFIER),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_CRYPKI_IDENTIFIER=$(.DEFAULT_SIGNER_CRYPKI_IDENTIFIER)' endif -ifneq ($(DEFAULT_CRYPKI_ALGORITHM),) -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_CFSSL_TIMEOUT=$(DEFAULT_CFSSL_TIMEOUT)' -else -LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_CFSSL_TIMEOUT=RSA' +ifneq ($(.DEFAULT_SIGNER_CRYPKI_TIMEOUT),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_CRYPKI_TIMEOUT=$(.DEFAULT_SIGNER_CRYPKI_TIMEOUT)' +endif + +ifneq ($(DEFAULT_SIGNER_CFSSL_SIGN_URL),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_CFSSL_SIGN_URL=$(DEFAULT_SIGNER_CFSSL_SIGN_URL)' +endif +ifneq ($(DEFAULT_SIGNER_CFSSL_CA_URL),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_CFSSL_CA_URL=$(DEFAULT_SIGNER_CFSSL_CA_URL)' +endif +ifneq ($(.DEFAULT_SIGNER_CFSSL_TIMEOUT),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_CFSSL_TIMEOUT=$(.DEFAULT_SIGNER_CFSSL_TIMEOUT)' +endif + +ifneq ($(DEFAULT_SIGNER_VAULT_JWT_ROLE),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_VAULT_JWT_ROLE=$(DEFAULT_SIGNER_VAULT_JWT_ROLE)' +endif +ifneq ($(DEFAULT_SIGNER_VAULT_PKI_NAME),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_VAULT_PKI_NAME=$(DEFAULT_SIGNER_VAULT_PKI_NAME)' +endif +ifneq ($(DEFAULT_SIGNER_VAULT_PKI_ROLE),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_VAULT_PKI_ROLE=$(DEFAULT_SIGNER_VAULT_PKI_ROLE)' +endif +ifneq ($(DEFAULT_SIGNER_VAULT_SIGN_URL),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_VAULT_SIGN_URL=$(DEFAULT_SIGNER_VAULT_SIGN_URL)' +endif +ifneq ($(DEFAULT_SIGNER_VAULT_CA_URL),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_VAULT_CA_URL=$(DEFAULT_SIGNER_VAULT_CA_URL)' +endif +ifneq ($(DEFAULT_SIGNER_VAULT_ISSUER_REF),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_VAULT_ISSUER_REF=$(DEFAULT_SIGNER_VAULT_ISSUER_REF)' +endif +ifneq ($(DEFAULT_SIGNER_VAULT_TTL),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_VAULT_TTL=$(DEFAULT_SIGNER_VAULT_TTL)' +endif +ifneq ($(DEFAULT_SIGNER_VAULT_TIMEOUT),) +LDFLAGS_ARGS += -X '$(APP_REPO_URL)/pkg/signer.DEFAULT_SIGNER_VAULT_TIMEOUT=$(DEFAULT_SIGNER_VAULT_TIMEOUT)' endif ifneq ($(LDFLAGS_ARGS),) diff --git a/cmd/main.go b/cmd/main.go index 58d065a..6964731 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -51,7 +51,7 @@ Options: } // Parse argument flags - signerName := flag.String("signer", DEFAULT_SIGNER_NAME, "Name for the certificate signer product (\"crypki\" or \"cfssl\")") + signerName := flag.String("signer", DEFAULT_SIGNER_NAME, "Name for the certificate signer product (\"crypki\", \"cfssl\" or \"vault\")") signerURL := flag.String("sign-url", "", "Target destination URL to send the certificate sign request (leave it empty to use default)") caURL := flag.String("ca-url", "", "Target destination URL to retrieve the ca certificate (leave it empty to use default)") @@ -115,6 +115,13 @@ Options: if *caURL == "" { *caURL = signer.DEFAULT_SIGNER_CFSSL_CA_URL } + case "vault": + if *signerURL == "" { + *signerURL = signer.DEFAULT_SIGNER_VAULT_SIGN_URL + } + if *caURL == "" { + *caURL = signer.DEFAULT_SIGNER_VAULT_CA_URL + } } if *debug { fmt.Printf("Signer URL is set as:%s\n", *signerURL) @@ -165,6 +172,35 @@ Options: if *debug { fmt.Printf("CA certificate:\n%s\n", cacert) } + case "vault": + err, vaulttoken := signer.GetVaultToken(signer.DEFAULT_SIGNER_VAULT_JWT_LOGIN_URL, signer.DEFAULT_SIGNER_VAULT_JWT_ROLE, accesstoken, nil) + if err != nil { + fmt.Printf("Failed to get vault token: %s\n", err) + os.Exit(1) + } + if *debug { + fmt.Printf("Vault Token retrieved Successfully:\n%s\n", vaulttoken) + } + err, cert = signer.SendVaultCSR(*commonName, *signerURL, csr, &map[string][]string{ + "X-Vault-Token": []string{vaulttoken}, + }) + if err != nil { + fmt.Printf("Failed to get signed certificate: %s\n", err) + os.Exit(1) + } + if *debug { + fmt.Printf("Signed certificate:\n%s\n", cert) + } + err, cacert = signer.GetVaultRootCA(false, *caURL, &map[string][]string{ + "X-Vault-Token": []string{vaulttoken}, + }) + if err != nil { + fmt.Printf("Failed to get ca certificate: %s\n", err) + os.Exit(1) + } + if *debug { + fmt.Printf("CA certificate:\n%s\n", cacert) + } } keyPEM, err := certificate.PrivateKeyToPEM(*key) diff --git a/cmd/test.go b/cmd/test.go index 3312871..28493d2 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -34,6 +34,13 @@ func ExecuteTestCommand(arg []string, testFlagSet *flag.FlagSet) { if *caURL == "" { *caURL = signer.DEFAULT_SIGNER_CFSSL_CA_URL } + case "vault": + if *signerURL == "" { + *signerURL = signer.DEFAULT_SIGNER_CFSSL_SIGN_URL + } + if *caURL == "" { + *caURL = signer.DEFAULT_SIGNER_CFSSL_CA_URL + } } if *debug { fmt.Printf("Signer URL is set as:%s\n", *signerURL) @@ -52,6 +59,12 @@ func ExecuteTestCommand(arg []string, testFlagSet *flag.FlagSet) { fmt.Printf("Failed to get ca certificate: %s\n", err) os.Exit(1) } + case "vault": + err, _ := signer.GetVaultRootCA(true, *caURL, &map[string][]string{}) + if err != nil { + fmt.Printf("Failed to get ca certificate: %s\n", err) + os.Exit(1) + } } fmt.Printf("%s test complete\n", DEFAULT_APP_NAME) } diff --git a/cmd/version.go b/cmd/version.go index 8d2f356..d057b19 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -42,4 +42,14 @@ func ExecuteVersionCommand(arg []string, versionFlagSet *flag.FlagSet) { fmt.Printf(" CLI X.509 Certificate Signer URL: %s\n", signer.DEFAULT_SIGNER_CFSSL_SIGN_URL) fmt.Printf(" CLI X.509 Certificate CA URL: %s\n", signer.DEFAULT_SIGNER_CFSSL_CA_URL) fmt.Printf(" CLI X.509 Certificate Request Timeout: %s seconds\n", signer.DEFAULT_SIGNER_CFSSL_TIMEOUT) + fmt.Printf(" CLI X.509 configuration for Vault:\n") + fmt.Printf(" CLI X.509 Certificate Login URL: %s\n", signer.DEFAULT_SIGNER_VAULT_JWT_LOGIN_URL) + fmt.Printf(" CLI X.509 Certificate Login JWT Role: %s\n", signer.DEFAULT_SIGNER_VAULT_JWT_ROLE) + fmt.Printf(" CLI X.509 Certificate PKI Name: %s\n", signer.DEFAULT_SIGNER_VAULT_PKI_NAME) + fmt.Printf(" CLI X.509 Certificate PKI Role: %s\n", signer.DEFAULT_SIGNER_VAULT_PKI_ROLE) + fmt.Printf(" CLI X.509 Certificate Signer URL: %s\n", signer.DEFAULT_SIGNER_VAULT_SIGN_URL) + fmt.Printf(" CLI X.509 Certificate CA URL: %s\n", signer.DEFAULT_SIGNER_VAULT_CA_URL) + fmt.Printf(" CLI X.509 Certificate Issuer Reference: %s\n", signer.DEFAULT_SIGNER_VAULT_ISSUER_REF) + fmt.Printf(" CLI X.509 Certificate TTL: %s\n", signer.DEFAULT_SIGNER_VAULT_TTL) + fmt.Printf(" CLI X.509 Certificate Request Timeout: %s seconds\n", signer.DEFAULT_SIGNER_VAULT_TIMEOUT) } diff --git a/pkg/oidc/accesstoken.go b/pkg/oidc/accesstoken.go index 4b6c5ab..ecf7a17 100644 --- a/pkg/oidc/accesstoken.go +++ b/pkg/oidc/accesstoken.go @@ -45,11 +45,8 @@ func getCachedAccessToken(debug bool) (string, error) { validity, _ := strconv.Atoi(strings.TrimSpace(DEFAULT_OIDC_ACCESS_TOKEN_VALIDITY)) if expired, err := isCacheFileExpired(accessTokenFile, float64(validity), debug); !expired && err == nil { data, err := os.ReadFile(accessTokenFile) - if err != nil { - return "", fmt.Errorf("could not read the cache file, error: %v", err) - } - if expired { - return "", fmt.Errorf("access Token has expired") + if err != nil || expired { + return "", err } return strings.TrimSpace(string(data)), nil } else { @@ -64,8 +61,10 @@ func isCacheFileExpired(filename string, maxAge float64, debug bool) (bool, erro } delta := time.Since(info.ModTime()) // return false if duration exceeds maxAge - expired := delta.Minutes() > maxAge - return expired, nil + if expired := delta.Minutes() > maxAge; expired { + return expired, fmt.Errorf("access token has expired") + } + return false, nil } func createCacheDir(dirname string, debug bool) (bool, error) { @@ -111,7 +110,7 @@ func GetOIDCDiscovery(debug *bool) (string, string, error) { func GetAuthAccessToken(responseMode *string, debug *bool) (string, error) { accessToken, err := getCachedAccessToken(*debug) if *debug && err != nil { - fmt.Printf("Failed get cached access token: %s", err) + fmt.Printf("Failed get cached access token: %s\n", err) } if accessToken != "" { return accessToken, err diff --git a/pkg/signer/crypki.go b/pkg/signer/crypki.go index ba7607d..4c9118d 100644 --- a/pkg/signer/crypki.go +++ b/pkg/signer/crypki.go @@ -14,7 +14,7 @@ import ( var ( DEFAULT_SIGNER_CRYPKI_SIGN_URL = "http://localhost:10000/v3/sig/x509-cert/keys/x509-key" DEFAULT_SIGNER_CRYPKI_CA_URL = "http://localhost:10000/v3/sig/x509-cert/keys/x509-key" - DEFAULT_SIGNER_CRYPKI_VALIDITY = "2592000" // 30 * 24 * 60 * 60, 30 days in seconds + DEFAULT_SIGNER_CRYPKI_VALIDITY = "43200" // 30 * 24 * 60, 1 hour in seconds DEFAULT_SIGNER_CRYPKI_IDENTIFIER = "athenz" DEFAULT_SIGNER_CRYPKI_TIMEOUT = "10" // in seconds ) diff --git a/pkg/signer/vault.go b/pkg/signer/vault.go new file mode 100644 index 0000000..c537503 --- /dev/null +++ b/pkg/signer/vault.go @@ -0,0 +1,216 @@ +package signer + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "strings" + "time" +) + +var ( + DEFAULT_SIGNER_VAULT_JWT_LOGIN_URL = "http://localhost:10000/v1/auth/jwt/login" + DEFAULT_SIGNER_VAULT_JWT_ROLE = "jwt" + DEFAULT_SIGNER_VAULT_PKI_NAME = "rootca" + DEFAULT_SIGNER_VAULT_PKI_ROLE = "issuers" + DEFAULT_SIGNER_VAULT_SIGN_URL = "http://localhost:10000/v1/" + DEFAULT_SIGNER_VAULT_PKI_NAME + "/sign/" + DEFAULT_SIGNER_VAULT_PKI_ROLE + DEFAULT_SIGNER_VAULT_CA_URL = "http://localhost:10000/v1/" + DEFAULT_SIGNER_VAULT_PKI_NAME + "/cert/ca_chain" + DEFAULT_SIGNER_VAULT_ISSUER_REF = "default" + DEFAULT_SIGNER_VAULT_TTL = "1h" + DEFAULT_SIGNER_VAULT_TIMEOUT = "10" // in seconds +) + +func GetVaultToken(url string, role string, jwt string, headers *map[string][]string) (error, string) { + type RequestBody struct { + Role string `json:"role"` + JWT string `json:"jwt"` + } + + body := RequestBody{ + Role: role, + JWT: jwt, + } + + jsonData, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("Failed to marshal JSON: %s", err), "" + } + + timeout, _ := strconv.Atoi(strings.TrimSpace(DEFAULT_SIGNER_VAULT_TIMEOUT)) + client := &http.Client{ + Timeout: time.Duration(timeout) * time.Second, + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("Failed to create request: %s", err), "" + } + + req.Header.Set("Content-Type", "application/json") + if headers != nil { + for key, values := range *headers { + for _, value := range values { + req.Header.Add(key, value) + } + } + } + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("Failed to send request: %s", err), "" + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusBadRequest { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("Received non-OK status: %s, url: %s, response: %s", resp.Status, url, body), "" + } + + type auth struct { + ClientToken string `json:"client_token"` + } + + var response struct { + Auth auth `json:"auth"` + } + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return fmt.Errorf("Failed to parse JSON response: %w", err), "" + } + + return nil, response.Auth.ClientToken +} + +// SendVaultCSR sends a CSR to the Vault server to issue an certificate +// Vault API reference: +// https://developer.hashicorp.com/vault/api-docs/secret/pki#sign-certificate +func SendVaultCSR(commonName string, url string, csr string, headers *map[string][]string) (error, string) { + type RequestBody struct { + CSR string `json:"csr"` + CommonName string `json:"common_name"` + IssuerRef string `json:"issuer_ref"` + TTL string `json:"ttl"` + } + + body := RequestBody{ + CSR: csr, + CommonName: commonName, + IssuerRef: DEFAULT_SIGNER_VAULT_ISSUER_REF, + TTL: DEFAULT_SIGNER_VAULT_TTL, + } + + jsonData, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("Failed to marshal JSON: %s", err), "" + } + + timeout, _ := strconv.Atoi(strings.TrimSpace(DEFAULT_SIGNER_VAULT_TIMEOUT)) + client := &http.Client{ + Timeout: time.Duration(timeout) * time.Second, + } + + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData)) + if err != nil { + return fmt.Errorf("Failed to create request: %s", err), "" + } + + req.Header.Set("Content-Type", "application/json") + if headers != nil { + for key, values := range *headers { + for _, value := range values { + req.Header.Add(key, value) + } + } + } + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("Failed to send request: %s", err), "" + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusBadRequest { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("Received non-OK status: %s, url: %s, response: %s", resp.Status, url, body), "" + } + + type data struct { + Expiration int `json:"expiration"` + Certificate string `json:"certificate"` + CA string `json:"issuing_ca"` + CAChain []string `json:"ca_chain"` + Serial string `json:"serial_number"` + } + + var response struct { + Data data `json:"data"` + } + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return fmt.Errorf("Failed to parse JSON response: %w", err), "" + } + + return nil, response.Data.Certificate +} + +// GetVaultRootCA gets issuer certificate from the Vault server +// Vault API reference: +// https://developer.hashicorp.com/vault/api-docs/secret/pki#read-default-issuer-certificate-chain +// https://developer.hashicorp.com/vault/api-docs/secret/pki#read-issuer-certificate +func GetVaultRootCA(test bool, url string, headers *map[string][]string) (error, string) { + timeout, _ := strconv.Atoi(strings.TrimSpace(DEFAULT_SIGNER_VAULT_TIMEOUT)) + client := &http.Client{ + Timeout: time.Duration(timeout) * time.Second, + } + + req, err := http.NewRequest("GET", url, bytes.NewBuffer(nil)) + if err != nil { + return fmt.Errorf("Failed to create request: %s", err), "" + } + + req.Header.Set("Content-Type", "application/json") + if headers != nil { + for key, values := range *headers { + for _, value := range values { + req.Header.Add(key, value) + } + } + } + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("Failed to send request: %s", err), "" + } + defer resp.Body.Close() + + if test && resp.StatusCode == http.StatusUnauthorized { + return nil, "" + } + + if resp.StatusCode >= http.StatusBadRequest { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("Received non-OK status: %s, url: %s, response: %s", resp.Status, url, body), "" + } + + type data struct { + Certificate string `json:"certificate"` + CAChain string `json:"ca_chain"` + IssuedID string `json:"issuer_id"` + RevocationTime int `json:"revocation_time"` + RevocationTimeRFC3339 string `json:"revocation_time_rfc3339"` + } + + var response struct { + Data data `json:"data"` + } + + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("Failed to parse JSON response: %w, respose: %#v", err, body), "" + } + + return nil, response.Data.CAChain +}