Skip to content

Commit

Permalink
Merge 9bae23e into 3c68be1
Browse files Browse the repository at this point in the history
  • Loading branch information
joevanwanzeeleKF authored Feb 26, 2025
2 parents 3c68be1 + 9bae23e commit 53388ed
Show file tree
Hide file tree
Showing 13 changed files with 785 additions and 560 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
- 1.4.1
- Updated CA and CA chain retreival to work for CA's hosted outside of Command (EJBCA)
- Updated Keyfactor Client library to 1.2.0
- Now passing scopes and audience along with oAuth token request.

- 1.4.0
- Added support for oAuth2 authentication to Keyfactor Command.
- Included the ability to specify CA and Template via command parameters
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,13 @@ any of the paths below, use the help command with any route matching
the path pattern. Note that depending on the policy of your auth token,
you may or may not be able to access certain paths.
^ca(/pem)?$
^ca
Fetch a CA, CRL, CA Chain, or non-revoked certificate.
pass "ca=<ca name>" to retrieve them for a CA other than the one set in the configuration.
^ca_chain(/pem)?$
^ca_chain
Fetch a CA, CRL, CA Chain, or non-revoked certificate.
pass "ca=<ca name>" to retrieve them for a CA other than the one set in the configuration.
^certs/?$
Use with the "list" command to display the list of certificate serial numbers for certificates managed by this secrets engine.
Expand Down Expand Up @@ -396,7 +398,7 @@ Here is a table of the available configuration paramaters
| **token_url** | string | no[^3] | | oAuth authentication: Endpoint for retreiving the authentication token |
| **access_token** | string | no | | oAuth access token, if retrieved outside the context of the plugin |
| **scopes** | []string (comma separated list) | no | | the defined scopes to apply to the retreived token in the oAuth authorization flow. If not provided, all available scopes for the service account will be assigned to the token upon authentication |
| **audience** | []string (comma seperated list) | no | | the OpenID Connect v1.0 or oAuth v2.0 token audience |
| **audience** | string | no | | the OpenID Connect v1.0 or oAuth v2.0 token audience |
| **skip_verify** | bool | no | _false_ | set this to true to skip checking the CRL list of the HTTPS endpoint |
| **command_cert_path** | string | no | | set this value to the local path of the CA cert if it is untrusted by the client and skip_verify is false

Expand Down Expand Up @@ -617,10 +619,10 @@ instance of the plugin is named "keyfactor".

### Read CA cert

`vault read keyfactor/ca`
`vault read keyfactor/ca ca=<ca name>`

### Read CA chain

`vault read keyfactor/ca_chain`
`vault read keyfactor/ca_chain ca=<ca name>`


