Skip to content

Commit dae7275

Browse files
authored
Merge 8019d9d into 0fd1a96
2 parents 0fd1a96 + 8019d9d commit dae7275

File tree

7 files changed

+856
-136
lines changed

7 files changed

+856
-136
lines changed

v3/api/certificate.go

Lines changed: 196 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error)
113113
var missingFields []string
114114

115115
// TODO: Probably a better way to express these if blocks
116-
if ea.Template == "" {
117-
missingFields = append(missingFields, "Template")
116+
if ea.Template == "" && ea.EnrollmentPatternId == 0 {
117+
missingFields = append(missingFields, "Template or EnrollmentPatternId")
118118
}
119119
if ea.CertificateAuthority == "" {
120120
missingFields = append(missingFields, "CertificateAuthority")
@@ -151,7 +151,11 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error)
151151
}
152152
ea.SubjectString = subject
153153
} else {
154-
return nil, fmt.Errorf("subject is required to use enrollpfx(). Please configure either SubjectString or Subject")
154+
log.Println("[DEBUG] EnrollPFXV2: Subject is nil checks if there are SANs")
155+
if ea.SANs == nil || (len(ea.SANs.DNS) == 0 && len(ea.SANs.URI) == 0 && len(ea.SANs.IP4) == 0 &&
156+
len(ea.SANs.IP6) == 0) {
157+
return nil, fmt.Errorf("subject or subject alternative names are required to use enrollpfx(). Please configure either SubjectString or Subject or SANs")
158+
}
155159
}
156160
}
157161

