Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1567302
chore(deps): Bump `keyfactor-auth-client-go` to `v1.1.2`
spbsoluble Jan 16, 2025
278bfdc
chore(deps): ```
spbsoluble Jan 16, 2025
0bac539
chore(deps): Bump `github.com/Keyfactor/keyfactor-auth-client-go` to …
spbsoluble Jan 16, 2025
d897eea
chore(deps): Bump `github.com/Keyfactor/keyfactor-auth-client-go` to …
spbsoluble Jan 17, 2025
f50e0a7
fix(certs): When creating subject escape any values that main contain…
spbsoluble Mar 3, 2025
a6dff6f
Expand secret type (#47)
doebrowsk Sep 21, 2025
ce88abe
Merge branch 'v3' into release-v3.2
spbsoluble Sep 21, 2025
1bc7e47
Merge pull request #48 from Keyfactor/release-v3.2
spbsoluble Sep 21, 2025
8dc5fd1
fix(certs): For V2 PFX enrollments don't require `subject` and update…
spbsoluble Sep 23, 2025
de70b30
feat(certs): Add fields `AdditionalEnrollmentFields`, `AlternativeKey…
spbsoluble Sep 23, 2025
2612c4c
feat(enrollmentpatterns): Add `/EnrollmentPattern` models and endpoints
spbsoluble Sep 23, 2025
b109189
feat(certficates): Add `OwnerRoleId,OwnerRoleName,AltKeyAlgorithm,Alt…
spbsoluble Sep 24, 2025
5175bb8
fix(stores): Change `Password` type from `UpdateStorePasswordConfig` …
spbsoluble Sep 24, 2025
bb177bc
fix(certs): Check that `Template` and `EnrollmentPattern` are not bot…
spbsoluble Sep 24, 2025
02ac064
fix(certs): Add `ChangeCertificateOwnerRole`
spbsoluble Sep 24, 2025
c2ae7d3
feat(certs): Add the following fields to `EnrollCSRFctArgs`: `Private…
spbsoluble Sep 25, 2025
f82208c
feat(certs): Add base64 response from `DownloadCertificate`
spbsoluble Sep 28, 2025
6868250
fix(patterns): Fix enrollmentpattern models
spbsoluble Oct 1, 2025
8019d9d
Merge pull request #49 from Keyfactor/v25_enrollment_updates
spbsoluble Oct 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 196 additions & 35 deletions v3/api/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error)
var missingFields []string

// TODO: Probably a better way to express these if blocks
if ea.Template == "" {
missingFields = append(missingFields, "Template")
if ea.Template == "" && ea.EnrollmentPatternId == 0 {
missingFields = append(missingFields, "Template or EnrollmentPatternId")
}
if ea.CertificateAuthority == "" {
missingFields = append(missingFields, "CertificateAuthority")
Expand Down Expand Up @@ -151,7 +151,11 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error)
}
ea.SubjectString = subject
} else {
return nil, fmt.Errorf("subject is required to use enrollpfx(). Please configure either SubjectString or Subject")
log.Println("[DEBUG] EnrollPFXV2: Subject is nil checks if there are SANs")
if ea.SANs == nil || (len(ea.SANs.DNS) == 0 && len(ea.SANs.URI) == 0 && len(ea.SANs.IP4) == 0 &&
len(ea.SANs.IP6) == 0) {
return nil, fmt.Errorf("subject or subject alternative names are required to use enrollpfx(). Please configure either SubjectString or Subject or SANs")
}
}
}

