diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ce45fc9..d8c3abe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 }} \ No newline at end of file diff --git a/api/store_container.go b/api/store_container.go index 97cbf1a..cbabf14 100644 --- a/api/store_container.go +++ b/api/store_container.go @@ -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) diff --git a/api/store_models.go b/api/store_models.go index 5de4ce7..ffbc3af 100644 --- a/api/store_models.go +++ b/api/store_models.go @@ -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 { diff --git a/api/store_type.go b/api/store_type.go index 8c9ecdc..82689f4 100644 --- a/api/store_type.go +++ b/api/store_type.go @@ -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) @@ -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) diff --git a/api/template.go b/api/template.go index 43c3314..2cefd28 100644 --- a/api/template.go +++ b/api/template.go @@ -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) diff --git a/v2/api/certificate.go b/v2/api/certificate.go index e96e228..8f6c866 100644 --- a/v2/api/certificate.go +++ b/v2/api/certificate.go @@ -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: @@ -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") } @@ -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) } @@ -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 == "" { diff --git a/v2/api/certificate_models.go b/v2/api/certificate_models.go index 495928b..9ea2c29 100644 --- a/v2/api/certificate_models.go +++ b/v2/api/certificate_models.go @@ -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 @@ -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. @@ -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"` @@ -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"` diff --git a/v2/api/client.go b/v2/api/client.go index 189bb33..06e8c54 100644 --- a/v2/api/client.go +++ b/v2/api/client.go @@ -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 diff --git a/v2/api/constants.go b/v2/api/constants.go new file mode 100644 index 0000000..179698f --- /dev/null +++ b/v2/api/constants.go @@ -0,0 +1,7 @@ +package api + +const ( + MAX_ITERATIONS = 100000 + MAX_WAIT_SECONDS = 30 + MAX_CONTEXT_DEADLINE_RETRIES = 5 +) diff --git a/v2/api/store_models.go b/v2/api/store_models.go index 990f017..0472454 100644 --- a/v2/api/store_models.go +++ b/v2/api/store_models.go @@ -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 { diff --git a/v2/api/workflow.go b/v2/api/workflow.go new file mode 100644 index 0000000..7b78f07 --- /dev/null +++ b/v2/api/workflow.go @@ -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 + +} diff --git a/v2/api/workflow_models.go b/v2/api/workflow_models.go new file mode 100644 index 0000000..26cbe18 --- /dev/null +++ b/v2/api/workflow_models.go @@ -0,0 +1,49 @@ +package api + +import "time" + +type WorkflowCertificate struct { + Id int `json:"Id"` + CARequestId string `json:"CARequestId"` + CommonName string `json:"CommonName"` + DistinguishedName string `json:"DistinguishedName"` + SubmissionDate time.Time `json:"SubmissionDate"` + CertificateAuthority string `json:"CertificateAuthority"` + Template string `json:"Template"` + Requester string `json:"Requester"` + State int `json:"State"` + StateString string `json:"StateString"` + Metadata map[string]string `json:"Metadata"` +} + +type WorkflowActionResponse struct { + Failures []struct { + CARowId int `json:"CARowId"` + CARequestId string `json:"CARequestId"` + CAHost string `json:"CAHost"` + CALogicalName string `json:"CALogicalName"` + KeyfactorRequestId int `json:"KeyfactorRequestId"` + Comment string `json:"Comment"` + } `json:"Failures"` + Denials []struct { + CARowId int `json:"CARowId"` + CARequestId string `json:"CARequestId"` + CAHost string `json:"CAHost"` + CALogicalName string `json:"CALogicalName"` + KeyfactorRequestId int `json:"KeyfactorRequestId"` + Comment string `json:"Comment"` + } `json:"Denials"` + Successes []struct { + CARowId int `json:"CARowId"` + CARequestId string `json:"CARequestId"` + CAHost string `json:"CAHost"` + CALogicalName string `json:"CALogicalName"` + KeyfactorRequestId int `json:"KeyfactorRequestId"` + Comment string `json:"Comment"` + } `json:"Successes"` +} + +type WorkflowDenyCertificateRequest struct { + Comment string `json:"Comment"` + CertificateRequestIds []int `json:"CertificateRequestIds"` +}