@@ -191,12 +195,16 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error)
191195
// Returns:
192196
// - Leaf certificate
193197
// - Certificate chain
198+
// - Raw certificate data (as base64 string, if applicable)
199+
// - Error
194200
func (c *Client) DownloadCertificate(
195201
certId int,
196202
thumbprint string,
197203
serialNumber string,
198204
issuerDn string,
199-
) (*x509.Certificate, []*x509.Certificate, error) {
205+
collectionId int,
206+
certificateFormat string,
207+
) (*x509.Certificate, []*x509.Certificate, *string, error) {
200208
log.Println("[INFO] Downloading certificate")
201209

202210
/* The download certificate endpoint requires one of the following to retrieve a cert:
@@ -216,7 +224,7 @@ func (c *Client) DownloadCertificate(
216224
}
217225

218226
if !validInput {
219-
return nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to dowload certificate")
227+
return nil, nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to dowload certificate")
220228
}
221229

222230
payload := &downloadCertificateBody{
@@ -228,12 +236,33 @@ func (c *Client) DownloadCertificate(
228236
ChainOrder: "EndEntityFirst",
229237
}
230238

239+
query := apiQuery{
240+
Query: []StringTuple{},
241+
}
242+
if collectionId > 0 {
243+
log.Println("[DEBUG] RecoverCertificate: Collection ID:", collectionId)
244+
query.Query = append(
245+
query.Query, StringTuple{
246+
"collectionId", fmt.Sprintf("%d", collectionId),
247+
},
248+
)
249+
log.Println("[DEBUG] RecoverCertificate: Query:", query)
250+
}
251+
231252
// Set Keyfactor-specific headers
253+
switch certificateFormat {
254+
case "CER", "CRT", "DER", "PEM":
255+
// do nothing these are valid formats
256+
break
257+
default:
258+
// if not specified or invalid format then default to P7B
259+
certificateFormat = "P7B"
260+
}
232261
headers := &apiHeaders{
233262
Headers: []StringTuple{
234263
{"x-keyfactor-api-version", "1"},
235264
{"x-keyfactor-requested-with", "APIClient"},
236-
{"x-certificateformat", "P7B"},
265+
{"x-certificateformat", certificateFormat},
237266
},
238267
}
239268

@@ -242,17 +271,18 @@ func (c *Client) DownloadCertificate(
242271
Endpoint: "Certificates/Download",
243272
Headers: headers,
244273
Payload: payload,
274+
Query: &query,
245275
}
246276

247277
resp, err := c.sendRequest(keyfactorAPIStruct)
248278
if err != nil {
249-
return nil, nil, err
279+
return nil, nil, nil, err
250280
}
251281

252282
jsonResp := &downloadCertificateResponse{}
253283
err = json.NewDecoder(resp.Body).Decode(&jsonResp)
254284
if err != nil {
255-
return nil, nil, err
285+
return nil, nil, nil, err
256286
}
257287
//buf, err := base64.StdEncoding.DecodeString(jsonResp.Content)
258288
//if err != nil {
@@ -266,17 +296,17 @@ func (c *Client) DownloadCertificate(
266296

267297
certs, p7bErr := ConvertBase64P7BtoCertificates(jsonResp.Content)
268298
if p7bErr != nil {
269-
return nil, nil, p7bErr
299+
return nil, nil, &jsonResp.Content, p7bErr
270300
}
271301

272302
var leaf *x509.Certificate
273303
if len(certs) > 1 {
274304
//leaf is last cert in chain
275305
leaf = certs[0] // First cert in chain is the leaf
276-
return leaf, certs, nil
306+
return leaf, certs, &jsonResp.Content, nil
277307
}
278308

279-
return certs[0], nil, nil
309+
return certs[0], nil, &jsonResp.Content, nil
280310
}
281311

282312
// EnrollCSR takes arguments for EnrollCSRFctArgs to enroll a passed Certificate Signing
@@ -644,7 +674,8 @@ func (c *Client) RecoverCertificate(
644674
issuerDn string,
645675
password string,
646676
collectionId int,
647-
) (interface{}, *x509.Certificate, []*x509.Certificate, error) {
677+
certificateFormat string,
678+
) (interface{}, *x509.Certificate, []*x509.Certificate, *string, error) {
648679
log.Println("[DEBUG] Enter RecoverCertificate")
649680
log.Println("[INFO] Recovering certificate ID:", certId)
650681
/* The download certificate endpoint requires one of the following to retrieve a cert:
@@ -654,6 +685,9 @@ func (c *Client) RecoverCertificate(
654685
655686
Check for this input
656687
*/
688+
if certificateFormat == "" {
689+
certificateFormat = "PFX"
690+
}
657691
validInput := false
658692
if certId != 0 {
659693
validInput = true
@@ -665,12 +699,12 @@ func (c *Client) RecoverCertificate(
665699

666700
if !validInput {
667701
log.Println("[ERROR] RecoverCertificate: certID, thumbprint, or serial number AND issuer DN required to download certificate")
668-
return nil, nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to download certificate")
702+
return nil, nil, nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to download certificate")
669703
}
670704
log.Println("[DEBUG] RecoverCertificate: Valid input")
671705

672706
if password == "" {
673-
return nil, nil, nil, fmt.Errorf("password required to recover private key with certificate")
707+
return nil, nil, nil, nil, fmt.Errorf("password required to recover private key with certificate")
674708
}
675709

676710
rca := &recoverCertArgs{
@@ -688,7 +722,7 @@ func (c *Client) RecoverCertificate(
688722
Headers: []StringTuple{
689723
{"x-keyfactor-api-version", "1"},
690724
{"x-keyfactor-requested-with", "APIClient"},
691-
{"x-certificateformat", "PFX"},
725+
{"x-certificateformat", certificateFormat},
692726
},
693727
}
694728

@@ -719,65 +753,192 @@ func (c *Client) RecoverCertificate(
719753
resp, err := c.sendRequest(keyfactorAPIStruct)
720754
if err != nil {
721755
log.Println("[ERROR] RecoverCertificate: Error recovering certificate from Keyfactor Command", err.Error())
722-
return nil, nil, nil, err
756+
return nil, nil, nil, nil, err
723757
}
724758

725759
jsonResp := &recoverCertResponse{}
726760
log.Println("[DEBUG] RecoverCertificate: Decoding response")
727761
err = json.NewDecoder(resp.Body).Decode(&jsonResp)
728762
if err != nil {
729763
log.Println("[ERROR] RecoverCertificate: Error decoding response from Keyfactor Command", err.Error())
730-
return nil, nil, nil, err
764+
return nil, nil, nil, nil, err
731765
}
732766

733-
log.Println("[DEBUG] RecoverCertificate: Decoding PFX")
734-
pfxDer, err := base64.StdEncoding.DecodeString(jsonResp.PFX)
735-
if err != nil {
736-
log.Println("[ERROR] RecoverCertificate: Error decoding PFX", err.Error())
737-
return nil, nil, nil, err
767+
switch certificateFormat {
768+
case "PFX", "pfx", "pkcs12", "p12", "jks", "JKS":
769+
log.Println("[DEBUG] RecoverCertificate: decoding `PFX` response field")
770+
pfxDer := jsonResp.PFX
771+
if pfxDer == "" {
772+
log.Println("[ERROR] RecoverCertificate: Error decoding PFX", err.Error())
773+
return nil, nil, nil, &pfxDer, fmt.Errorf("pfx field in response is empty")
774+
}
775+
log.Println("[INFO] Recovered certificate successfully")
776+
log.Println("[DEBUG] RecoverCertificate returning in PFX format")
777+
return nil, nil, nil, &pfxDer, nil
778+
case "PEM", "pem":
779+
log.Println("[DEBUG] RecoverCertificate: Decoding PFX")
780+
pfxDer, dErr := base64.StdEncoding.DecodeString(jsonResp.PFX)
781+
if dErr != nil {
782+
log.Println("[ERROR] RecoverCertificate: Error decoding PFX", dErr.Error())
783+
return nil, nil, nil, &jsonResp.PFX, dErr
784+
}
785+
786+
log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain")
787+
priv, leaf, chain, pErr := pkcs12.DecodeChain(pfxDer, rca.Password)
788+
if pErr != nil {
789+
log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", pErr.Error())
790+
return nil, nil, nil, &jsonResp.PFX, pErr
791+
}
792+
793+
log.Println("[INFO] Recovered certificate successfully")
794+
log.Println("[DEBUG] RecoverCertificate: ", leaf, chain)
795+
return priv, leaf, chain, &jsonResp.PFX, nil
796+
default:
797+
log.Println("[DEBUG] RecoverCertificate: Decoding PFX")
798+
pfxDer, dErr := base64.StdEncoding.DecodeString(jsonResp.PFX)
799+
if dErr != nil {
800+
log.Println("[ERROR] RecoverCertificate: Error decoding PFX", dErr.Error())
801+
return nil, nil, nil, &jsonResp.PFX, dErr
802+
}
803+
804+
log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain")
805+
priv, leaf, chain, pErr := pkcs12.DecodeChain(pfxDer, rca.Password)
806+
if pErr != nil {
807+
log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", pErr.Error())
808+
return nil, nil, nil, &jsonResp.PFX, pErr
809+
}
810+
811+
log.Println("[INFO] Recovered certificate successfully")
812+
log.Println("[DEBUG] RecoverCertificate returning in PEM format")
813+
814+
var pemCerts []string
815+
816+
// Encode leaf certificate to PEM
817+
pemLeaf := pem.EncodeToMemory(
818+
&pem.Block{
819+
Type: "CERTIFICATE",
820+
Bytes: leaf.Raw,
821+
},
822+
)
823+
pemCerts = append(pemCerts, string(pemLeaf))
824+
825+
// Encode chain certificates to PEM
826+
for _, cert := range chain {
827+
pemCert := pem.EncodeToMemory(
828+
&pem.Block{
829+
Type: "CERTIFICATE",
830+
Bytes: cert.Raw,
831+
},
832+
)
833+
pemCerts = append(pemCerts, string(pemCert))
834+
}
835+
836+
pemData := strings.Join(pemCerts, "\n")
837+
return priv, leaf, chain, &pemData, nil
838+
}
839+
840+
}
841+
842+
// ChangeCertificateOwnerRole changes the certificate's owner. Users must be in the current owner's role and the new owner's role.
843+
// If removing the owner, leave both NewRoleId and NewRoleName empty in the request.
844+
// Calls PUT /Certificates/{id}/Owner endpoint.
845+
func (c *Client) ChangeCertificateOwnerRole(
846+
certificateId int,
847+
req *OwnerRequest,
848+
params ...*CertificateOwnerChangeParams,
849+
) error {
850+
log.Printf("[INFO] Changing owner of certificate with ID %d in Keyfactor", certificateId)
851+
852+
// Validate certificate ID
853+
if certificateId <= 0 {
854+
return errors.New("certificate ID must be a positive integer")
738855
}
739856

740-
log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain")
741-
priv, leaf, chain, err := pkcs12.DecodeChain(pfxDer, rca.Password)
857+
// Set Keyfactor-specific headers
858+
headers := &apiHeaders{
859+
Headers: []StringTuple{
860+
{"x-keyfactor-api-version", "1"},
861+
{"x-keyfactor-requested-with", "APIClient"},
862+
{"Content-Type", "application/json"},
863+
},
864+
}
865+
866+
// Build URL with query parameters
867+
endpoint := fmt.Sprintf("Certificates/%d/Owner", certificateId)
868+
var queryParams []string
869+
870+
if len(params) > 0 && params[0] != nil {
871+
param := params[0]
872+
if param.CollectionId != nil {
873+
queryParams = append(queryParams, fmt.Sprintf("collectionId=%d", *param.CollectionId))
874+
}
875+
if param.ContainerId != nil {
876+
queryParams = append(queryParams, fmt.Sprintf("containerId=%d", *param.ContainerId))
877+
}
878+
}
879+
880+
if len(queryParams) > 0 {
881+
endpoint += "?" + strings.Join(queryParams, "&")
882+
}
883+
884+
keyfactorAPIStruct := &request{
885+
Method: "PUT",
886+
Endpoint: endpoint,
887+
Headers: headers,
888+
Payload: req,
889+
}
890+
891+
resp, err := c.sendRequest(keyfactorAPIStruct)
742892
if err != nil {
743-
log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", err.Error())
744-
return nil, nil, nil, err
893+
return err
894+
}
895+
896+
// Check if the response indicates success (204 No Content expected)
897+
if resp.StatusCode != http.StatusNoContent {
898+
return fmt.Errorf("failed to change certificate owner: HTTP %d", resp.StatusCode)
745899
}
746900

747-
log.Println("[INFO] Recovered certificate successfully")
748-
log.Println("[DEBUG] RecoverCertificate: ", leaf, chain)
749-
return priv, leaf, chain, nil
901+
return nil
750902
}
751903

752904
// createSubject builds the certificate subject string from a passed CertificateSubject argument.
753905
func createSubject(cs CertificateSubject) (string, error) {
754906
var subject string
755907

756908
if cs.SubjectCommonName != "" && cs.SubjectCommonName != "<null>" {
757-
subject = "CN=" + cs.SubjectCommonName + ","
909+
subject = "CN=" + escapeDNValue(cs.SubjectCommonName) + ","
758910
} else {
759911
return "", errors.New("build subject: common name required") // Common name is required!
760912
}
761913
if cs.SubjectOrganizationalUnit != "" && cs.SubjectOrganizationalUnit != "<null>" {
762-
subject += "OU=" + cs.SubjectOrganizationalUnit + ","
914+
subject += "OU=" + escapeDNValue(cs.SubjectOrganizationalUnit) + ","
763915
}
764916
if cs.SubjectOrganization != "" && cs.SubjectOrganization != "<null>" {
765-
subject += "O=" + cs.SubjectOrganization + ","
917+
subject += "O=" + escapeDNValue(cs.SubjectOrganization) + ","
766918
}
767919
if cs.SubjectLocality != "" && cs.SubjectLocality != "<null>" {
768-
subject += "L=" + cs.SubjectLocality + ","
920+
subject += "L=" + escapeDNValue(cs.SubjectLocality) + ","
769921
}
770922
if cs.SubjectState != "" && cs.SubjectState != "<null>" {
771-
subject += "ST=" + cs.SubjectState + ","
923+
subject += "ST=" + escapeDNValue(cs.SubjectState) + ","
772924
}
773925
if cs.SubjectCountry != "" && cs.SubjectCountry != "<null>" {
774-
subject += "C=" + cs.SubjectCountry + ","
926+
subject += "C=" + escapeDNValue(cs.SubjectCountry) + ","
775927
}
776928
subject = strings.TrimRight(subject, ",") // remove trailing comma
777929
log.Printf("[DEBUG] createSubject(): Certificate subject created: %s\n", subject)
778930
return subject, nil
779931
}
780932

933+
// escapeDNValue ensures that a value in a DN is properly escaped if it contains special characters.
934+
func escapeDNValue(value string) string {
935+
// If the value contains a comma, quote it
936+
if strings.Contains(value, ",") {
937+
return `"` + value + `"`
938+
}
939+
return value
940+
}
941+
781942
// validateDeployPFXArgs validates the arguments required to deploy a PFX certificate.
782943
func validateDeployPFXArgs(dpfxa *DeployPFXArgs) error {
783944
if dpfxa.StoreIds == nil {

0 commit comments

Comments
 (0)