Skip to content

Commit

Permalink
Merge pull request #30 from Keyfactor/enrollment_timeout_fix
Browse files Browse the repository at this point in the history
Enrollment timeout fix
  • Loading branch information
spbsoluble authored Jan 24, 2024
2 parents 22f361c + e4187e8 commit 8ac97c8
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ jobs:
args: release --clean
env:
# GitHub sets the GITHUB_TOKEN secret automatically.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.V2BUILDTOKEN }}
GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
2 changes: 1 addition & 1 deletion api/store_container.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (c *Client) GetStoreContainers() (*[]CertStoreContainer, error) {
}

var newResp []CertStoreContainer
for i, _ := range resp {
for i := range resp {
var newCont CertStoreContainer
mapResp, _ := resp[i].ToMap()
jsonData, _ := json.Marshal(mapResp)
Expand Down
3 changes: 3 additions & 0 deletions api/store_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@ type CertificateStore struct {

// A Boolean that sets whether to include the private key of the certificate in the certificate store if private keys are optional for the given certificate store (true) or not (false). The default is false.
IncludePrivateKey bool `json:"IncludePrivateKey,omitempty"`

// Job Parameters
JobParameters map[string]string `json:"JobFields,omitempty"`
}

type ListCertificateStoresResponse struct {
Expand Down
4 changes: 2 additions & 2 deletions api/store_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (c *Client) GetCertificateStoreTypeByName(name string) (*CertificateStoreTy
}

var newResp []CertificateStoreType
for i, _ := range resp {
for i := range resp {
var newCertType CertificateStoreType
mapResp, _ := resp[i].ToMap()
jsonData, _ := json.Marshal(mapResp)
Expand Down Expand Up @@ -128,7 +128,7 @@ func (c *Client) ListCertificateStoreTypes() (*[]CertificateStoreType, error) {
}

var newResp []CertificateStoreType
for i, _ := range resp {
for i := range resp {
var newCertType CertificateStoreType
mapResp, _ := resp[i].ToMap()
jsonData, _ := json.Marshal(mapResp)
Expand Down
2 changes: 1 addition & 1 deletion api/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func (c *Client) GetTemplates() ([]GetTemplateResponse, error) {
}

var newResp []GetTemplateResponse
for i, _ := range resp {
for i := range resp {
var newTemp GetTemplateResponse
mapResp, _ := resp[i].ToMap()
jsonData, _ := json.Marshal(mapResp)
Expand Down
87 changes: 86 additions & 1 deletion v2/api/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,79 @@ func (c *Client) EnrollPFX(ea *EnrollPFXFctArgs) (*EnrollResponse, error) {
return jsonResp, nil
}

func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error) {
log.Println("[INFO] Enrolling PFX certificate with Keyfactor")

/* Ensure required inputs exist */
var missingFields []string

// TODO: Probably a better way to express these if blocks
if ea.Template == "" {
missingFields = append(missingFields, "Template")
}
if ea.CertificateAuthority == "" {
missingFields = append(missingFields, "CertificateAuthority")
}
if ea.CertFormat == "" {
missingFields = append(missingFields, "CertFormat")
}
//if ea.Password == "" {
// missingFields = append(missingFields, "Password")
//}

if len(missingFields) > 0 {
return nil, errors.New("Required field(s) missing: " + strings.Join(missingFields, ", "))
}

// Set Keyfactor-specific headers
headers := &apiHeaders{
Headers: []StringTuple{
{"x-keyfactor-api-version", "2"},
{"x-keyfactor-requested-with", "APIClient"},
{"x-certificateformat", ea.CertFormat},
},
}

if ea.Timestamp == "" {
ea.Timestamp = getTimestamp()
}

if ea.SubjectString == "" {
if ea.Subject != nil {
subject, err := createSubject(*ea.Subject)
if err != nil {
return nil, err
}
ea.SubjectString = subject
} else {
return nil, fmt.Errorf("subject is required to use enrollpfx(). Please configure either SubjectString or Subject")
}
}

keyfactorAPIStruct := &request{
Method: "POST",
Endpoint: "Enrollment/PFX",
Headers: headers,
Payload: &ea,
}

resp, err := c.sendRequest(keyfactorAPIStruct)
if err != nil {
return nil, err
}

jsonResp := &EnrollResponseV2{}
err = json.NewDecoder(resp.Body).Decode(&jsonResp)
if err != nil {
return nil, err
}
//err = decodePKCS12Blob(jsonResp)
//if err != nil {
// return nil, err
//}
return jsonResp, nil
}

// DownloadCertificate takes arguments for DownloadCertArgs to facilitate a call to Keyfactor
// that downloads a certificate from Keyfactor.
// The download certificate endpoint requires one of the following to retrieve a cert:
Expand Down Expand Up @@ -321,7 +394,7 @@ func (c *Client) DeployPFXCertificate(args *DeployPFXArgs) (*DeployPFXResp, erro
// and include locations add additional data, but can be set to false if they are unneeded. A pointer to a
// GetCertificateResponse structure is returned, containing the certificate context.
func (c *Client) GetCertificateContext(gca *GetCertificateContextArgs) (*GetCertificateResponse, error) {
if gca.Id <= 0 && gca.Thumbprint == "" && gca.CommonName == "" {
if gca.Id <= 0 && gca.Thumbprint == "" && gca.CommonName == "" && gca.RequestId <= 0 {
return nil, errors.New("keyfactor certificate id, common name, or thumbprint are required to get certificate")
}

Expand Down Expand Up @@ -371,6 +444,11 @@ func (c *Client) GetCertificateContext(gca *GetCertificateContextArgs) (*GetCert
"pq.queryString", fmt.Sprintf(`IssuedCN -eq "%s"`, gca.CommonName),
})
endpoint = "Certificates"
} else if (gca.Id <= 0 && gca.CommonName == "" && gca.Thumbprint == "") && gca.RequestId > 0 {
query.Query = append(query.Query, StringTuple{
"pq.queryString", fmt.Sprintf(`CertRequestId -eq %d`, gca.RequestId),
})
endpoint = "Certificates"
} else {
endpoint = "Certificates/" + fmt.Sprintf("%d", gca.Id)
}
Expand Down Expand Up @@ -402,6 +480,13 @@ func (c *Client) GetCertificateContext(gca *GetCertificateContextArgs) (*GetCert
if len(lCerts) > 1 {
var newestCert GetCertificateResponse
for _, cert := range lCerts {

if gca.RequestId > 0 && cert.CertRequestId == gca.RequestId {
return &cert, nil
} else if gca.Thumbprint == cert.Thumbprint {
return &cert, nil
}

importDate, _ := time.Parse(time.RFC3339, cert.ImportDate)
// Check if newestCert is empty, if it is set it to the first cert in the list
if newestCert.ImportDate == "" {
Expand Down
49 changes: 49 additions & 0 deletions v2/api/certificate_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ type EnrollPFXFctArgs struct {
CertFormat string `json:"-"`
}

type EnrollPFXFctArgsV2 struct {
Stores []CertificateStore `json:"Stores,omitempty"`
CustomFriendlyName string `json:"CustomFriendlyName,omitempty"`
Password string `json:"Password"`
PopulateMissingValuesFromAD bool `json:"PopulateMissingValuesFromAD"`
// Configure the SubjectString field as the full string subject for the certificate. For example, if you don't have
// subject fields individually separated, and the subject is already in the format required by RFC5280, use the SubjectString field.
SubjectString string `json:"Subject"`

// If the certificate subject is not already in the format required by RFC5280, configure the subject fields using a CertificateSubject
// struct, and EnrollPFX will automatically compile this information into a proper subject.
Subject *CertificateSubject `json:"-"`
IncludeChain bool `json:"IncludeChain"`
RenewalCertificateId int `json:"RenewalCertificateId,omitempty"`
CertificateAuthority string `json:"CertificateAuthority"`
Timestamp string `json:"Timestamp"`
Template string `json:"Template"`
SANs *SANs `json:"SANs,omitempty"`
Metadata map[string]interface{} `json:"Metadata,omitempty"`
CertFormat string `json:"-"`
InstallIntoExistingCertificateStores bool `json:"InstallIntoExistingCertificateStores,omitempty"`
ChainOrder string `json:"ChainOrder,omitempty"`
KeyType string `json:"KeyType,omitempty"`
KeyLength int `json:"KeyLength,omitempty"`
}

// EnrollCSRFctArgs holds the function arguments used for calling the EnrollCSR method.
type EnrollCSRFctArgs struct {
CSR string
Expand Down Expand Up @@ -60,6 +86,7 @@ type GetCertificateContextArgs struct {
CommonName string // Query
Id int // Query
IncludeHasPrivateKey *bool
RequestId int
}

// DeployPFXArgs holds the function arguments used for calling the DeployPFXCertificate method.
Expand Down Expand Up @@ -122,6 +149,12 @@ type EnrollResponse struct {
CertificateInformation CertificateInformation `json:"CertificateInformation"`
}

type EnrollResponseV2 struct {
SuccessfulStores []string `json:"SuccessfulStores"`
CertificateInformation CertificateInformation `json:"CertificateInformation"`
Metadata interface{} `json:"Metadata,omitempty"`
}

// CertificateInformation contains response data from the Enroll methods.
type CertificateInformation struct {
SerialNumber string `json:"SerialNumber"`
Expand All @@ -136,6 +169,22 @@ type CertificateInformation struct {
EnrollmentContext interface{} `json:"EnrollmentContext"`
}

type CertificateInformationV2 struct {
SerialNumber string `json:"SerialNumber"`
IssuerDN string `json:"IssuerDN"`
Thumbprint string `json:"Thumbprint"`
KeyfactorId int `json:"KeyfactorId"`
Pkcs12Blob string `json:"Pkcs12Blob"`
Password interface{} `json:"Password"`
WorkflowInstanceId string `json:"WorkflowInstanceId"`
WorkflowReferenceId int `json:"WorkflowReferenceId"`
StoreIdsInvalidForRenewal []interface{} `json:"StoreIdsInvalidForRenewal"`
KeyfactorRequestId int `json:"KeyfactorRequestId"`
RequestDisposition string `json:"RequestDisposition"`
DispositionMessage string `json:"DispositionMessage"`
EnrollmentContext interface{} `json:"EnrollmentContext"`
}

// GetCertificateResponse contains the response elements returned from the GetCertificateContext method.
type GetCertificateResponse struct {
Id int `json:"Id"`
Expand Down
29 changes: 28 additions & 1 deletion v2/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,34 @@ func (c *Client) sendRequest(request *request) (*http.Response, error) {
}

resp, respErr := c.httpClient.Do(req)
if respErr != nil {

// check if context deadline exceeded
if respErr != nil && strings.Contains(respErr.Error(), "context deadline exceeded") || http.StatusRequestTimeout == resp.StatusCode {
// retry until max retries reached
sleepDuration := time.Duration(1) * time.Second
for i := 0; i < MAX_CONTEXT_DEADLINE_RETRIES; i++ {
// sleep for exponential backoff
if i > 0 {
sleepDuration = sleepDuration * 2
if sleepDuration > time.Duration(MAX_WAIT_SECONDS)*time.Second {
sleepDuration = time.Duration(MAX_WAIT_SECONDS) * time.Second
}
log.Printf("[DEBUG] %s request to %s failed with error %s, retrying in %s seconds...", request.Method, keyfactorPath, respErr.Error(), sleepDuration)
time.Sleep(sleepDuration)
}

log.Printf("[DEBUG] %s request to %s failed with error %s, retrying...", request.Method, keyfactorPath, respErr.Error())
req, reqErr = http.NewRequest(request.Method, keyfactorPath, bytes.NewBuffer(jsonByes))
if reqErr != nil {
return nil, reqErr
}
resp2, respErr2 := c.httpClient.Do(req)
if respErr2 == nil {
resp = resp2
break
}
}
} else if respErr != nil {
return nil, respErr
}
var stringMessage string
Expand Down
7 changes: 7 additions & 0 deletions v2/api/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package api

const (
MAX_ITERATIONS = 100000
MAX_WAIT_SECONDS = 30
MAX_CONTEXT_DEADLINE_RETRIES = 5
)
3 changes: 3 additions & 0 deletions v2/api/store_models.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ type CertificateStore struct {

// A Boolean that sets whether to include the private key of the certificate in the certificate store if private keys are optional for the given certificate store (true) or not (false). The default is false.
IncludePrivateKey bool `json:"IncludePrivateKey,omitempty"`

// Entry Parameters map
JobParameters map[string]string `json:"JobFields,omitempty"`
}

type ListCertificateStoresResponse struct {
Expand Down
55 changes: 55 additions & 0 deletions v2/api/workflow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package api

import (
"encoding/json"
"fmt"
)

func (c *Client) ListPendingCertificates(q map[string]string) ([]WorkflowCertificate, error) {
return c.ListWorkflowCert("Pending")
}

func (c *Client) ListDeniedCertificates(q map[string]string) ([]WorkflowCertificate, error) {
return c.ListWorkflowCert("Denied")
}

func (c *Client) ListExternalValidationPendingCertificates(q map[string]string) ([]WorkflowCertificate, error) {
return c.ListWorkflowCert("ExternalValidation")
}

func (c *Client) ListWorkflowCert(endpoint string) ([]WorkflowCertificate, error) {
// Set Keyfactor-specific headers
headers := &apiHeaders{
Headers: []StringTuple{
{"x-keyfactor-api-version", "1"},
{"x-keyfactor-requested-with", "APIClient"},
},
}
query := apiQuery{
Query: []StringTuple{},
}
query.Query = append(query.Query, StringTuple{
"pagedQuery.returnLimit", "1000",
})

keyfactorAPIStruct := &request{
Method: "GET",
Endpoint: fmt.Sprintf("Workflow/Certificates/%s", endpoint),
Headers: headers,
Query: &query,
Payload: nil,
}

resp, err := c.sendRequest(keyfactorAPIStruct)
if err != nil {
return nil, err
}

var jsonResp []WorkflowCertificate
err = json.NewDecoder(resp.Body).Decode(&jsonResp)
if err != nil {
return nil, err
}
return jsonResp, err

}
Loading

0 comments on commit 8ac97c8

Please sign in to comment.