Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
858a9e8
added utility code for extracting CA from cert+chain.
joevanwanzeeleKF Feb 17, 2025
5290d04
updated keyfactor go client to 1.2, passing scopes and audience on to…
joevanwanzeeleKF Feb 17, 2025
62349f6
Update generated README
Feb 17, 2025
763b83d
updated changelog
joevanwanzeeleKF Feb 17, 2025
d07e353
Merge branch '68509_including_scopes_and_audience_for_auth' of https:…
joevanwanzeeleKF Feb 17, 2025
8b2b270
Updated to enable retreiving CA certs and chain as long as a cert iss…
joevanwanzeeleKF Feb 20, 2025
74e4f1e
Update generated README
Feb 20, 2025
3b0edd0
Merge branch '68509_including_scopes_and_audience_for_auth' of https:…
joevanwanzeeleKF Feb 20, 2025
6a2fb94
merged
joevanwanzeeleKF Feb 20, 2025
3e863c0
Merge branch 'cert_authority_retrieval' of https://github.com/Keyfact…
joevanwanzeeleKF Feb 20, 2025
5df90d8
removed unused code
joevanwanzeeleKF Feb 20, 2025
94c73ac
Updated code to prevent error when user sets api_path to ""
joevanwanzeeleKF Feb 24, 2025
9bae23e
updated to pass the metadata, dns_sans and ip_sans along with strict …
joevanwanzeeleKF Feb 26, 2025
d250c9b
updated changelog
joevanwanzeeleKF Feb 26, 2025
9b9fd2c
fixed log error when trying to parse ip sans
joevanwanzeeleKF Feb 28, 2025
b2b9088
fixed issue when passing ca names that include spaces.
joevanwanzeeleKF Mar 7, 2025
e4d9b5b
strict signing can now occur without providing DNS SANs parameter
joevanwanzeeleKF Mar 7, 2025
b2c7a25
Merge pull request #39 from Keyfactor/68509_including_scopes_and_audi…
spbsoluble Apr 3, 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
- 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.
- including dns_sans, ip_sans and metadata along with pre-generated csr sign requests

- 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