diff --git a/CHANGELOG.md b/CHANGELOG.md index 30d65df..dc47006 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index d903159..fbb5cf4 100644 --- a/README.md +++ b/README.md @@ -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=" 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=" 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. @@ -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 @@ -617,10 +619,10 @@ instance of the plugin is named "keyfactor". ### Read CA cert -`vault read keyfactor/ca` +`vault read keyfactor/ca ca=` ### Read CA chain -`vault read keyfactor/ca_chain` +`vault read keyfactor/ca_chain ca=` diff --git a/backend.go b/backend.go index 4b4455a..bdab3b1 100644 --- a/backend.go +++ b/backend.go @@ -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" ) @@ -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 } @@ -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 - } -} diff --git a/cert_util.go b/cert_util.go index bcdeb60..9849609 100644 --- a/cert_util.go +++ b/cert_util.go @@ -25,112 +25,271 @@ import ( "io" "net" "net/http" - "net/url" "strings" "time" + "github.com/hashicorp/errwrap" "github.com/hashicorp/vault/sdk/helper/errutil" "github.com/hashicorp/vault/sdk/logical" + "go.mozilla.org/pkcs7" ) -// fetch the CA info from keyfactor -func fetchCAInfo(ctx context.Context, req *logical.Request, b *keyfactorBackend) (response *logical.Response, retErr error) { - // first we see if we have previously retreived the CA or chain +// Generate keypair and CSR +func (b *keyfactorBackend) generateCSR(cn string, ip_sans []string, dns_sans []string) (string, []byte) { + keyBytes, _ := rsa.GenerateKey(rand.Reader, 2048) + subj := pkix.Name{ + CommonName: cn, + } + rawSubj := subj.ToRDNSequence() + asn1Subj, _ := asn1.Marshal(rawSubj) + var netIPSans []net.IP + for i := range ip_sans { + netIPSans = append(netIPSans, net.ParseIP(ip_sans[i])) + } + + csrtemplate := x509.CertificateRequest{ + RawSubject: asn1Subj, + SignatureAlgorithm: x509.SHA256WithRSA, + IPAddresses: netIPSans, + DNSNames: dns_sans, + } + csrBytes, _ := x509.CreateCertificateRequest(rand.Reader, &csrtemplate, keyBytes) + csrBuf := new(bytes.Buffer) + pem.Encode(csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) + return csrBuf.String(), x509.MarshalPKCS1PrivateKey(keyBytes) +} + +// 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, dns_sans []string, ip_sans []string, metaDataJson string) ([]string, string, error) { config, err := b.fetchConfig(ctx, req.Storage) if err != nil { - return nil, err + return nil, "", err } if config == nil { - return logical.ErrorResponse("could not load configuration"), nil + return nil, "", errors.New("configuration is empty") } - caEntry, err := req.Storage.Get(ctx, "ca") + 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 logical.ErrorResponse("error fetching ca: %s", err), nil + return nil, "", fmt.Errorf("error getting client: %w", err) } - if caEntry != nil { - var r map[string]interface{} - json.Unmarshal(caEntry.Value, &r) - b.Logger().Debug("stored ca = ", r) - resp := &logical.Response{ - Data: r, + b.Logger().Debug("Closing idle connections") + client.httpClient.CloseIdleConnections() + + // build request parameter structure + + // build dns_sans payload string + dns_sans_payload_string := "" + + for _, d := range dns_sans { + if d != dns_sans[0] { + dns_sans_payload_string += "," // pre-pend a comma before next entry if not the first entry } - return resp, nil + dns_sans_payload_string = dns_sans_payload_string + fmt.Sprintf("\"%s\"", d) } + b.Logger().Debug("dns_sans payload string = %s", dns_sans_payload_string) - // if not we search certs for 'CA -eq "" AND CertState -eq "6"' - // + ip_sans_payload_string := "" + + for _, i := range ip_sans { + if i != ip_sans[0] { + ip_sans_payload_string += "," + } + ip_sans_payload_string = ip_sans_payload_string + fmt.Sprintf("\"%s\"", i) + } + b.Logger().Debug("ip_sans payload string = %s", ip_sans_payload_string) + + 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 := "\"SANs\": {" + + if dns_sans_payload_string != "" || ip_sans_payload_string != "" { + if dns_sans_payload_string != "" { + sans_payload += "\"dns\": [" + dns_sans_payload_string + "]" + } + if ip_sans_payload_string != "" { + sans_payload += ", \"ip\": [" + ip_sans_payload_string + "]" + } + } + sans_payload += "}" + + b.Logger().Trace(fmt.Sprintf("sans_payload: %s", sans_payload)) + bodyContent += ", " + sans_payload + "}" + payload := strings.NewReader(bodyContent) + + b.Logger().Debug("request body: " + bodyContent) + httpReq, err := http.NewRequest("POST", url, payload) - caId, err := getCAId(ctx, req, b) if err != nil { - return nil, errutil.InternalError{Err: fmt.Sprintf("error getting CA ID from Keyfactor: %s", err)} + b.Logger().Info("Error forming request: {{err}}", err) } - // with the certificate Id, we can retreive and store the CA certificate from Keyfactor + 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 - caCert, err := fetchCertFromKeyfactor(ctx, req, b, caId, false) + b.Logger().Debug("About to connect to " + config.KeyfactorUrl + "for csr submission") + res, err := client.httpClient.Do(httpReq) if err != nil { - return nil, errutil.InternalError{Err: fmt.Sprintf("error getting certificate from Keyfactor: %s", err)} + 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 - certBytes, _ := base64.StdEncoding.DecodeString(caCert) - certString := string(certBytes[:]) - caStorageEntry, err := logical.StorageEntryJSON("ca/", certString) + defer res.Body.Close() + body, err := io.ReadAll(res.Body) if err != nil { - b.Logger().Error("error creating ca entry", err) + b.Logger().Error("Error reading response: {{err}}", err) + return nil, "", err } - err = req.Storage.Put(ctx, caStorageEntry) + // 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...) + + caEntry, err := logical.StorageEntryJSON("ca_chain/", certs[1:]) if err != nil { - b.Logger().Error("error storing the ca locally", err) + b.Logger().Error("error creating ca_chain entry", err) } - cn := config.CertAuthority - resp := &logical.Response{ - Data: map[string]interface{}{ - cn: certString, - }, + err = req.Storage.Put(ctx, caEntry) + if err != nil { + b.Logger().Error("error storing the ca_chain locally", err) } - return resp, nil + 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 } -func fetchCaChainInfo(ctx context.Context, req *logical.Request, b *keyfactorBackend) (response *logical.Response, retErr error) { +// fetch the CA info from keyfactor +func fetchCAInfo(ctx context.Context, req *logical.Request, b *keyfactorBackend, caName string, includeChain bool) (response *logical.Response, retErr error) { + var resp *logical.Response + // first we see if we have previously retreived the CA or chain - caEntry, err := req.Storage.Get(ctx, "ca_chain") + config, err := b.fetchConfig(ctx, req.Storage) + if err != nil { + return nil, err + } + if config == nil { + return logical.ErrorResponse("could not load configuration"), nil + } + storagePath := fmt.Sprintf("ca/%s", caName) // the storage path for the ca cert is "ca/{{ca name}}" + + if includeChain { + storagePath = fmt.Sprintf("%s_chain", storagePath) // the storage path for the ca chain is "ca/{{ca name}}_chain" + } + b.Logger().Debug("local storage path = %s", storagePath) + + caEntry, err := req.Storage.Get(ctx, storagePath) + if err != nil { - return logical.ErrorResponse("error fetching ca_chain: %s", err), nil + return logical.ErrorResponse("error fetching ca: %s", err), nil } - if caEntry != nil { - var r map[string]interface{} + + if caEntry != nil { // the CA is stored locally, just need to return it + var r string json.Unmarshal(caEntry.Value, &r) - b.Logger().Debug("caChainEntry.Value = ", r) + b.Logger().Debug("stored ca = ", r) - resp := &logical.Response{ - Data: r, + if includeChain { + resp = &logical.Response{ + Data: map[string]interface{}{ + "CA Chain": r, + }, + } + } else { + resp = &logical.Response{ + Data: map[string]interface{}{ + "CA Certificate": r, + }, + } } + return resp, nil } - // if not we search certs for 'CA -eq "keyfactor-KFTRAIN-CA" AND CertState -eq "6"' - // + // it hasn't been stored locally, we we need to retreive a certificate issued by the CA + // and then extract the chain + + issued_certs, err := fetchCertIssuedByCA(ctx, req, b, caName) // we get the ID of a cert issued by the CA - caId, err := getCAId(ctx, req, b) if err != nil { - return nil, errutil.InternalError{Err: fmt.Sprintf("error getting CA ID from Keyfactor: %s", err)} + return nil, errutil.InternalError{Err: fmt.Sprintf("failed to retreive any cert issued by the CA: %s", err)} } - // with the certificate Id, we can retreive and store the CA certificate from Keyfactor + if len(issued_certs) == 0 { + return nil, fmt.Errorf("no certificates issued by %s were found", caName) + } - caCert, err := fetchCertFromKeyfactor(ctx, req, b, caId, true) + issued_cert := issued_certs[0] + + b.Logger().Trace("extracting the CA and Chain from the retreived cert.") + ca_chain, ca_cert, err := fetchChainAndCAForCert(ctx, req, b, issued_cert.ID) // we download the full cert and chain if err != nil { - return nil, errutil.InternalError{Err: fmt.Sprintf("error getting certificate from Keyfactor: %s", err)} + b.Logger().Error("error getting full chain and CA for cert: %s", err) + return nil, err } + b.Logger().Trace("extracted ca and chain from cert. chain has a length of %d \n", len(ca_chain)) - certBytes, _ := base64.StdEncoding.DecodeString(caCert) - certString := string(certBytes[:]) - caStorageEntry, err := logical.StorageEntryJSON("ca_chain/", certString) + // now we have the full cert + chain, in PEM format + + // store the CA cert locally + caStorageEntry, err := logical.StorageEntryJSON("ca/"+caName, ca_cert) if err != nil { b.Logger().Error("error creating ca entry", err) } @@ -140,121 +299,169 @@ func fetchCaChainInfo(ctx context.Context, req *logical.Request, b *keyfactorBac b.Logger().Error("error storing the ca locally", err) } - resp := &logical.Response{ - Data: map[string]interface{}{ - "CA_CHAIN": certString, - }, + ca_chain_combined := strings.Join(ca_chain, "") // store as a single PEM chain + + // store the full chain locally + caChainStorageEntry, err := logical.StorageEntryJSON("ca/"+caName+"_chain", ca_chain_combined) + if err != nil { + b.Logger().Error("error creating ca chain entry", err) + } + + err = req.Storage.Put(ctx, caChainStorageEntry) + if err != nil { + b.Logger().Error("error storing the ca chain locally", err) + } + + if includeChain { + resp = &logical.Response{ + Data: map[string]interface{}{ + "CA Chain": ca_chain, + }, + } + } else { + resp = &logical.Response{ + Data: map[string]interface{}{ + "CA Certificate": ca_cert, + }, + } } return resp, nil } -func getCAId(ctx context.Context, req *logical.Request, b *keyfactorBackend) (string, error) { - config, err := b.fetchConfig(ctx, req.Storage) +// Allows fetching certificates from the backend; it handles the slightly +// separate pathing for CA and revoked certificates. +func fetchCertBySerial(ctx context.Context, req *logical.Request, prefix, serial string) (*logical.StorageEntry, error) { + var path, legacyPath string + var err error + var certEntry *logical.StorageEntry + + hyphenSerial := normalizeSerial(serial) + colonSerial := strings.Replace(strings.ToLower(serial), "-", ":", -1) + + switch { + // Revoked goes first as otherwise ca/crl get hardcoded paths which fail if + // we actually want revocation info + case strings.HasPrefix(prefix, "revoked/"): + legacyPath = "revoked/" + colonSerial + path = "revoked/" + hyphenSerial + default: + legacyPath = "certs/" + colonSerial + path = "certs/" + hyphenSerial + } + + certEntry, err = req.Storage.Get(ctx, path) if err != nil { - return "", err + return nil, errutil.InternalError{Err: fmt.Sprintf("error fetching certificate %s: %s", serial, err)} } - if config == nil { - return "", errors.New("unable to load configuration") + if certEntry != nil { + if len(certEntry.Value) == 0 { + return nil, errutil.InternalError{Err: fmt.Sprintf("returned certificate bytes for serial %s were empty", serial)} + } + return certEntry, nil + } + + // If legacyPath is unset, it's going to be a CA or CRL; return immediately + if legacyPath == "" { + return nil, nil } - if config.CertAuthority == "" { - b.Logger().Error("no value in config for CA.") - return "", nil + // Retrieve the old-style path. We disregard errors here because they + // always manifest on windows, and thus the initial check for a revoked + // cert fails would return an error when the cert isn't revoked, preventing + // the happy path from working. + certEntry, _ = req.Storage.Get(ctx, legacyPath) + if certEntry == nil { + return nil, nil + } + if len(certEntry.Value) == 0 { + return nil, errutil.InternalError{Err: fmt.Sprintf("returned certificate bytes for serial %s were empty", serial)} } - ca_name := strings.Split(config.CertAuthority, `\\`)[1] + // Update old-style paths to new-style paths + certEntry.Key = path + if err = req.Storage.Put(ctx, certEntry); err != nil { + return nil, errutil.InternalError{Err: fmt.Sprintf("error saving certificate with serial %s to new location", serial)} + } + if err = req.Storage.Delete(ctx, legacyPath); err != nil { + return nil, errutil.InternalError{Err: fmt.Sprintf("error deleting certificate with serial %s from old location", serial)} + } - // This is only needed when running as a vault extension - b.Logger().Debug("Closing idle connections") + return certEntry, nil +} + +func fetchCertIssuedByCA(ctx context.Context, req *logical.Request, b *keyfactorBackend, caName string) (KeyfactorCertResponse, error) { + // call certificates endpoint, limit results to 1, filter by CA name + config, err := b.fetchConfig(ctx, req.Storage) + if err != nil { + return nil, err + } + if config == nil { + return nil, errors.New("unable to load configuration") + } + + // get the client client, err := b.getClient(ctx, req.Storage) if err != nil { b.Logger().Error("unable to create the http client") } + // This is only needed when running as a vault extension + b.Logger().Debug("Closing idle connections") client.httpClient.CloseIdleConnections() + caName = strings.Replace(caName, " ", "%20", -1) + reqUrl := config.KeyfactorUrl + "/" + config.CommandAPIPath + "/Certificates?pq.queryString=CA%20-eq%20%22" + caName + "%20%22&ReturnLimit=1" - ca_name = url.QueryEscape(ca_name) - - //creds := config.Username + ":" + config.Password - //encCreds := b64.StdEncoding.EncodeToString([]byte(creds)) + b.Logger().Debug("url: " + reqUrl) - // Build request - - url := config.KeyfactorUrl + "/" + config.CommandAPIPath + "/Certificates?pq.queryString=CA%20-eq%20%22" + ca_name + "%22%20AND%20CertState%20-eq%20%226%22" // CertState 6 = cert - b.Logger().Debug("url: " + url) - httpReq, err := http.NewRequest("GET", url, nil) + httpReq, err := http.NewRequest("GET", reqUrl, nil) if err != nil { b.Logger().Info("Error forming request: {{err}}", err) } - //httpReq.Header.Add("x-keyfactor-requested-with", "APIClient") - httpReq.Header.Add("x-keyfactor-api-version", "1") - //httpReq.Header.Add("authorization", "Basic "+encCreds) + + httpReq.Header.Add("x-keyfactor-requested-with", "APIClient") + httpReq.Header.Add("content-type", "application/json") // Send request and check status - b.Logger().Debug("About to connect to " + config.KeyfactorUrl + "for ca retrieval") + b.Logger().Debug("About to connect to " + reqUrl + "for cert retrieval") res, err := client.httpClient.Do(httpReq) if err != nil { - b.Logger().Info("failed getting CA: {{err}}", err) - return "", err + b.Logger().Info("failed getting cert: {{err}}", err) + return nil, err } if res.StatusCode != 200 { b.Logger().Error("request failed: server returned" + fmt.Sprint(res.StatusCode)) - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - b.Logger().Info("Error reading response: {{err}}", err) - return "", err - } - b.Logger().Error("Error response = " + fmt.Sprint(body)) - return "", fmt.Errorf("error querying certificates for CA. returned status = %d\n ", res.StatusCode) + b.Logger().Error("Error response = " + fmt.Sprint(res.Body)) + return nil, fmt.Errorf("error downloading certificate. returned status = %d\n ", res.StatusCode) } // Read response and return certificate and key defer res.Body.Close() - // Parse response - var r KeyfactorCertResponse - err = json.NewDecoder(res.Body).Decode(&r) + body, err := io.ReadAll(res.Body) if err != nil { - panic(err) + b.Logger().Info("Error reading response: {{err}}", err) + return nil, err } - b.Logger().Debug("response = ", r) - return fmt.Sprintf("%d", r[0].ID), nil -} + // Parse response + var r KeyfactorCertResponse + json.Unmarshal(body, &r) + b.Logger().Debug("response = ", r) -// Generate keypair and CSR -func (b *keyfactorBackend) generateCSR(cn string, ip_sans []string, dns_sans []string) (string, []byte) { - keyBytes, _ := rsa.GenerateKey(rand.Reader, 2048) - subj := pkix.Name{ - CommonName: cn, - } - rawSubj := subj.ToRDNSequence() - asn1Subj, _ := asn1.Marshal(rawSubj) - var netIPSans []net.IP - for i := range ip_sans { - netIPSans = append(netIPSans, net.ParseIP(ip_sans[i])) + if len(r) == 0 { + return nil, fmt.Errorf("no certificates issued by CA %s found in Command. At least 1 must exist in order to retreive the CA or CA chain certificate(s)", caName) } - csrtemplate := x509.CertificateRequest{ - RawSubject: asn1Subj, - SignatureAlgorithm: x509.SHA256WithRSA, - IPAddresses: netIPSans, - DNSNames: dns_sans, - } - csrBytes, _ := x509.CreateCertificateRequest(rand.Reader, &csrtemplate, keyBytes) - csrBuf := new(bytes.Buffer) - pem.Encode(csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) - return csrBuf.String(), x509.MarshalPKCS1PrivateKey(keyBytes) + return r, nil } -func fetchCertFromKeyfactor(ctx context.Context, req *logical.Request, b *keyfactorBackend, kfCertId string, includeChain bool) (string, error) { +func fetchChainAndCAForCert(ctx context.Context, req *logical.Request, b *keyfactorBackend, kfCertId int) ([]string, string, error) { config, err := b.fetchConfig(ctx, req.Storage) if err != nil { - return "", err + return nil, "", err } if config == nil { - return "", errors.New("unable to load configuration") + return nil, "", errors.New("unable to load configuration") } // get the client @@ -266,142 +473,130 @@ func fetchCertFromKeyfactor(ctx context.Context, req *logical.Request, b *keyfac b.Logger().Debug("Closing idle connections") client.httpClient.CloseIdleConnections() - include := "false" - if includeChain { - include = "true" - } - // Build request - url := config.KeyfactorUrl + "Certificates/Download" - b.Logger().Debug("url: " + url) - bodyContent := fmt.Sprintf(`{"CertID": %s, "IncludeChain": %s }`, kfCertId, include) + reqUrl := config.KeyfactorUrl + "/" + config.CommandAPIPath + "/Certificates/Download" + b.Logger().Debug("url: " + reqUrl) + bodyContent := fmt.Sprintf(`{"CertID": %d, "IncludeChain": true, "ChainOrder": "endentityfirst" }`, kfCertId) payload := strings.NewReader(bodyContent) b.Logger().Debug("body: " + bodyContent) - httpReq, err := http.NewRequest("POST", url, payload) + httpReq, err := http.NewRequest("POST", reqUrl, payload) if err != nil { - b.Logger().Info("Error forming request: {{err}}", err) + b.Logger().Info("Error forming request: %s", err) } httpReq.Header.Add("x-keyfactor-requested-with", "APIClient") httpReq.Header.Add("content-type", "application/json") - httpReq.Header.Add("x-certificateformat", "PEM") + httpReq.Header.Add("x-certificateformat", "P7B") // Send request and check status b.Logger().Debug("About to connect to " + config.KeyfactorUrl + "for cert retrieval") res, err := client.httpClient.Do(httpReq) if err != nil { - b.Logger().Info("failed getting cert: {{err}}", err) - return "", err + b.Logger().Info(fmt.Sprintf("failed getting cert: %s", err)) + return nil, "", err } if res.StatusCode != 200 { b.Logger().Error("request failed: server returned" + fmt.Sprint(res.StatusCode)) b.Logger().Error("Error response = " + fmt.Sprint(res.Body)) - return "", fmt.Errorf("error downloading certificate. returned status = %d\n ", res.StatusCode) + return nil, "", fmt.Errorf("error downloading certificate. returned status = %d\n ", res.StatusCode) } // Read response and return certificate and key defer res.Body.Close() + // Parse response body, err := io.ReadAll(res.Body) if err != nil { - b.Logger().Info("Error reading response: {{err}}", err) - return "", err + b.Logger().Info("Error reading response: %s", err) + return nil, "", err } - - // Parse response var r KeyfactorCertDownloadResponse json.Unmarshal(body, &r) b.Logger().Debug("response = ", r) - return r.Content, nil - -} + certs, p7bErr := ConvertBase64P7BtoCertificates(r.Content) + if p7bErr != nil { + return nil, "", p7bErr + } -// Allows fetching certificates from the backend; it handles the slightly -// separate pathing for CA and revoked certificates. -func fetchCertBySerial(ctx context.Context, req *logical.Request, prefix, serial string) (*logical.StorageEntry, error) { - var path, legacyPath string - var err error - var certEntry *logical.StorageEntry + // first cert is leaf, next cert is CA, remaining certs are chain + ca_chain := certs[1:] + ca_cert := certs[1] - hyphenSerial := normalizeSerial(serial) - colonSerial := strings.Replace(strings.ToLower(serial), "-", ":", -1) + b.Logger().Trace(fmt.Sprintf("the chain contains %d certs", len(ca_chain))) - switch { - // Revoked goes first as otherwise ca/crl get hardcoded paths which fail if - // we actually want revocation info - case strings.HasPrefix(prefix, "revoked/"): - legacyPath = "revoked/" + colonSerial - path = "revoked/" + hyphenSerial - default: - legacyPath = "certs/" + colonSerial - path = "certs/" + hyphenSerial - } + var ca_chain_pem []string + var ca_pem string - certEntry, err = req.Storage.Get(ctx, path) - if err != nil { - return nil, errutil.InternalError{Err: fmt.Sprintf("error fetching certificate %s: %s", serial, err)} - } - if certEntry != nil { - if len(certEntry.Value) == 0 { - return nil, errutil.InternalError{Err: fmt.Sprintf("returned certificate bytes for serial %s were empty", serial)} + // Encode each certificate found in the PKCS#7 structure into PEM format. + for _, cert := range ca_chain { + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, } - return certEntry, nil + pemEncoded := pem.EncodeToMemory(pemBlock) + ca_chain_pem = append(ca_chain_pem, string(pemEncoded)) } - // If legacyPath is unset, it's going to be a CA or CRL; return immediately - if legacyPath == "" { - return nil, nil + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: ca_cert.Raw, } + caPemEncoded := pem.EncodeToMemory(pemBlock) + ca_pem = string(caPemEncoded) - // Retrieve the old-style path. We disregard errors here because they - // always manifest on windows, and thus the initial check for a revoked - // cert fails would return an error when the cert isn't revoked, preventing - // the happy path from working. - certEntry, _ = req.Storage.Get(ctx, legacyPath) - if certEntry == nil { - return nil, nil + return ca_chain_pem, ca_pem, nil +} + +func normalizeSerial(serial string) string { + return strings.Replace(strings.ToLower(serial), ":", "-", -1) +} + +// ConvertBase64P7BtoCertificates takes a base64 encoded P7B certificate string and returns a slice of *x509.Certificate. +func ConvertBase64P7BtoCertificates(base64P7B string) ([]*x509.Certificate, error) { + // Decode the base64 string to a byte slice. + decodedBytes, err := base64.StdEncoding.DecodeString(base64P7B) + if err != nil { + return nil, fmt.Errorf("error decoding base64 string: %w", err) } - if len(certEntry.Value) == 0 { - return nil, errutil.InternalError{Err: fmt.Sprintf("returned certificate bytes for serial %s were empty", serial)} + + // Parse the PKCS#7 structure. + p7, err := pkcs7.Parse(decodedBytes) + if err != nil { + return nil, fmt.Errorf("error parsing PKCS#7 data: %w", err) } - // Update old-style paths to new-style paths - certEntry.Key = path - if err = req.Storage.Put(ctx, certEntry); err != nil { - return nil, errutil.InternalError{Err: fmt.Sprintf("error saving certificate with serial %s to new location", serial)} + // Return the certificates. + return p7.Certificates, nil +} + +func ConvertBase64P7BtoPEM(base64P7B string) ([]string, error) { + // Decode the base64 string to a byte slice. + decodedBytes, err := base64.StdEncoding.DecodeString(base64P7B) + if err != nil { + return []string{}, fmt.Errorf("error decoding base64 string: %w", err) } - if err = req.Storage.Delete(ctx, legacyPath); err != nil { - return nil, errutil.InternalError{Err: fmt.Sprintf("error deleting certificate with serial %s from old location", serial)} + + // Parse the PKCS#7 structure. + p7, err := pkcs7.Parse(decodedBytes) + + if err != nil { + return []string{}, fmt.Errorf("error parsing PKCS#7 data: %w", err) } - return certEntry, nil -} + // Initialize an empty string to append the PEM encoded certificates. + var pemEncodedCerts []string -func parseOtherSANs(others []string) (map[string][]string, error) { - result := map[string][]string{} - for _, other := range others { - splitOther := strings.SplitN(other, ";", 2) - if len(splitOther) != 2 { - return nil, fmt.Errorf("expected a semicolon in other SAN %q", other) - } - splitType := strings.SplitN(splitOther[1], ":", 2) - if len(splitType) != 2 { - return nil, fmt.Errorf("expected a colon in other SAN %q", other) + // Encode each certificate found in the PKCS#7 structure into PEM format. + for _, cert := range p7.Certificates { + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, } - switch { - case strings.EqualFold(splitType[0], "utf8"): - case strings.EqualFold(splitType[0], "utf-8"): - default: - return nil, fmt.Errorf("only utf8 other SANs are supported; found non-supported type in other SAN %q", other) - } - result[splitOther[0]] = append(result[splitOther[0]], splitType[1]) + pemEncoded := pem.EncodeToMemory(pemBlock) + pemEncodedCerts = append(pemEncodedCerts, string(pemEncoded)) } - return result, nil -} - -func normalizeSerial(serial string) string { - return strings.Replace(strings.ToLower(serial), ":", "-", -1) + return pemEncodedCerts, nil } type KeyfactorCertResponse []struct { diff --git a/client.go b/client.go index d73a1cf..bd1d75e 100644 --- a/client.go +++ b/client.go @@ -88,6 +88,8 @@ func newClient(config *keyfactorConfig, b *keyfactorBackend) (*keyfactorClient, WithClientSecret(config.ClientSecret). WithTokenUrl(config.TokenUrl). WithAccessToken(config.AccessToken). + WithScopes(config.Scopes). + WithAudience(config.Audience). Authenticate() if oErr != nil { diff --git a/fields.go b/fields.go index 15ce2f5..a493d9a 100644 --- a/fields.go +++ b/fields.go @@ -28,7 +28,11 @@ func addNonCACommonFields(fields map[string]*framework.FieldSchema) map[string]* fields["dns_sans"] = &framework.FieldSchema{ Type: framework.TypeString, Description: `Comma seperated list of DNS Subject Alternative Names`, - Required: true, + } + + fields["ip_sans"] = &framework.FieldSchema{ + Type: framework.TypeString, + Description: `Comma seperated list of IP address SANs`, } fields["role"] = &framework.FieldSchema{ @@ -46,35 +50,35 @@ in the role, this may be an email address.`, Required: true, } - fields["alt_names"] = &framework.FieldSchema{ - Type: framework.TypeString, - Description: `The requested Subject Alternative Names, if any, -in a comma-delimited list. If email protection -is enabled for the role, this may contain -email addresses.`, - DisplayAttrs: &framework.DisplayAttributes{ - Name: "DNS/Email Subject Alternative Names (SANs)", - }, - } - - fields["serial_number"] = &framework.FieldSchema{ - Type: framework.TypeString, - Description: `The requested serial number, if any. If you want -more than one, specify alternative names in -the alt_names map using OID 2.5.4.5.`, - } - - fields["ttl"] = &framework.FieldSchema{ - Type: framework.TypeDurationSecond, - Description: `The requested Time To Live for the certificate; -sets the expiration date. If not specified -the role default, backend default, or system -default TTL is used, in that order. Cannot -be larger than the role max TTL.`, - DisplayAttrs: &framework.DisplayAttributes{ - Name: "TTL", - }, - } + // fields["alt_names"] = &framework.FieldSchema{ + // Type: framework.TypeString, + // Description: `The requested Subject Alternative Names, if any, + // in a comma-delimited list. If email protection + // is enabled for the role, this may contain + // email addresses.`, + // DisplayAttrs: &framework.DisplayAttributes{ + // Name: "DNS/Email Subject Alternative Names (SANs)", + // }, + // } + + // fields["serial_number"] = &framework.FieldSchema{ + // Type: framework.TypeString, + // Description: `The requested serial number, if any. If you want + // more than one, specify alternative names in + // the alt_names map using OID 2.5.4.5.`, + // } + + // fields["ttl"] = &framework.FieldSchema{ + // Type: framework.TypeDurationSecond, + // Description: `The requested Time To Live for the certificate; + // sets the expiration date. If not specified + // the role default, backend default, or system + // default TTL is used, in that order. Cannot + // be larger than the role max TTL.`, + // DisplayAttrs: &framework.DisplayAttributes{ + // Name: "TTL", + // }, + // } fields["metadata"] = &framework.FieldSchema{ Type: framework.TypeString, diff --git a/go.mod b/go.mod index 760be25..f5d39b5 100644 --- a/go.mod +++ b/go.mod @@ -5,22 +5,29 @@ go 1.23 toolchain go1.23.3 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.0.0-rc.2 - github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0 + github.com/Keyfactor/keyfactor-auth-client-go v1.2.0 github.com/hashicorp/errwrap v1.0.0 github.com/hashicorp/go-hclog v1.5.0 github.com/hashicorp/vault/api v1.1.1 github.com/hashicorp/vault/sdk v0.2.1 + go.mozilla.org/pkcs7 v0.9.0 ) require ( - github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect github.com/armon/go-metrics v0.3.3 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/cenkalti/backoff/v3 v3.0.0 // indirect github.com/fatih/color v1.13.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/protobuf v1.4.2 // indirect github.com/golang/snappy v0.0.1 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.1 // indirect github.com/hashicorp/go-immutable-radix v1.1.0 // indirect github.com/hashicorp/go-kms-wrapping/entropy v0.1.0 // indirect @@ -33,8 +40,8 @@ require ( github.com/hashicorp/go-version v1.2.0 // indirect github.com/hashicorp/golang-lru v0.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect @@ -44,15 +51,14 @@ require ( github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/oklog/run v1.0.0 // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/ryanuber/go-glob v1.0.0 // indirect - github.com/spbsoluble/go-pkcs12 v0.3.3 // indirect - go.mozilla.org/pkcs7 v0.9.0 // indirect go.uber.org/atomic v1.6.0 // indirect - golang.org/x/crypto v0.11.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/oauth2 v0.23.0 // indirect - golang.org/x/sys v0.12.0 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/oauth2 v0.25.0 // indirect + golang.org/x/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect google.golang.org/grpc v1.29.1 // indirect diff --git a/go.sum b/go.sum index c3b9ec9..3566476 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,26 @@ bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 h1:WLUIpeyv04H0RCcQHaA4TNoyrQ39Ox7V+re+iaqzTe0= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0/go.mod h1:hd8hTTIY3VmUVPRHNH7GVCHO3SHgXkJKZHReby/bnUQ= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/Keyfactor/keyfactor-auth-client-go v1.0.0-rc.2 h1:/61pAEjAhTAGtFCvAcp04qEZWV9pMYH5fxADReN2KdU= -github.com/Keyfactor/keyfactor-auth-client-go v1.0.0-rc.2/go.mod h1:UTPLARTONwfc+j1y2SjEa54gbFFCObQucHf3ubQVyDk= -github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2 h1:caLlzFCz2L4Dth/9wh+VlypFATmOMmCSQkCPKOKMxw8= -github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2/go.mod h1:Z5pSk8YFGXHbKeQ1wTzVN8A4P/fZmtAwqu3NgBHbDOs= -github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0 h1:yMChWRnnxmcgLt6kEQ3FZfteps05v/qot5KXLXxa6so= -github.com/Keyfactor/keyfactor-go-client/v3 v3.0.0/go.mod h1:HWb+S60YAALFVSfB8QuQ8ugjsjr+FHLQET0/4K7EVWw= +github.com/Keyfactor/keyfactor-auth-client-go v1.2.0 h1:uNSlyOW5Bqpi0nsOGZtOYQzN0vP/h4S4J38jtQes+OI= +github.com/Keyfactor/keyfactor-auth-client-go v1.2.0/go.mod h1:7htRcBIWn+X4fI5jaYBALSYwP84H/djN7d8y3n0ZDQ0= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -31,6 +43,8 @@ github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3 github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -50,6 +64,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20200319182547-c7ad2b866182/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= @@ -83,6 +99,8 @@ github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -107,6 +125,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -151,8 +171,6 @@ github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8 github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= -github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f/go.mod h1:euTFbi2YJgwcju3imEt919lhJKF68nN1cQPq3aA+kBE= github.com/hashicorp/vault/api v1.1.1 h1:907ld+Z9cALyvbZK2qUX9cLwvSaEQsMVQB3x2KE8+AI= github.com/hashicorp/vault/api v1.1.1/go.mod h1:29UXcn/1cLOPHQNMWA7bCz2By4PSd0VKPAydKXS5yN0= @@ -168,18 +186,23 @@ github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeY github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= +github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= @@ -234,6 +257,8 @@ github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144T github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -257,6 +282,10 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= @@ -264,8 +293,6 @@ github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvH github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= -github.com/spbsoluble/go-pkcs12 v0.3.3/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -276,8 +303,9 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= @@ -290,8 +318,8 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -313,11 +341,11 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= -golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= +golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -348,13 +376,14 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -367,8 +396,8 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -403,8 +432,9 @@ gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/path_ca.go b/path_ca.go index 42eb199..54e17ea 100644 --- a/path_ca.go +++ b/path_ca.go @@ -11,6 +11,7 @@ package kfbackend import ( "context" + "fmt" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" @@ -19,8 +20,14 @@ import ( func pathCA(b *keyfactorBackend) []*framework.Path { return []*framework.Path{ { //fetch ca - Pattern: `ca(/pem)?`, - + Pattern: `ca`, + Fields: map[string]*framework.FieldSchema{ + "ca": { + Type: framework.TypeString, + Description: pathCAFieldDesck, + Required: false, + }, + }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.ReadOperation: b.pathFetchCa, }, @@ -29,10 +36,16 @@ func pathCA(b *keyfactorBackend) []*framework.Path { HelpDescription: pathFetchCAHelpDesc, }, { // fetch ca chain - Pattern: `ca_chain(/pem)?`, - + Pattern: `ca_chain`, + Fields: map[string]*framework.FieldSchema{ + "ca": { + Type: framework.TypeString, + Description: pathCAFieldDesck, + Required: false, + }, + }, Callbacks: map[logical.Operation]framework.OperationFunc{ - logical.ReadOperation: b.pathFetchCa, + logical.ReadOperation: b.pathFetchCaChain, }, HelpSynopsis: pathFetchChainHelp, @@ -42,32 +55,35 @@ func pathCA(b *keyfactorBackend) []*framework.Path { } func (b *keyfactorBackend) pathFetchCa(ctx context.Context, req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) { - var serial string + b.Logger().Debug("fetching ca, path = " + req.Path) + b.Logger().Trace("reading CA name...") - response = &logical.Response{ - Data: map[string]interface{}{}, + caName := data.Get("ca").(string) + if caName == "" { + b.Logger().Debug("no ca passed, retreiving from config") + caName = b.cachedConfig.CertAuthority + } + b.Logger().Debug(fmt.Sprintf("ca name = %s", caName)) + if caName == "" { + return nil, fmt.Errorf("the CA name needs to be specified in the configuration, or passed along with the request") } - // Some of these need to return raw and some non-raw; - // this is basically handled by setting contentType or not. - // Errors don't cause an immediate exit, because the raw - // paths still need to return raw output. - b.Logger().Debug("fetching ca, path = " + req.Path) + return fetchCAInfo(ctx, req, b, caName, false) +} - switch { - case req.Path == "ca" || req.Path == "ca/pem": - serial = "ca" - case req.Path == "ca_chain" || req.Path == "cert/ca_chain": - serial = "ca_chain" - default: - serial = "ca" +func (b *keyfactorBackend) pathFetchCaChain(ctx context.Context, req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) { + b.Logger().Debug("fetching ca chain, path = " + req.Path) + b.Logger().Trace("reading CA name...") + caName := data.Get("ca").(string) + if caName == "" { + b.Logger().Debug("no ca passed, retreiving from config") + caName = b.cachedConfig.CertAuthority } - - if serial == "ca" { - return fetchCAInfo(ctx, req, b) + b.Logger().Debug(fmt.Sprintf("ca name = %s", caName)) + if caName == "" { + return nil, fmt.Errorf("the CA name needs to be specified in the configuration, or passed along with the request") } - - return fetchCaChainInfo(ctx, req, b) + return fetchCAInfo(ctx, req, b, caName, true) } const pathFetchCAHelp = ` @@ -78,11 +94,13 @@ const pathFetchChainHelp = ` Fetch a CA Chain. ` const pathFetchCAHelpDesc = ` -This allows Certificate Authorities to be fetched. -The "ca" command fetches the appropriate information in DER encoding. Add "/pem" to either to get PEM encoding. +This allows the Certificate Authority certificate to be fetched. +The "ca" command fetches the PEM encoded CA certificate. The CA will have to be defined in the configuration or passed along with the request. ` const pathFetchChainHelpDesc = ` -This allows the Certificate Authority chain to be fetched. -The "ca_chain" command fetches the certificate authority trust chain in PEM encoding. +This allows the Certificate Authority chain certificates to be fetched. +The "ca_chain" command fetches the PEM encoded CA certificate chain. The CA will have to be defined in the configuration or passed along with the request. ` + +const pathCAFieldDesck = `The logical CA Name as defined in Command. If not provided, we will attempt to use the CA Name stored in the configuration.` diff --git a/path_certs.go b/path_certs.go index af12f6b..47e7379 100644 --- a/path_certs.go +++ b/path_certs.go @@ -16,7 +16,6 @@ import ( "fmt" "io" "net/http" - "strconv" "strings" "time" @@ -98,7 +97,7 @@ func pathCerts(b *keyfactorBackend) []*framework.Path { Fields: map[string]*framework.FieldSchema{ "serial": { Type: framework.TypeString, - Description: `The cerial number of the certificate to revoke`, + Description: `The serial number of the certificate to revoke`, }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -239,7 +238,7 @@ func (b *keyfactorBackend) pathIssue(ctx context.Context, req *logical.Request, return logical.ErrorResponse("role key type \"any\" not allowed for issuing certificates, only signing"), nil } - return b.pathIssueSignCert(ctx, req, data, role) + return b.pathIssueSignCert(ctx, req, data, role, roleName) } // pathSign issues a certificate from a submitted CSR, subject to role @@ -247,6 +246,7 @@ func (b *keyfactorBackend) pathIssue(ctx context.Context, req *logical.Request, func (b *keyfactorBackend) pathSign(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { roleName := data.Get("role").(string) csr := data.Get("csr").(string) + // Get the role role, err := b.getRole(ctx, req.Storage, roleName) if err != nil { @@ -256,11 +256,75 @@ func (b *keyfactorBackend) pathSign(ctx context.Context, req *logical.Request, d return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", roleName)), nil } + if !role.NoStore && b.System().ReplicationState().HasState(consts.ReplicationPerformanceStandby) { + return nil, logical.ErrReadOnly + } + + var err_resp error + var valid bool + + arg, _ := json.Marshal(req.Data) + b.Logger().Debug(string(arg)) + + // validate DNS SANS (optional) + var dns_sans []string + b.Logger().Debug("parsing dns_sans...") + dns_sans_string, ok := data.GetOk("dns_sans") + + if ok && dns_sans_string != nil && dns_sans_string == "" { + dns_sans_string = dns_sans_string.(string) + dns_sans = strings.Split(dns_sans_string.(string), ",") + b.Logger().Debug(fmt.Sprintf("dns_sans = %s", dns_sans)) + + b.Logger().Trace("checking to make sure all DNS SANs are allowed by role..") + + // check the provided DNS sans against allowed domains + valid, err_resp = checkAllowedDomains(role, roleName, dns_sans) + if err_resp != nil && !valid { + b.Logger().Error(err_resp.Error()) + return logical.ErrorResponse("DNS_SAN(s) not allowed for role: %s", err_resp.Error()), err_resp + } + } else { + b.Logger().Debug("no DNS SANs provided") + } + + // ip sans (optional) + var ip_sans []string + b.Logger().Debug("parsing ip_sans...") + ip_sans_string, ok := data.GetOk("ip_sans") + + if ok && ip_sans_string != nil && ip_sans_string.(string) != "" { + b.Logger().Trace(fmt.Sprintf("passed ip_sans: %s", ip_sans_string.(string))) + ip_sans = strings.Split(ip_sans_string.(string), ",") + } else { + b.Logger().Debug("no IP SANs provided") + } + + // get the CA name + b.Logger().Debug("parsing ca...") caName := data.Get("ca").(string) + if caName == "" { + b.Logger().Debug("no ca passed, retreiving from config") + caName = b.cachedConfig.CertAuthority + } + if caName == "" { + return logical.ErrorResponse("no certificate authority was provided and there is no configuration entry for ca"), fmt.Errorf("CA name is required") + } + b.Logger().Debug(fmt.Sprintf("ca name = %s", caName)) + + // get the template name + b.Logger().Debug("parsing template name...") templateName := data.Get("template").(string) + if templateName == "" { + b.Logger().Debug("no template name in parameters, retrieving from config") + templateName = b.cachedConfig.CertTemplate + if templateName == "" { + return logical.ErrorResponse("no certificate template name was provided and there is no configuration entry for 'template'"), fmt.Errorf("template name is required") + } + } + b.Logger().Debug(fmt.Sprintf("template name: %s", templateName)) - b.Logger().Debug("CA Name parameter = " + caName) - b.Logger().Debug("Template name parameter = " + templateName) + //check role permissions metadata := data.Get("metadata").(string) @@ -271,12 +335,15 @@ func (b *keyfactorBackend) pathSign(ctx context.Context, req *logical.Request, d // verify that any passed metadata string is valid JSON if !b.isValidJSON(metadata) { - err := fmt.Errorf("'%s' is not a valid JSON string", metadata) - b.Logger().Error(err.Error()) - return nil, err + err_resp := fmt.Errorf("'%s' is not a valid JSON string", metadata) + b.Logger().Error(err_resp.Error()) + } + + if err_resp != nil { + return nil, err_resp } - certs, serial, errr := b.submitCSR(ctx, req, csr, caName, templateName, metadata) + certs, serial, errr := b.submitCSR(ctx, req, csr, caName, templateName, dns_sans, ip_sans, metadata) if errr != nil { return nil, fmt.Errorf("could not sign csr: %s", errr) @@ -292,7 +359,7 @@ func (b *keyfactorBackend) pathSign(ctx context.Context, req *logical.Request, d return response, nil } -func (b *keyfactorBackend) pathIssueSignCert(ctx context.Context, req *logical.Request, data *framework.FieldData, role *roleEntry) (*logical.Response, error) { +func (b *keyfactorBackend) pathIssueSignCert(ctx context.Context, req *logical.Request, data *framework.FieldData, role *roleEntry, roleName string) (*logical.Response, error) { // If storing the certificate and on a performance standby, forward this request on to the primary if !role.NoStore && b.System().ReplicationState().HasState(consts.ReplicationPerformanceStandby) { return nil, logical.ErrReadOnly @@ -300,52 +367,73 @@ func (b *keyfactorBackend) pathIssueSignCert(ctx context.Context, req *logical.R var ip_sans []string var dns_sans []string + var err_resp error + var valid bool arg, _ := json.Marshal(req.Data) b.Logger().Debug(string(arg)) - // get common name + // validate Common Name (required) b.Logger().Debug("parsing common_name...") cn, ok := data.GetOk("common_name") - if !ok { + if !ok || cn == nil || cn.(string) == "" { return nil, fmt.Errorf("common_name must be provided to issue certificate") } - cn = cn.(string) - if cn == "" { - return nil, fmt.Errorf("common_name must be provided to issue certificate") - } - b.Logger().Debug(fmt.Sprintf("common_name = %s", cn)) - // get dns sans (required) + // check to make sure common name is allowed by role + b.Logger().Trace("checking common name" + cn.(string)) + valid, err_resp = checkAllowedDomains(role, roleName, []string{cn.(string)}) + + if err_resp != nil && !valid { + b.Logger().Error(err_resp.Error()) + return logical.ErrorResponse("disallowed common name was provided: %s", err_resp.Error()), err_resp + } + + // validate DNS SANS (required) b.Logger().Debug("parsing dns_sans...") dns_sans_string, ok := data.GetOk("dns_sans") - if !ok { + if !ok || dns_sans_string == nil || dns_sans_string == "" { return nil, fmt.Errorf("dns_sans must be provided to issue certificate") } - dns_sans_string = dns_sans_string.(string) + dns_sans = strings.Split(dns_sans_string.(string), ",") - if dns_sans_string == "" { - return nil, fmt.Errorf("dns_sans must be provided to issue certificate") - } + b.Logger().Debug(fmt.Sprintf("dns_sans = %s", dns_sans)) - dns_sans = strings.Split(dns_sans_string.(string), ",") + cnMatch := false - if len(dns_sans) == 0 { - return nil, fmt.Errorf("dns_sans must be provided to issue certificate") + // make sure at least one DNS SAN matches the common name + for u := range dns_sans { + if cnMatch { + break + } // no need to check the rest if there was a match. + cnMatch = dns_sans[u] == cn.(string) } - b.Logger().Debug(fmt.Sprintf("dns_sans = %s", dns_sans)) + if !cnMatch { + err_resp = fmt.Errorf("at least one DNS SAN is required to match the supplied Common Name for RFC 2818 compliance") + b.Logger().Error(err_resp.Error()) + return logical.ErrorResponse(err_resp.Error()), err_resp + } + + b.Logger().Trace("checking to make sure all DNS SANs are allowed by role..") - // get ip sans (optional) + // check the provided DNS sans against allowed domains + valid, err_resp = checkAllowedDomains(role, roleName, dns_sans) + if err_resp != nil && !valid { + b.Logger().Error(err_resp.Error()) + return logical.ErrorResponse("DNS_SAN(s) not allowed for role: %s", err_resp.Error()), err_resp + } + + // ip sans (optional) b.Logger().Debug("parsing ip_sans...") ip_sans_string, ok := data.GetOk("ip_sans") - if ok && ip_sans_string != "" { + if ok && ip_sans_string != nil && ip_sans_string.(string) != "" { ip_sans = strings.Split(ip_sans_string.(string), ",") } @@ -356,6 +444,9 @@ func (b *keyfactorBackend) pathIssueSignCert(ctx context.Context, req *logical.R b.Logger().Debug("no ca passed, retreiving from config") caName = b.cachedConfig.CertAuthority } + if caName == "" { + return logical.ErrorResponse("no certificate authority was provided and there is no configuration entry for ca"), fmt.Errorf("CA name is required") + } b.Logger().Debug(fmt.Sprintf("ca name = %s", caName)) // get the template name @@ -368,62 +459,6 @@ func (b *keyfactorBackend) pathIssueSignCert(ctx context.Context, req *logical.R b.Logger().Debug(fmt.Sprintf("template name: %s", templateName)) //check role permissions - var err_resp error - var valid bool - var hasSuffix bool - - // check the allowed domains for a match. - // if allowed_domains is '*', allow any domain - - for _, v := range role.AllowedDomains { - if v == "*" || strings.HasSuffix(cn.(string), v) { // if it has the suffix.. - hasSuffix = true - if cn.(string) == v || role.AllowSubdomains { // and there is an exact match, or subdomains are allowed.. - valid = true // then it is valid - } - } - } - - if !valid { - err_resp = fmt.Errorf("common name not allowed for role") - } - if !valid && hasSuffix { - err_resp = fmt.Errorf("sub-domains not allowed for role") - } - - if err_resp != nil { - return nil, err_resp - } - - // check the provided DNS sans against allowed domains - var cnMatch = false - b.Logger().Trace("checking dns sans" + dns_sans[0] + ", ...") - for u := range dns_sans { - valid = false - hasSuffix = false - cnMatch = cnMatch || dns_sans[u] == cn.(string) // check to make sure at least one of the dns_sans match the cn - b.Logger().Trace("checking SANs") - for _, v := range role.AllowedDomains { - if v == "*" || strings.HasSuffix(dns_sans[u], v) { // if it has the suffix.. - hasSuffix = true - if dns_sans[u] == v || role.AllowSubdomains { // and there is an exact match, or subdomains are allowed.. - valid = true // then it is valid - } - } - } - if !valid { - err_resp = fmt.Errorf("subject alternative name %s not allowed for provided role", dns_sans[u]) - } - if !valid && hasSuffix { - err_resp = fmt.Errorf("sub-domains not allowed for role") - } - } - - b.Logger().Trace("cnMatch = " + strconv.FormatBool(cnMatch)) - - if !cnMatch { - err_resp = fmt.Errorf("at least one DNS SAN is required to match the supplied Common Name for RFC 2818 compliance") - } metadata := data.Get("metadata").(string) @@ -445,7 +480,7 @@ func (b *keyfactorBackend) pathIssueSignCert(ctx context.Context, req *logical.R //generate and submit CSR b.Logger().Debug("generating the CSR...") csr, key := b.generateCSR(cn.(string), ip_sans, dns_sans) - certs, serial, errr := b.submitCSR(ctx, req, csr, caName, templateName, metadata) + certs, serial, errr := b.submitCSR(ctx, req, csr, caName, templateName, dns_sans, ip_sans, metadata) if errr != nil { return nil, fmt.Errorf("could not enroll certificate: %s", errr) @@ -467,15 +502,15 @@ func (b *keyfactorBackend) pathIssueSignCert(ctx context.Context, req *logical.R } func (b *keyfactorBackend) pathRevokeCert(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + if b.System().ReplicationState().HasState(consts.ReplicationPerformanceStandby) { + return nil, logical.ErrReadOnly + } + serial := data.Get("serial").(string) b.Logger().Debug("serial = " + serial) if len(serial) == 0 { - return logical.ErrorResponse("The serial number must be provided"), nil - } - - if b.System().ReplicationState().HasState(consts.ReplicationPerformanceStandby) { - return nil, logical.ErrReadOnly + return logical.ErrorResponse("the serial number must be provided"), fmt.Errorf("the serial number must be provided") } // We store and identify by lowercase colon-separated hex, but other @@ -487,11 +522,6 @@ func (b *keyfactorBackend) pathRevokeCert(ctx context.Context, req *logical.Requ // Revokes a cert, and tries to be smart about error recovery func revokeCert(ctx context.Context, b *keyfactorBackend, req *logical.Request, serial string, fromLease bool) (*logical.Response, error) { - // As this backend is self-contained and this function does not hook into - // third parties to manage users or resources, if the mount is tainted, - // revocation doesn't matter anyways -- the CRL that would be written will - // be immediately blown away by the view being cleared. So we can simply - // fast path a successful exit. if b.System().Tainted() { return nil, nil } @@ -505,14 +535,6 @@ func revokeCert(ctx context.Context, b *keyfactorBackend, req *logical.Request, b.Logger().Debug("Closing idle connections") client.httpClient.CloseIdleConnections() - // config, err := b.fetchConfig(ctx, req.Storage) - // if err != nil { - // return nil, err - // } - // if config == nil { - // return logical.ErrorResponse("could not load configuration"), nil - // } - kfId, err := req.Storage.Get(ctx, "kfId/"+serial) //retrieve the keyfactor certificate ID, keyed by sn here if err != nil { b.Logger().Error("Unable to retreive Keyfactor certificate ID for cert with serial: "+serial, err) @@ -631,6 +653,57 @@ func revokeCert(ctx context.Context, b *keyfactorBackend, req *logical.Request, return resp, nil } +func checkAllowedDomains(role *roleEntry, roleName string, domains []string) (bool, error) { + //check role permissions + var err_resp error + var valid bool + var hasSuffix bool + var disallowed []string + + // check the allowed domains for a match. + // if allowed_domains is '*', allow any domain + + for _, d := range domains { + for _, v := range role.AllowedDomains { + if v == "*" || strings.HasSuffix(d, v) { // if it has the suffix.. + hasSuffix = true + if d == v || role.AllowSubdomains { // and there is an exact match, or subdomains are allowed.. + valid = true // then it is valid + } else { + valid = false + disallowed = append(disallowed, d) + } + } + } + } + if !valid { + var disallowed_domains = strings.Join(disallowed, ",") + var allowed_domains = strings.Join(role.AllowedDomains, ",") + err_resp = fmt.Errorf("domain name not allowed for role: %s. \n allowed domains for %s are: %s", disallowed_domains, roleName, allowed_domains) + } + if !valid && hasSuffix { + err_resp = fmt.Errorf("sub-domains are not allowed for role %s", roleName) + } + + if err_resp != nil { + return false, err_resp + } + + return true, nil +} + +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 + } +} + const pathIssueHelpSyn = ` Request a certificate using a certain role with the provided details. example: vault write keyfactor/issue/ common_name= dns_sans= diff --git a/path_config.go b/path_config.go index cd9e579..04daac2 100644 --- a/path_config.go +++ b/path_config.go @@ -34,7 +34,7 @@ type keyfactorConfig struct { AccessToken string `json:"access_token"` SkipTLSVerify bool `json:"skip_verify"` Scopes []string `json:"scopes"` - Audience []string `json:"audience"` + Audience string `json:"audience"` CertTemplate string `json:"template"` CertAuthority string `json:"ca"` CommandCertPath string `json:"command_cert_path"` @@ -42,6 +42,9 @@ type keyfactorConfig struct { func (b *keyfactorBackend) fetchConfig(ctx context.Context, s logical.Storage) (*keyfactorConfig, error) { if b.cachedConfig != nil { + if b.cachedConfig.CommandAPIPath == "" { + b.cachedConfig.CommandAPIPath = "KeyfactorAPI" + } return b.cachedConfig, nil } @@ -115,7 +118,7 @@ func pathConfig(b *keyfactorBackend) []*framework.Path { Required: false, }, "audience": { - Type: framework.TypeCommaStringSlice, + Type: framework.TypeString, Description: "The audience for authenticating with Keyfactor Command using `OAuth2` client" + " credentials.", Required: false, @@ -252,7 +255,7 @@ func (b *keyfactorBackend) pathConfigUpdate( TokenUrl: data.Get("token_url").(string), AccessToken: data.Get("access_token").(string), Scopes: data.Get("scopes").([]string), - Audience: data.Get("audience").([]string), + Audience: data.Get("audience").(string), Domain: data.Get("domain").(string), CommandCertPath: data.Get("command_cert_path").(string), SkipTLSVerify: data.Get("skip_verify").(bool), @@ -315,7 +318,7 @@ func (b *keyfactorBackend) pathConfigUpdate( } if audience, ok := data.GetOk("audience"); ok { - existingConfig.Audience = audience.([]string) + existingConfig.Audience = audience.(string) } if domain, ok := data.GetOk("domain"); ok { diff --git a/path_roles.go b/path_roles.go index 14358c0..90f6f8c 100644 --- a/path_roles.go +++ b/path_roles.go @@ -313,6 +313,29 @@ func (b *keyfactorBackend) pathRoleCreate(ctx context.Context, req *logical.Requ return nil, nil } +func parseOtherSANs(others []string) (map[string][]string, error) { + result := map[string][]string{} + for _, other := range others { + splitOther := strings.SplitN(other, ";", 2) + if len(splitOther) != 2 { + return nil, fmt.Errorf("expected a semicolon in other SAN %q", other) + } + splitType := strings.SplitN(splitOther[1], ":", 2) + if len(splitType) != 2 { + return nil, fmt.Errorf("expected a colon in other SAN %q", other) + } + switch { + case strings.EqualFold(splitType[0], "utf8"): + case strings.EqualFold(splitType[0], "utf-8"): + default: + return nil, fmt.Errorf("only utf8 other SANs are supported; found non-supported type in other SAN %q", other) + } + result[splitOther[0]] = append(result[splitOther[0]], splitType[1]) + } + + return result, nil +} + type roleEntry struct { LeaseMax string `json:"lease_max"` Lease string `json:"lease"` diff --git a/readme_source.md b/readme_source.md index 0e6b92e..fb42fc0 100644 --- a/readme_source.md +++ b/readme_source.md @@ -308,11 +308,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=" 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=" 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. @@ -372,7 +374,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 @@ -593,8 +595,8 @@ instance of the plugin is named "keyfactor". ### Read CA cert -`vault read keyfactor/ca` +`vault read keyfactor/ca ca=` ### Read CA chain -`vault read keyfactor/ca_chain` +`vault read keyfactor/ca_chain ca=`