Expand Down Expand Up @@ -191,12 +195,16 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error)
// Returns:
// - Leaf certificate
// - Certificate chain
// - Raw certificate data (as base64 string, if applicable)
// - Error
func (c *Client) DownloadCertificate(
certId int,
thumbprint string,
serialNumber string,
issuerDn string,
) (*x509.Certificate, []*x509.Certificate, error) {
collectionId int,
certificateFormat string,
) (*x509.Certificate, []*x509.Certificate, *string, error) {
log.Println("[INFO] Downloading certificate")

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

if !validInput {
return nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to dowload certificate")
return nil, nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to dowload certificate")
}

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

query := apiQuery{
Query: []StringTuple{},
}
if collectionId > 0 {
log.Println("[DEBUG] RecoverCertificate: Collection ID:", collectionId)
query.Query = append(
query.Query, StringTuple{
"collectionId", fmt.Sprintf("%d", collectionId),
},
)
log.Println("[DEBUG] RecoverCertificate: Query:", query)
}

// Set Keyfactor-specific headers
switch certificateFormat {
case "CER", "CRT", "DER", "PEM":
// do nothing these are valid formats
break
default:
// if not specified or invalid format then default to P7B
certificateFormat = "P7B"
}
headers := &apiHeaders{
Headers: []StringTuple{
{"x-keyfactor-api-version", "1"},
{"x-keyfactor-requested-with", "APIClient"},
{"x-certificateformat", "P7B"},
{"x-certificateformat", certificateFormat},
},
}

Expand All @@ -242,17 +271,18 @@ func (c *Client) DownloadCertificate(
Endpoint: "Certificates/Download",
Headers: headers,
Payload: payload,
Query: &query,
}

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

jsonResp := &downloadCertificateResponse{}
err = json.NewDecoder(resp.Body).Decode(&jsonResp)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
//buf, err := base64.StdEncoding.DecodeString(jsonResp.Content)
//if err != nil {
Expand All @@ -266,17 +296,17 @@ func (c *Client) DownloadCertificate(

certs, p7bErr := ConvertBase64P7BtoCertificates(jsonResp.Content)
if p7bErr != nil {
return nil, nil, p7bErr
return nil, nil, &jsonResp.Content, p7bErr
}

var leaf *x509.Certificate
if len(certs) > 1 {
//leaf is last cert in chain
leaf = certs[0] // First cert in chain is the leaf
return leaf, certs, nil
return leaf, certs, &jsonResp.Content, nil
}

return certs[0], nil, nil
return certs[0], nil, &jsonResp.Content, nil
}

// EnrollCSR takes arguments for EnrollCSRFctArgs to enroll a passed Certificate Signing
Expand Down Expand Up @@ -644,7 +674,8 @@ func (c *Client) RecoverCertificate(
issuerDn string,
password string,
collectionId int,
) (interface{}, *x509.Certificate, []*x509.Certificate, error) {
certificateFormat string,
) (interface{}, *x509.Certificate, []*x509.Certificate, *string, error) {
log.Println("[DEBUG] Enter RecoverCertificate")
log.Println("[INFO] Recovering certificate ID:", certId)
/* The download certificate endpoint requires one of the following to retrieve a cert:
Expand All @@ -654,6 +685,9 @@ func (c *Client) RecoverCertificate(

Check for this input
*/
if certificateFormat == "" {
certificateFormat = "PFX"
}
validInput := false
if certId != 0 {
validInput = true
Expand All @@ -665,12 +699,12 @@ func (c *Client) RecoverCertificate(

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

if password == "" {
return nil, nil, nil, fmt.Errorf("password required to recover private key with certificate")
return nil, nil, nil, nil, fmt.Errorf("password required to recover private key with certificate")
}

rca := &recoverCertArgs{
Expand All @@ -688,7 +722,7 @@ func (c *Client) RecoverCertificate(
Headers: []StringTuple{
{"x-keyfactor-api-version", "1"},
{"x-keyfactor-requested-with", "APIClient"},
{"x-certificateformat", "PFX"},
{"x-certificateformat", certificateFormat},
},
}

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

jsonResp := &recoverCertResponse{}
log.Println("[DEBUG] RecoverCertificate: Decoding response")
err = json.NewDecoder(resp.Body).Decode(&jsonResp)
if err != nil {
log.Println("[ERROR] RecoverCertificate: Error decoding response from Keyfactor Command", err.Error())
return nil, nil, nil, err
return nil, nil, nil, nil, err
}

log.Println("[DEBUG] RecoverCertificate: Decoding PFX")
pfxDer, err := base64.StdEncoding.DecodeString(jsonResp.PFX)
if err != nil {
log.Println("[ERROR] RecoverCertificate: Error decoding PFX", err.Error())
return nil, nil, nil, err
switch certificateFormat {
case "PFX", "pfx", "pkcs12", "p12", "jks", "JKS":
log.Println("[DEBUG] RecoverCertificate: decoding `PFX` response field")
pfxDer := jsonResp.PFX
if pfxDer == "" {
log.Println("[ERROR] RecoverCertificate: Error decoding PFX", err.Error())
return nil, nil, nil, &pfxDer, fmt.Errorf("pfx field in response is empty")
}
log.Println("[INFO] Recovered certificate successfully")
log.Println("[DEBUG] RecoverCertificate returning in PFX format")
return nil, nil, nil, &pfxDer, nil
case "PEM", "pem":
log.Println("[DEBUG] RecoverCertificate: Decoding PFX")
pfxDer, dErr := base64.StdEncoding.DecodeString(jsonResp.PFX)
if dErr != nil {
log.Println("[ERROR] RecoverCertificate: Error decoding PFX", dErr.Error())
return nil, nil, nil, &jsonResp.PFX, dErr
}

log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain")
priv, leaf, chain, pErr := pkcs12.DecodeChain(pfxDer, rca.Password)
if pErr != nil {
log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", pErr.Error())
return nil, nil, nil, &jsonResp.PFX, pErr
}

log.Println("[INFO] Recovered certificate successfully")
log.Println("[DEBUG] RecoverCertificate: ", leaf, chain)
return priv, leaf, chain, &jsonResp.PFX, nil
default:
log.Println("[DEBUG] RecoverCertificate: Decoding PFX")
pfxDer, dErr := base64.StdEncoding.DecodeString(jsonResp.PFX)
if dErr != nil {
log.Println("[ERROR] RecoverCertificate: Error decoding PFX", dErr.Error())
return nil, nil, nil, &jsonResp.PFX, dErr
}

log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain")
priv, leaf, chain, pErr := pkcs12.DecodeChain(pfxDer, rca.Password)
if pErr != nil {
log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", pErr.Error())
return nil, nil, nil, &jsonResp.PFX, pErr
}

log.Println("[INFO] Recovered certificate successfully")
log.Println("[DEBUG] RecoverCertificate returning in PEM format")

var pemCerts []string

// Encode leaf certificate to PEM
pemLeaf := pem.EncodeToMemory(
&pem.Block{
Type: "CERTIFICATE",
Bytes: leaf.Raw,
},
)
pemCerts = append(pemCerts, string(pemLeaf))

// Encode chain certificates to PEM
for _, cert := range chain {
pemCert := pem.EncodeToMemory(
&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
},
)
pemCerts = append(pemCerts, string(pemCert))
}

pemData := strings.Join(pemCerts, "\n")
return priv, leaf, chain, &pemData, nil
}

}

// ChangeCertificateOwnerRole changes the certificate's owner. Users must be in the current owner's role and the new owner's role.
// If removing the owner, leave both NewRoleId and NewRoleName empty in the request.
// Calls PUT /Certificates/{id}/Owner endpoint.
func (c *Client) ChangeCertificateOwnerRole(
certificateId int,
req *OwnerRequest,
params ...*CertificateOwnerChangeParams,
) error {
log.Printf("[INFO] Changing owner of certificate with ID %d in Keyfactor", certificateId)

// Validate certificate ID
if certificateId <= 0 {
return errors.New("certificate ID must be a positive integer")
}

log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain")
priv, leaf, chain, err := pkcs12.DecodeChain(pfxDer, rca.Password)
// Set Keyfactor-specific headers
headers := &apiHeaders{
Headers: []StringTuple{
{"x-keyfactor-api-version", "1"},
{"x-keyfactor-requested-with", "APIClient"},
{"Content-Type", "application/json"},
},
}

// Build URL with query parameters
endpoint := fmt.Sprintf("Certificates/%d/Owner", certificateId)
var queryParams []string

if len(params) > 0 && params[0] != nil {
param := params[0]
if param.CollectionId != nil {
queryParams = append(queryParams, fmt.Sprintf("collectionId=%d", *param.CollectionId))
}
if param.ContainerId != nil {
queryParams = append(queryParams, fmt.Sprintf("containerId=%d", *param.ContainerId))
}
}

if len(queryParams) > 0 {
endpoint += "?" + strings.Join(queryParams, "&")
}

keyfactorAPIStruct := &request{
Method: "PUT",
Endpoint: endpoint,
Headers: headers,
Payload: req,
}

resp, err := c.sendRequest(keyfactorAPIStruct)
if err != nil {
log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", err.Error())
return nil, nil, nil, err
return err
}

// Check if the response indicates success (204 No Content expected)
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("failed to change certificate owner: HTTP %d", resp.StatusCode)
}

log.Println("[INFO] Recovered certificate successfully")
log.Println("[DEBUG] RecoverCertificate: ", leaf, chain)
return priv, leaf, chain, nil
return nil
}

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

if cs.SubjectCommonName != "" && cs.SubjectCommonName != "<null>" {
subject = "CN=" + cs.SubjectCommonName + ","
subject = "CN=" + escapeDNValue(cs.SubjectCommonName) + ","
} else {
return "", errors.New("build subject: common name required") // Common name is required!
}
if cs.SubjectOrganizationalUnit != "" && cs.SubjectOrganizationalUnit != "<null>" {
subject += "OU=" + cs.SubjectOrganizationalUnit + ","
subject += "OU=" + escapeDNValue(cs.SubjectOrganizationalUnit) + ","
}
if cs.SubjectOrganization != "" && cs.SubjectOrganization != "<null>" {
subject += "O=" + cs.SubjectOrganization + ","
subject += "O=" + escapeDNValue(cs.SubjectOrganization) + ","
}
if cs.SubjectLocality != "" && cs.SubjectLocality != "<null>" {
subject += "L=" + cs.SubjectLocality + ","
subject += "L=" + escapeDNValue(cs.SubjectLocality) + ","
}
if cs.SubjectState != "" && cs.SubjectState != "<null>" {
subject += "ST=" + cs.SubjectState + ","
subject += "ST=" + escapeDNValue(cs.SubjectState) + ","
}
if cs.SubjectCountry != "" && cs.SubjectCountry != "<null>" {
subject += "C=" + cs.SubjectCountry + ","
subject += "C=" + escapeDNValue(cs.SubjectCountry) + ","
}
subject = strings.TrimRight(subject, ",") // remove trailing comma
log.Printf("[DEBUG] createSubject(): Certificate subject created: %s\n", subject)
return subject, nil
}

// escapeDNValue ensures that a value in a DN is properly escaped if it contains special characters.
func escapeDNValue(value string) string {
// If the value contains a comma, quote it
if strings.Contains(value, ",") {
return `"` + value + `"`
}
return value
}

// validateDeployPFXArgs validates the arguments required to deploy a PFX certificate.
func validateDeployPFXArgs(dpfxa *DeployPFXArgs) error {
if dpfxa.StoreIds == nil {
Expand Down
Loading
Loading