142 changes: 2 additions & 140 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,11 @@ package kfbackend

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"sync"
"time"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
Expand Down Expand Up @@ -110,6 +105,8 @@ func (b *keyfactorBackend) getClient(ctx context.Context, s logical.Storage) (*k
defer b.configLock.RUnlock()

if b.client != nil {
b.Logger().Debug("closing idle connections before returning existing client")
b.client.httpClient.CloseIdleConnections()
return b.client, nil
}

Expand All @@ -129,141 +126,6 @@ func (b *keyfactorBackend) getClient(ctx context.Context, s logical.Storage) (*k
return b.client, nil
}

// Handle interface with Keyfactor API to enroll a certificate with given content
func (b *keyfactorBackend) submitCSR(ctx context.Context, req *logical.Request, csr string, caName string, templateName string, metaDataJson string) ([]string, string, error) {
config, err := b.fetchConfig(ctx, req.Storage)
if err != nil {
return nil, "", err
}
if config == nil {
return nil, "", errors.New("configuration is empty")
}

location, _ := time.LoadLocation("UTC")
t := time.Now().In(location)
time := t.Format("2006-01-02T15:04:05")

// get client
client, err := b.getClient(ctx, req.Storage)
if err != nil {
return nil, "", fmt.Errorf("error getting client: %w", err)
}

b.Logger().Debug("Closing idle connections")
client.httpClient.CloseIdleConnections()

// build request parameter structure

url := config.KeyfactorUrl + "/" + config.CommandAPIPath + "/Enrollment/CSR"
b.Logger().Debug("url: " + url)
bodyContent := "{\"CSR\": \"" + csr + "\",\"CertificateAuthority\":\"" + caName + "\",\"IncludeChain\": true, \"Metadata\": " + metaDataJson + ", \"Timestamp\": \"" + time + "\",\"Template\": \"" + templateName + "\",\"SANs\": {}}"
payload := strings.NewReader(bodyContent)
b.Logger().Debug("body: " + bodyContent)
httpReq, err := http.NewRequest("POST", url, payload)

if err != nil {
b.Logger().Info("Error forming request: {{err}}", err)
}

httpReq.Header.Add("x-keyfactor-requested-with", "APIClient")
httpReq.Header.Add("content-type", "application/json")
httpReq.Header.Add("x-certificateformat", "PEM")

// Send request and check status

b.Logger().Debug("About to connect to " + config.KeyfactorUrl + "for csr submission")
res, err := client.httpClient.Do(httpReq)
if err != nil {
b.Logger().Info("CSR Enrollment failed: {{err}}", err.Error())
return nil, "", err
}
if res.StatusCode != 200 {
b.Logger().Error("CSR Enrollment failed: server returned" + fmt.Sprint(res.StatusCode))
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
b.Logger().Error("Error response: " + string(body[:]))
return nil, "", fmt.Errorf("CSR Enrollment request failed with status code %d and error: "+string(body[:]), res.StatusCode)
}

// Read response and return certificate and key

defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
b.Logger().Error("Error reading response: {{err}}", err)
return nil, "", err
}

// Parse response
var r map[string]interface{}
json.Unmarshal(body, &r)
b.Logger().Debug("response = ", r)

inner := r["CertificateInformation"].(map[string]interface{})
certI := inner["Certificates"].([]interface{})
certs := make([]string, len(certI))
for i, v := range certI {
certs[i] = v.(string)
start := strings.Index(certs[i], "-----BEGIN CERTIFICATE-----")
certs[i] = certs[i][start:]
}
serial := inner["SerialNumber"].(string)
kfId := inner["KeyfactorID"].(float64)

b.Logger().Debug("parsed response: ", certI...)

if err != nil {
b.Logger().Error("unable to parse ca_chain response", fmt.Sprint(err))
}
caEntry, err := logical.StorageEntryJSON("ca_chain/", certs[1:])
if err != nil {
b.Logger().Error("error creating ca_chain entry", err)
}

err = req.Storage.Put(ctx, caEntry)
if err != nil {
b.Logger().Error("error storing the ca_chain locally", err)
}

key := "certs/" + normalizeSerial(serial)

entry := &logical.StorageEntry{
Key: key,
Value: []byte(certs[0]),
}

b.Logger().Debug("cert entry.Value = ", string(entry.Value))

err = req.Storage.Put(ctx, entry)
if err != nil {
return nil, "", errwrap.Wrapf("unable to store certificate locally: {{err}}", err)
}

kfIdEntry, err := logical.StorageEntryJSON("kfId/"+normalizeSerial(serial), kfId)
if err != nil {
return nil, "", err
}

err = req.Storage.Put(ctx, kfIdEntry)
if err != nil {
return nil, "", errwrap.Wrapf("unable to store the keyfactor ID for the certificate locally: {{err}}", err)
}

return certs, serial, nil
}

const keyfactorHelp = `
The Keyfactor backend is a pki service that issues and manages certificates.
`

func (b *keyfactorBackend) isValidJSON(str string) bool {
var js json.RawMessage
err := json.Unmarshal([]byte(str), &js)
if err != nil {
b.Logger().Debug(err.Error())
return false
} else {
b.Logger().Debug("the metadata was able to be parsed as valid JSON")
return true
}
}
Loading

0 comments on commit 53388ed

Please sign in to comment.