Skip to content

Commit 57f1d00

Browse files
committed
feat(api): Added Workflow/Certificates/ endpoint
feat(api): Added enrollment V2 api endpoint feat(api): Added ability to filter certs by a given `CertRequestId`. *Note* this only works with Microsoft CA certs.
1 parent 74742aa commit 57f1d00

File tree

5 files changed

+243
-1
lines changed

5 files changed

+243
-1
lines changed

v2/api/certificate.go

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,79 @@ func (c *Client) EnrollPFX(ea *EnrollPFXFctArgs) (*EnrollResponse, error) {
9090
return jsonResp, nil
9191
}
9292

93+
func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error) {
94+
log.Println("[INFO] Enrolling PFX certificate with Keyfactor")
95+
96+
/* Ensure required inputs exist */
97+
var missingFields []string
98+
99+
// TODO: Probably a better way to express these if blocks
100+
if ea.Template == "" {
101+
missingFields = append(missingFields, "Template")
102+
}
103+
if ea.CertificateAuthority == "" {
104+
missingFields = append(missingFields, "CertificateAuthority")
105+
}
106+
if ea.CertFormat == "" {
107+
missingFields = append(missingFields, "CertFormat")
108+
}
109+
//if ea.Password == "" {
110+
// missingFields = append(missingFields, "Password")
111+
//}
112+
113+
if len(missingFields) > 0 {
114+
return nil, errors.New("Required field(s) missing: " + strings.Join(missingFields, ", "))
115+
}
116+
117+
// Set Keyfactor-specific headers
118+
headers := &apiHeaders{
119+
Headers: []StringTuple{
120+
{"x-keyfactor-api-version", "2"},
121+
{"x-keyfactor-requested-with", "APIClient"},
122+
{"x-certificateformat", ea.CertFormat},
123+
},
124+
}
125+
126+
if ea.Timestamp == "" {
127+
ea.Timestamp = getTimestamp()
128+
}
129+
130+
if ea.SubjectString == "" {
131+
if ea.Subject != nil {
132+
subject, err := createSubject(*ea.Subject)
133+
if err != nil {
134+
return nil, err
135+
}
136+
ea.SubjectString = subject
137+
} else {
138+
return nil, fmt.Errorf("subject is required to use enrollpfx(). Please configure either SubjectString or Subject")
139+
}
140+
}
141+
142+
keyfactorAPIStruct := &request{
143+
Method: "POST",
144+
Endpoint: "Enrollment/PFX",
145+
Headers: headers,
146+
Payload: &ea,
147+
}
148+
149+
resp, err := c.sendRequest(keyfactorAPIStruct)
150+
if err != nil {
151+
return nil, err
152+
}
153+
154+
jsonResp := &EnrollResponseV2{}
155+
err = json.NewDecoder(resp.Body).Decode(&jsonResp)
156+
if err != nil {
157+
return nil, err
158+
}
159+
//err = decodePKCS12Blob(jsonResp)
160+
//if err != nil {
161+
// return nil, err
162+
//}
163+
return jsonResp, nil
164+
}
165+
93166
// DownloadCertificate takes arguments for DownloadCertArgs to facilitate a call to Keyfactor
94167
// that downloads a certificate from Keyfactor.
95168
// 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
321394
// and include locations add additional data, but can be set to false if they are unneeded. A pointer to a
322395
// GetCertificateResponse structure is returned, containing the certificate context.
323396
func (c *Client) GetCertificateContext(gca *GetCertificateContextArgs) (*GetCertificateResponse, error) {
324-
if gca.Id <= 0 && gca.Thumbprint == "" && gca.CommonName == "" {
397+
if gca.Id <= 0 && gca.Thumbprint == "" && gca.CommonName == "" && gca.RequestId <= 0 {
325398
return nil, errors.New("keyfactor certificate id, common name, or thumbprint are required to get certificate")
326399
}
327400

@@ -371,6 +444,11 @@ func (c *Client) GetCertificateContext(gca *GetCertificateContextArgs) (*GetCert
371444
"pq.queryString", fmt.Sprintf(`IssuedCN -eq "%s"`, gca.CommonName),
372445
})
373446
endpoint = "Certificates"
447+
} else if (gca.Id <= 0 && gca.CommonName == "" && gca.Thumbprint == "") && gca.RequestId > 0 {
448+
query.Query = append(query.Query, StringTuple{
449+
"pq.queryString", fmt.Sprintf(`CertRequestId -eq %d`, gca.RequestId),
450+
})
451+
endpoint = "Certificates"
374452
} else {
375453
endpoint = "Certificates/" + fmt.Sprintf("%d", gca.Id)
376454
}
@@ -402,6 +480,13 @@ func (c *Client) GetCertificateContext(gca *GetCertificateContextArgs) (*GetCert
402480
if len(lCerts) > 1 {
403481
var newestCert GetCertificateResponse
404482
for _, cert := range lCerts {
483+
484+
if gca.RequestId > 0 && cert.CertRequestId == gca.RequestId {
485+
return &cert, nil
486+
} else if gca.Thumbprint == cert.Thumbprint {
487+
return &cert, nil
488+
}
489+
405490
importDate, _ := time.Parse(time.RFC3339, cert.ImportDate)
406491
// Check if newestCert is empty, if it is set it to the first cert in the list
407492
if newestCert.ImportDate == "" {

v2/api/certificate_models.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,30 @@ type EnrollPFXFctArgs struct {
3030
CertFormat string `json:"-"`
3131
}
3232

33+
type EnrollPFXFctArgsV2 struct {
34+
Stores []CertificateStore `json:"Stores,omitempty"`
35+
CustomFriendlyName string `json:"CustomFriendlyName,omitempty"`
36+
Password string `json:"Password"`
37+
PopulateMissingValuesFromAD bool `json:"PopulateMissingValuesFromAD"`
38+
// Configure the SubjectString field as the full string subject for the certificate. For example, if you don't have
39+
// subject fields individually separated, and the subject is already in the format required by RFC5280, use the SubjectString field.
40+
SubjectString string `json:"Subject"`
41+
42+
// If the certificate subject is not already in the format required by RFC5280, configure the subject fields using a CertificateSubject
43+
// struct, and EnrollPFX will automatically compile this information into a proper subject.
44+
Subject *CertificateSubject `json:"-"`
45+
IncludeChain bool `json:"IncludeChain"`
46+
RenewalCertificateId int `json:"RenewalCertificateId,omitempty"`
47+
CertificateAuthority string `json:"CertificateAuthority"`
48+
Timestamp string `json:"Timestamp"`
49+
Template string `json:"Template"`
50+
SANs *SANs `json:"SANs,omitempty"`
51+
Metadata map[string]interface{} `json:"Metadata,omitempty"`
52+
CertFormat string `json:"-"`
53+
InstallIntoExistingCertificateStores bool `json:"InstallIntoExistingCertificateStores,omitempty"`
54+
ChainOrder string `json:"ChainOrder,omitempty"`
55+
}
56+
3357
// EnrollCSRFctArgs holds the function arguments used for calling the EnrollCSR method.
3458
type EnrollCSRFctArgs struct {
3559
CSR string
@@ -60,6 +84,7 @@ type GetCertificateContextArgs struct {
6084
CommonName string // Query
6185
Id int // Query
6286
IncludeHasPrivateKey *bool
87+
RequestId int
6388
}
6489

6590
// DeployPFXArgs holds the function arguments used for calling the DeployPFXCertificate method.
@@ -122,6 +147,12 @@ type EnrollResponse struct {
122147
CertificateInformation CertificateInformation `json:"CertificateInformation"`
123148
}
124149

150+
type EnrollResponseV2 struct {
151+
SuccessfulStores []string `json:"SuccessfulStores"`
152+
CertificateInformation CertificateInformation `json:"CertificateInformation"`
153+
Metadata interface{} `json:"Metadata,omitempty"`
154+
}
155+
125156
// CertificateInformation contains response data from the Enroll methods.
126157
type CertificateInformation struct {
127158
SerialNumber string `json:"SerialNumber"`
@@ -136,6 +167,22 @@ type CertificateInformation struct {
136167
EnrollmentContext interface{} `json:"EnrollmentContext"`
137168
}
138169

170+
type CertificateInformationV2 struct {
171+
SerialNumber string `json:"SerialNumber"`
172+
IssuerDN string `json:"IssuerDN"`
173+
Thumbprint string `json:"Thumbprint"`
174+
KeyfactorId int `json:"KeyfactorId"`
175+
Pkcs12Blob string `json:"Pkcs12Blob"`
176+
Password interface{} `json:"Password"`
177+
WorkflowInstanceId string `json:"WorkflowInstanceId"`
178+
WorkflowReferenceId int `json:"WorkflowReferenceId"`
179+
StoreIdsInvalidForRenewal []interface{} `json:"StoreIdsInvalidForRenewal"`
180+
KeyfactorRequestId int `json:"KeyfactorRequestId"`
181+
RequestDisposition string `json:"RequestDisposition"`
182+
DispositionMessage string `json:"DispositionMessage"`
183+
EnrollmentContext interface{} `json:"EnrollmentContext"`
184+
}
185+
139186
// GetCertificateResponse contains the response elements returned from the GetCertificateContext method.
140187
type GetCertificateResponse struct {
141188
Id int `json:"Id"`

v2/api/constants.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package api
2+
3+
const (
4+
MAX_ITERATIONS = 100000
5+
MAX_WAIT_SECONDS = 30
6+
)

v2/api/workflow.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package api
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
7+
8+
func (c *Client) ListPendingCertificates(q map[string]string) ([]WorkflowCertificate, error) {
9+
return c.ListWorkflowCert("Pending")
10+
}
11+
12+
func (c *Client) ListDeniedCertificates(q map[string]string) ([]WorkflowCertificate, error) {
13+
return c.ListWorkflowCert("Denied")
14+
}
15+
16+
func (c *Client) ListExternalValidationPendingCertificates(q map[string]string) ([]WorkflowCertificate, error) {
17+
return c.ListWorkflowCert("ExternalValidation")
18+
}
19+
20+
func (c *Client) ListWorkflowCert(endpoint string) ([]WorkflowCertificate, error) {
21+
// Set Keyfactor-specific headers
22+
headers := &apiHeaders{
23+
Headers: []StringTuple{
24+
{"x-keyfactor-api-version", "1"},
25+
{"x-keyfactor-requested-with", "APIClient"},
26+
},
27+
}
28+
query := apiQuery{
29+
Query: []StringTuple{},
30+
}
31+
query.Query = append(query.Query, StringTuple{
32+
"pagedQuery.returnLimit", "1000",
33+
})
34+
35+
keyfactorAPIStruct := &request{
36+
Method: "GET",
37+
Endpoint: fmt.Sprintf("Workflow/Certificates/%s", endpoint),
38+
Headers: headers,
39+
Query: &query,
40+
Payload: nil,
41+
}
42+
43+
resp, err := c.sendRequest(keyfactorAPIStruct)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
var jsonResp []WorkflowCertificate
49+
err = json.NewDecoder(resp.Body).Decode(&jsonResp)
50+
if err != nil {
51+
return nil, err
52+
}
53+
return jsonResp, err
54+
55+
}

v2/api/workflow_models.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package api
2+
3+
import "time"
4+
5+
type WorkflowCertificate struct {
6+
Id int `json:"Id"`
7+
CARequestId string `json:"CARequestId"`
8+
CommonName string `json:"CommonName"`
9+
DistinguishedName string `json:"DistinguishedName"`
10+
SubmissionDate time.Time `json:"SubmissionDate"`
11+
CertificateAuthority string `json:"CertificateAuthority"`
12+
Template string `json:"Template"`
13+
Requester string `json:"Requester"`
14+
State int `json:"State"`
15+
StateString string `json:"StateString"`
16+
Metadata map[string]string `json:"Metadata"`
17+
}
18+
19+
type WorkflowActionResponse struct {
20+
Failures []struct {
21+
CARowId int `json:"CARowId"`
22+
CARequestId string `json:"CARequestId"`
23+
CAHost string `json:"CAHost"`
24+
CALogicalName string `json:"CALogicalName"`
25+
KeyfactorRequestId int `json:"KeyfactorRequestId"`
26+
Comment string `json:"Comment"`
27+
} `json:"Failures"`
28+
Denials []struct {
29+
CARowId int `json:"CARowId"`
30+
CARequestId string `json:"CARequestId"`
31+
CAHost string `json:"CAHost"`
32+
CALogicalName string `json:"CALogicalName"`
33+
KeyfactorRequestId int `json:"KeyfactorRequestId"`
34+
Comment string `json:"Comment"`
35+
} `json:"Denials"`
36+
Successes []struct {
37+
CARowId int `json:"CARowId"`
38+
CARequestId string `json:"CARequestId"`
39+
CAHost string `json:"CAHost"`
40+
CALogicalName string `json:"CALogicalName"`
41+
KeyfactorRequestId int `json:"KeyfactorRequestId"`
42+
Comment string `json:"Comment"`
43+
} `json:"Successes"`
44+
}
45+
46+
type WorkflowDenyCertificateRequest struct {
47+
Comment string `json:"Comment"`
48+
CertificateRequestIds []int `json:"CertificateRequestIds"`
49+
}

0 commit comments

Comments
 (0)