From 9a545e45de257666412a1d3a124dbc450504095f Mon Sep 17 00:00:00 2001 From: Joe VanWanzeele <76071503+joevanwanzeeleKF@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:53:33 -0400 Subject: [PATCH] Added Tidy task, additional fields, unit tests, documentation improvements (#51) * updated CSR enrollment to use Keyfactor Client SDK * updated get cert method to use Keyfactor SDK * replaced all Command API calls with Keyfactor GO SDK. Added RunningVersion and multiplexing support * got revoke working, better logging * Finished testing. Updated documentation. * Added unit tests, improved documentation, added additional cert fields * Update workflow to use starter.yml version 4 * added release_dir field for updated workflow --- .../workflows/keyfactor-starter-workflow.yml | 12 +- .gitignore | 1 + CHANGELOG.md | 19 + LICENSE.txt | 2 +- README.md | 127 +++- backend.go | 54 +- backend_test.go | 87 +++ cert_util.go | 449 +++++------- cert_util_test.go | 148 ++++ client.go | 54 +- cmd/keyfactor/main.go | 7 +- fields.go | 4 +- go.mod | 138 ++-- go.sum | 647 ++++++++++++------ installation.txt | 3 +- integration-manifest.json | 5 +- path_ca.go | 18 +- path_certs.go | 308 ++++++--- path_certs_test.go | 160 +++++ path_config.go | 110 ++- path_config_test.go | 144 ++++ path_revoke.go | 69 -- path_roles.go | 2 +- path_roles_test.go | 125 ++++ path_tidy.go | 291 ++++++++ path_tidy_test.go | 110 +++ readme_source.md | 130 +++- 27 files changed, 2404 insertions(+), 820 deletions(-) create mode 100644 backend_test.go create mode 100644 cert_util_test.go create mode 100644 path_certs_test.go create mode 100644 path_config_test.go delete mode 100644 path_revoke.go create mode 100644 path_roles_test.go create mode 100644 path_tidy.go create mode 100644 path_tidy_test.go diff --git a/.github/workflows/keyfactor-starter-workflow.yml b/.github/workflows/keyfactor-starter-workflow.yml index 6d8de53..aebc4ae 100644 --- a/.github/workflows/keyfactor-starter-workflow.yml +++ b/.github/workflows/keyfactor-starter-workflow.yml @@ -11,9 +11,17 @@ on: jobs: call-starter-workflow: - uses: keyfactor/actions/.github/workflows/starter.yml@v2 + uses: keyfactor/actions/.github/workflows/starter.yml@v4 + with: + command_token_url: ${{ vars.COMMAND_TOKEN_URL }} + command_hostname: ${{ vars.COMMAND_HOSTNAME }} + command_base_api_path: ${{ vars.COMMAND_API_PATH }} secrets: token: ${{ secrets.V2BUILDTOKEN}} - APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}} gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }} gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }} + scan_token: ${{ secrets.SAST_TOKEN }} + entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }} + entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }} + command_client_id: ${{ secrets.COMMAND_CLIENT_ID }} + command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }} diff --git a/.gitignore b/.gitignore index d2536d7..f2e3126 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ Keyfactor Vault Secrets Engine Guide.docx Makefile sample_config.json README.md +README.md diff --git a/CHANGELOG.md b/CHANGELOG.md index dc47006..a73d5e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +- 1.5.0 + - Added automatic and on-demand cleanup ("tidy") of expired certificates from local storage, via the new `tidy` and `tidy/status` endpoints and the `tidy_enabled`, `tidy_interval`, and `tidy_safety_buffer` configuration settings. + - Certificate listing (`vault list /certs`) now includes the common name alongside each serial number. + - Reading a certificate (`vault read /certs/`) now returns the serial number, common name, certificate content, submitted metadata, and expiration date. + - Certificate metadata submitted when issuing or signing is now stored locally and echoed back in the issue and sign responses. + - Fixed several potential nil-pointer panics that could occur when the Command API returned an error or an empty response. + - Fixed concurrency issues where the cached configuration and API client could be mutated while only a read lock was held. + - Deleting the configuration now clears the in-memory cache, so subsequent operations no longer use the deleted configuration. + - Fixed certificate revocation failing with "failed to decompress JSON: lz4: bad magic number" for certificates whose Keyfactor ID begins with a character matching a Vault compression canary; the stored ID is now decoded directly rather than through the compression-aware decoder. + - Fixed the sign path so that provided DNS SANs are validated against the role (previously this check was inadvertently skipped). + - Fixed certificate metadata JSON validation being silently bypassed. + - Corrected allowed-domain matching to prevent a subdomain-suffix bypass (for example, "evilexample.com" is no longer treated as matching "example.com"). + - The access token is now masked in the configuration read output. + - Added a unit test suite and corrected several documentation inaccuracies. + +- 1.4.2 + - Updated the Hashicorp SDK libraries + - Incorporated the Keyfactor GO SDK for authentication and interaction with the Command API + - 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 diff --git a/LICENSE.txt b/LICENSE.txt index 52b7995..944b825 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright 2024 Keyfactor +Copyright 2026 Keyfactor Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/README.md b/README.md index fbb5cf4..bffcc80 100644 --- a/README.md +++ b/README.md @@ -48,17 +48,21 @@ returned to Hashicorp Vault and stored within the Vault Secrets store to then be - [Keyfactor Requirements](#keyfactor-requirements) - [Hashicorp Vault Requirements](#hashicorp-vault-requirements) - [Installation - Keyfactor](#installation---keyfactor) - - [Create the Active Directory service account](#create-the-active-directory-service-account) + - [Create the Service Account in Command](#create-the-service-account-in-command) + - [Assign the user permissions in Keyfactor Command](#assign-the-user-permissions-in-keyfactor-command) - [Create a certificate template](#create-a-certificate-template) - - [Publish the template for the Certificate Authority](#publish-the-template-for-the-certificate-authority) + - [Allow the template to be used for CSR enrollment](#allow-the-template-to-be-used-for-csr-enrollment-through-keyfactor) - [Installation - Vault](#installation---vault) - [Check the Vault server status](#check-the-vault-server-status) - [Install and register the plugin](#install-and-register-the-plugin) - [Configure the plugin](#configure-the-plugin) + - [Basic Authentication](#basic-authentication-configuration) + - [OpenID Connect / oAuth](#openid-connect--oauth-configuration) - [Adding Roles](#adding-roles) - [Using the plugin](#using-the-plugin) - [Issuing Certificates](#issuing-certificates) - [Viewing Certificates](#viewing-certificates) + - [Tidying expired certificates](#tidying-expired-certificates) - [Command Reference](#plugin-command-reference) - [Create/update configuration](#createupdate-configuration) - [Read configuration](#read-configuration) @@ -73,6 +77,8 @@ returned to Hashicorp Vault and stored within the Vault Secrets store to then be - [Sign a CSR](#sign-csr) - [View CA Certificate](#read-ca-cert) - [View CA Certificate Chain](#read-ca-chain) + - [Tidy expired certificates](#tidy-expired-certificates) + - [Read tidy status](#read-tidy-status) ## Overview @@ -122,6 +128,24 @@ issued. The Keyfactor Secrets Engine offers the following enterprise capabilitie > native Vault policies, the roles implemented by the secrets engine plugin, and the template > rules available in Command. +### Per-instance configuration + +The Keyfactor plugin implements a **per-instance configuration**. Because Vault mounts every enabled +secrets engine at its own path, you can register and enable multiple independent instances of this +plugin at the same time, each with its own configuration, certificate store, roles, and default CA and +template. Each instance is scoped to a single Keyfactor Command connection and a single service-account +identity. + +This is a deliberate architectural choice rather than an incidental behavior. It lets you dedicate a +separate plugin instance to each certificate issuance workflow — for example, one instance +authenticating as a service account restricted to web-server templates, and a second instance scoped to +a different CA, template, and identity for client-authentication certificates. Because Vault ACL +policies are applied per mount path, this also allows you to grant different Vault clients access to +different Command identities and issuance policies without them interfering with one another. + +The remainder of this document describes configuring a single instance; repeat the enable and configure +steps for each additional instance you require, giving each a distinct mount path. + ## Compatibility This Vault Plugin has been tested against Hashicorp Vault version 1.10+ and the Keyfactor Platform 9.6+. We provide @@ -157,21 +181,28 @@ document. ### Create the Service Account in Command -This plugin can authenticate via username/password, TLS certificate authentication, and oAuth/openIDConnect. +This plugin authenticates to the Keyfactor Command API using one of two methods: + +- **Basic** — an Active Directory username, password, and domain. +- **OAuth / OpenID Connect** — a client ID, client secret, and token endpoint (or a pre-obtained access token). For the purposes of this document, we will not go into the details of how to create each type of service entity. -Refer to the Keyfactor platform documentation for guidance on creating these service accounts. +Refer to the Keyfactor platform documentation for guidance on creating these service accounts. -The configuration of the plugin will differ slightly for each different approach. Here is a table with the values +The configuration of the plugin will differ slightly for each approach. Here is a table with the values needed for authentication for each approach: -| basic | oAuth | TLS | -| ----------------- | ----- | --- | -| Username | Client ID | Certificate Path | -| Password | Client Secret | | -| AD Domain | Token Endpoint | | +| Basic | oAuth | +| ----- | ----- | +| Username (`username`) | Client ID (`client_id`) | +| Password (`password`) | Client Secret (`client_secret`) | +| AD Domain (`domain`) | Token Endpoint (`token_url`) | + +These values are discussed in greater detail in the [Configure the plugin](#configure-the-plugin) section of this document. -These values will be discussed in greater detail in the [Configure the plugin](#configure-the-plugin) section of this document. +> [!NOTE] +> `command_cert_path` and `skip_verify` control how the plugin *trusts Command's server TLS certificate*; +> they are not authentication credentials. See [Configure the plugin](#configure-the-plugin). ### Assign the user permissions in Keyfactor Command @@ -366,6 +397,12 @@ you may or may not be able to access certain paths. Request certificates using a certain role with the provided details. example: vault write keyfactor/sign/ csr= + ^tidy$ + Tidy up the locally-stored certificate store by removing expired certificates. + + ^tidy/status$ + Return the status of the most recent tidy operation. + ``` If you see this, you have successfully installed the plugin. Now we can configure it for connecting with Command for certificate enrollment. @@ -375,9 +412,9 @@ If you see this, you have successfully installed the plugin. Now we can configu Once the plugin has been successfully installed, the next step is to set the configuration values that will allow it to interact with the Keyfactor platform. -The Keyfactor plugin implements a per-instance configuration which allows multiple instances of the plugin to exist -simultaneously. This could be useful for creating multiple instances of the plugin; each scoped to a specific Command service account identity that -is specific to a particular issuance workflow. +The Keyfactor plugin uses a per-instance configuration (see +[Per-instance configuration](#per-instance-configuration) in the Overview). The settings described below +apply to the single plugin instance you are configuring; repeat these steps for each additional instance. To set a configuration value: @@ -399,8 +436,11 @@ Here is a table of the available configuration paramaters | **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 | 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 +| **skip_verify** | bool | no | _false_ | set this to true to skip verification of the Command **server's** TLS certificate. Intended for test environments only; leave false in production | +| **command_cert_path** | string | no | | the local path to a PEM-encoded CA certificate used to **trust the Command server's TLS certificate**, when it is not already trusted by the host and skip_verify is false. This establishes server trust only; it is not a client authentication credential | +| **tidy_enabled** | bool | no | _false_ | when true, the plugin periodically removes locally-stored certificates that have expired. See [Tidying expired certificates](#tidying-expired-certificates) | +| **tidy_interval** | duration | no | _24h_ | how often the automatic tidy sweep runs when tidy_enabled is true | +| **tidy_safety_buffer** | duration | no | _72h_ | how long an expired certificate is retained past its expiry before the tidy sweep removes it | [^1]: The **ca** and **template** fields can be provided via command line parameters. If they are not provided, the plugin will default to what is set in the configuration values. If neither are available an error will occur. @@ -491,11 +531,16 @@ the Vault secrets store. After certificates are stored in the secrets store, you can then retrieve those certificates at a later time if necessary. To list the certificates that exist within the Vault store, use the LIST option with vault. The only parameter that you need to include is the secrets store name for the store that you would like to read. The system will -then return a list of all of the serial numbers for certificates that are present in that secrets store. +then return the serial numbers of the certificates present in that secrets store. `vault list keyfactor/certs` -The results of the command will be a list of serial numbers for the certificates in that store location: +To also see the common name of each certificate, add the `-detailed` flag (the common name is returned as +additional key information that the standard `vault list` output does not display): + +`vault list -detailed keyfactor/certs` + +The plain command returns a list of serial numbers for the certificates in that store location: ``` Keys @@ -514,7 +559,10 @@ example: `vault read keyfactor/cert/750000276546d818cbe70231b6000000002765` -The response will show the value for that certificate. +The response includes the certificate's serial number, common name, the PEM-encoded certificate content, the +expiration date, and the revocation time (`0` if the certificate has not been revoked). If metadata was submitted +when the certificate was issued, it is also included; the metadata field is omitted when none was provided. For +example: ``` Key Value @@ -566,6 +614,38 @@ LS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0K revocation_time 0 ``` +### Tidying expired certificates + +Unless a role is configured with `no_store=true`, the plugin keeps a copy of every issued certificate in its +Vault storage (along with the certificate's Keyfactor ID and any revocation record). Over time, expired +certificates accumulate and consume storage. The `tidy` operation reclaims that space by removing certificates +whose expiration date (`NotAfter`), plus a safety buffer, has passed. The associated Keyfactor ID and revocation +records are removed along with each certificate. + +To run a tidy sweep on demand: + +`vault write keyfactor/tidy safety_buffer=72h` + +The sweep runs in the background so it does not block the request. To view its progress and the results of the +most recent run: + +`vault read keyfactor/tidy/status` + +The status includes whether a sweep is currently running, when it last ran, and how many certificates were +examined, deleted, and skipped. + +You can also enable automatic, scheduled tidying through the configuration: + +`vault write keyfactor/config tidy_enabled=true tidy_interval=24h tidy_safety_buffer=72h` + +When enabled, the plugin runs the sweep automatically on the configured interval (on the active node of the +primary cluster). Automatic tidy is **disabled by default**, so no certificates are ever removed until you either +run `keyfactor/tidy` manually or set `tidy_enabled=true`. + +> [!NOTE] +> Certificates that cannot be parsed are skipped and never deleted. Certificates issued using a role with +> `no_store=true` are not stored in the first place, so they are unaffected by tidy. + ## Plugin command reference The following commands are supported by the Keyfactor Hashicorp Vault Secrets Engine plugin. These examples assume the @@ -620,9 +700,18 @@ instance of the plugin is named "keyfactor". ### Read CA cert `vault read keyfactor/ca ca=` +> Note: The certificate for the CA needs to have been imported into Command for this endpoint to return the CA Certificate ### Read CA chain `vault read keyfactor/ca_chain ca=` +> Note: _All_ certificates in the chain need to have been imported into Command for this endpoint to return the CA Certificate Chain + +### Tidy expired certificates + +`vault write keyfactor/tidy safety_buffer=` +> Note: `safety_buffer` is optional and defaults to 72h. The sweep runs in the background. +### Read tidy status +`vault read keyfactor/tidy/status` diff --git a/backend.go b/backend.go index bdab3b1..866aeb1 100644 --- a/backend.go +++ b/backend.go @@ -1,5 +1,5 @@ /* - * Copyright 2024 Keyfactor + * Copyright 2026 Keyfactor * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -15,13 +15,16 @@ import ( "fmt" "strings" "sync" + "time" + "github.com/Keyfactor/keyfactor-go-client-sdk/v24" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" ) const ( operationPrefixKeyfactor string = "keyfactor" + PluginVersion = "1.5.0" // this should match the release version of the plugin ) // Factory configures and returns backend @@ -39,7 +42,13 @@ type keyfactorBackend struct { *framework.Backend configLock sync.RWMutex cachedConfig *keyfactorConfig - client *keyfactorClient + client *keyfactor.APIClient + + // tidy state guards the expired-certificate cleanup sweep. + tidyLock sync.Mutex + tidyRunning bool + lastTidy time.Time + tidyResult *tidyStatus } // keyfactorBackend defines the target API keyfactorBackend @@ -62,11 +71,14 @@ func backend() *keyfactorBackend { pathRoles(&b), pathCA(&b), pathCerts(&b), + pathTidy(&b), ), Secrets: []*framework.Secret{}, BackendType: logical.TypeLogical, Invalidate: b.invalidate, InitializeFunc: b.Initialize, + PeriodicFunc: b.periodicFunc, + RunningVersion: "v" + PluginVersion, } return &b } @@ -74,16 +86,14 @@ func backend() *keyfactorBackend { // reset clears any client configuration for a new // backend to be configured func (b *keyfactorBackend) reset() { - b.configLock.RLock() - defer b.configLock.RUnlock() + // This mutates shared state, so it requires the write lock. + b.configLock.Lock() + defer b.configLock.Unlock() b.cachedConfig = nil b.client = nil - } func (b *keyfactorBackend) Initialize(ctx context.Context, req *logical.InitializationRequest) error { - b.configLock.RLock() - defer b.configLock.RUnlock() if req == nil { return fmt.Errorf("initialization request is nil") } @@ -100,17 +110,18 @@ func (b *keyfactorBackend) invalidate(ctx context.Context, key string) { // getClient locks the backend as it configures and creates a // a new client for the target API -func (b *keyfactorBackend) getClient(ctx context.Context, s logical.Storage) (*keyfactorClient, error) { +func (b *keyfactorBackend) getClient(ctx context.Context, s logical.Storage) (*keyfactor.APIClient, error) { + // Fast path: return the cached client under a read lock. b.configLock.RLock() - 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 + client := b.client + b.configLock.RUnlock() + if client != nil { + b.Logger().Trace("returning existing client") + return client, nil } - // get configuration + // get configuration (fetchConfig manages its own locking, so we must not + // hold configLock while calling it to avoid a recursive-lock deadlock) config, err := b.fetchConfig(ctx, s) if err != nil { return nil, err @@ -119,13 +130,22 @@ func (b *keyfactorBackend) getClient(ctx context.Context, s logical.Storage) (*k return nil, errors.New("configuration is empty") } - b.client, err = newClient(config, b) + // Slow path: build the client under the write lock, re-checking in case + // another goroutine created it while we were unlocked. + b.configLock.Lock() + defer b.configLock.Unlock() + if b.client != nil { + return b.client, nil + } + + newC, err := newClient(config, b) if err != nil { return nil, err } + b.client = newC return b.client, nil } const keyfactorHelp = ` -The Keyfactor backend is a pki service that issues and manages certificates. +The Keyfactor backend is a pki service that issues and manages certificates via the Keyfactor Command platform. ` diff --git a/backend_test.go b/backend_test.go new file mode 100644 index 0000000..d87f13a --- /dev/null +++ b/backend_test.go @@ -0,0 +1,87 @@ +/* + * Copyright 2026 Keyfactor + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package kfbackend + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "testing" + "time" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/logical" +) + +// getTestBackend spins up an instance of the backend wired to in-memory +// storage, suitable for exercising paths that do not require a live Keyfactor +// Command connection. +func getTestBackend(t *testing.T) (*keyfactorBackend, logical.Storage) { + t.Helper() + + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + config.Logger = hclog.NewNullLogger() + + b, err := Factory(context.Background(), config) + if err != nil { + t.Fatalf("unable to create backend: %v", err) + } + + kb, ok := b.(*keyfactorBackend) + if !ok { + t.Fatalf("Factory returned unexpected type %T", b) + } + return kb, config.StorageView +} + +// makeTestCertDER creates a self-signed certificate and returns its DER bytes. +func makeTestCertDER(t *testing.T, cn string, notAfter time.Time) []byte { + t.Helper() + + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("failed to generate key: %v", err) + } + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(time.Now().UnixNano()), + Subject: pkix.Name{CommonName: cn}, + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: notAfter, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + if err != nil { + t.Fatalf("failed to create certificate: %v", err) + } + return der +} + +// makeTestCertPEM creates a self-signed certificate and returns it PEM-encoded. +func makeTestCertPEM(t *testing.T, cn string, notAfter time.Time) []byte { + t.Helper() + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: makeTestCertDER(t, cn, notAfter)}) +} + +func TestBackend_Factory(t *testing.T) { + b, storage := getTestBackend(t) + if b == nil { + t.Fatal("nil backend") + } + if b.Backend == nil { + t.Fatal("framework backend was not initialized") + } + if storage == nil { + t.Fatal("nil storage") + } +} diff --git a/cert_util.go b/cert_util.go index 9849609..3163600 100644 --- a/cert_util.go +++ b/cert_util.go @@ -1,5 +1,5 @@ /* - * Copyright 2024 Keyfactor + * Copyright 2026 Keyfactor * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -24,24 +24,29 @@ import ( "fmt" "io" "net" - "net/http" "strings" "time" - "github.com/hashicorp/errwrap" + v1 "github.com/Keyfactor/keyfactor-go-client-sdk/v24/api/keyfactor/v1" "github.com/hashicorp/vault/sdk/helper/errutil" "github.com/hashicorp/vault/sdk/logical" "go.mozilla.org/pkcs7" ) // Generate keypair and CSR -func (b *keyfactorBackend) generateCSR(cn string, ip_sans []string, dns_sans []string) (string, []byte) { - keyBytes, _ := rsa.GenerateKey(rand.Reader, 2048) +func (b *keyfactorBackend) generateCSR(cn string, ip_sans []string, dns_sans []string) (string, []byte, error) { + keyBytes, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return "", nil, fmt.Errorf("failed to generate RSA private key: %w", err) + } subj := pkix.Name{ CommonName: cn, } rawSubj := subj.ToRDNSequence() - asn1Subj, _ := asn1.Marshal(rawSubj) + asn1Subj, err := asn1.Marshal(rawSubj) + if err != nil { + return "", nil, fmt.Errorf("failed to marshal the certificate subject: %w", err) + } var netIPSans []net.IP for i := range ip_sans { netIPSans = append(netIPSans, net.ParseIP(ip_sans[i])) @@ -53,10 +58,15 @@ func (b *keyfactorBackend) generateCSR(cn string, ip_sans []string, dns_sans []s IPAddresses: netIPSans, DNSNames: dns_sans, } - csrBytes, _ := x509.CreateCertificateRequest(rand.Reader, &csrtemplate, keyBytes) + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrtemplate, keyBytes) + if err != nil { + return "", nil, fmt.Errorf("failed to create the certificate request: %w", err) + } csrBuf := new(bytes.Buffer) - pem.Encode(csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}) - return csrBuf.String(), x509.MarshalPKCS1PrivateKey(keyBytes) + if err := pem.Encode(csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrBytes}); err != nil { + return "", nil, fmt.Errorf("failed to PEM-encode the certificate request: %w", err) + } + return csrBuf.String(), x509.MarshalPKCS1PrivateKey(keyBytes), nil } // Handle interface with Keyfactor API to enroll a certificate with given content @@ -70,8 +80,7 @@ func (b *keyfactorBackend) submitCSR(ctx context.Context, req *logical.Request, } location, _ := time.LoadLocation("UTC") - t := time.Now().In(location) - time := t.Format("2006-01-02T15:04:05") + requestTime := time.Now().In(location) // get client client, err := b.getClient(ctx, req.Storage) @@ -79,117 +88,116 @@ func (b *keyfactorBackend) submitCSR(ctx context.Context, req *logical.Request, return nil, "", fmt.Errorf("error getting client: %w", err) } - b.Logger().Debug("Closing idle connections") - client.httpClient.CloseIdleConnections() - // build request parameter structure + metadataMap := make(map[string]interface{}) - // build dns_sans payload string - dns_sans_payload_string := "" + err = json.Unmarshal([]byte(metaDataJson), &metadataMap) - 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 - } - dns_sans_payload_string = dns_sans_payload_string + fmt.Sprintf("\"%s\"", d) + if err != nil { + return nil, "", fmt.Errorf("there was an error parsing the Metadata as JSON: %w", err) } - b.Logger().Debug("dns_sans payload string = %s", dns_sans_payload_string) - ip_sans_payload_string := "" + inclChain := true - 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) + enrollmentRequest := v1.EnrollmentCSREnrollmentRequest{ + CSR: csr, + CertificateAuthority: *v1.NewNullableString(&caName), + IncludeChain: &inclChain, + Metadata: metadataMap, + Timestamp: &requestTime, + Template: *v1.NewNullableString(&templateName), } - 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 parameter + b.Logger().Debug(fmt.Sprintf("ip_sans = %s", ip_sans)) + b.Logger().Debug(fmt.Sprintf("dns_sans = %s", dns_sans)) - sans_payload := "\"SANs\": {" + sans := make(map[string][]string) - 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 + "]" - } + if len(ip_sans) > 0 { + sans["ip"] = ip_sans + } + + if len(dns_sans) > 0 { + sans["dns"] = dns_sans } - sans_payload += "}" - b.Logger().Trace(fmt.Sprintf("sans_payload: %s", sans_payload)) - bodyContent += ", " + sans_payload + "}" - payload := strings.NewReader(bodyContent) + enrollmentRequest.SANs = sans + + enrollmentRequest.SetSANs(sans) - b.Logger().Debug("request body: " + bodyContent) - httpReq, err := http.NewRequest("POST", url, payload) + reqMap := make(map[string]interface{}) + reqMap, err = enrollmentRequest.ToMap() if err != nil { - b.Logger().Info("Error forming request: {{err}}", err) + b.Logger().Error(fmt.Sprintf("conversion of paramaters to map failed: %s", err.Error())) } - httpReq.Header.Add("x-keyfactor-requested-with", "APIClient") - httpReq.Header.Add("content-type", "application/json") - httpReq.Header.Add("x-certificateformat", "PEM") + b.Logger().Debug(fmt.Sprintf("request body: %s", reqMap)) + b.Logger().Debug(fmt.Sprintf("sans parameter: %s", enrollmentRequest.GetSANs())) // 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) - } + b.Logger().Debug("setting parameters on the request.. ") - // Read response and return certificate and key + apiRequest := client.V1.EnrollmentApi.NewCreateEnrollmentCSRRequest(ctx).ForceEnroll(true).EnrollmentCSREnrollmentRequest(enrollmentRequest).XCertificateformat("PEM") - defer res.Body.Close() - body, err := io.ReadAll(res.Body) - if err != nil { - b.Logger().Error("Error reading response: {{err}}", err) - return nil, "", err - } + b.Logger().Debug("about to connect to " + config.KeyfactorUrl + " with Keyfactor client for CSR submission") + + resData, httpRes, err := apiRequest.Execute() - // Parse response - var r map[string]interface{} - json.Unmarshal(body, &r) - b.Logger().Debug("response = ", r) + if err != nil || httpRes == nil || httpRes.StatusCode != 200 { + errMsg := "" + statusCode := 0 - 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:] + switch { + case httpRes == nil: + // The SDK returned no response (e.g. connection failure); fall + // back to the error itself, which is all we have. + if err != nil { + errMsg = err.Error() + } else { + errMsg = "no response received from Command" + } + default: + statusCode = httpRes.StatusCode + body, bodyErr := io.ReadAll(httpRes.Body) + if bodyErr != nil { + b.Logger().Error(fmt.Sprintf("there was an error reading the response body: %v", bodyErr)) + if err != nil { + errMsg = err.Error() + } + } else { + errMsg = string(body) + } + } + b.Logger().Error(fmt.Sprintf("there was an error performing CSR enrollment. HttpStatusCode: %d, error: %s", statusCode, errMsg)) + return nil, "", fmt.Errorf("CSR enrollment failed (status %d): %s", statusCode, errMsg) } - serial := inner["SerialNumber"].(string) - kfId := inner["KeyfactorID"].(float64) - b.Logger().Debug("parsed response: ", certI...) + // Read certificates from response + certs, ok := resData.CertificateInformation.GetCertificatesOk() - caEntry, err := logical.StorageEntryJSON("ca_chain/", certs[1:]) - if err != nil { - b.Logger().Error("error creating ca_chain entry", err) + if !ok || len(certs) == 0 { + b.Logger().Error("unable to read certificate from the Command enrollment response") + return nil, "", errors.New("no certificates were returned in the Command enrollment response") } - err = req.Storage.Put(ctx, caEntry) - if err != nil { - b.Logger().Error("error storing the ca_chain locally", err) + serial := resData.CertificateInformation.SerialNumber + kfId := resData.CertificateInformation.KeyfactorID + + if serial.Get() == nil { + return nil, "", errors.New("Command enrollment response did not include a serial number") } - key := "certs/" + normalizeSerial(serial) + resMap, _ := resData.ToMap() + b.Logger().Debug(fmt.Sprintf("full response: %s", resMap)) + + // store the certificate + + normalizedSerial := strings.ToUpper(*serial.Get()) + + key := "certs/" + normalizedSerial entry := &logical.StorageEntry{ Key: key, @@ -200,20 +208,32 @@ func (b *keyfactorBackend) submitCSR(ctx context.Context, req *logical.Request, err = req.Storage.Put(ctx, entry) if err != nil { - return nil, "", errwrap.Wrapf("unable to store certificate locally: {{err}}", err) + return nil, "", fmt.Errorf("unable to store certificate locally: %w", err) } - kfIdEntry, err := logical.StorageEntryJSON("kfId/"+normalizeSerial(serial), kfId) + kfIdEntry, err := logical.StorageEntryJSON("kfId/"+normalizedSerial, kfId) if err != nil { return nil, "", err } + b.Logger().Debug(fmt.Sprintf("writing the Keyfactor ID to storage for future operations: key = %s, value = %s", kfIdEntry.Key, kfIdEntry.Value)) + 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 nil, "", fmt.Errorf("unable to store the keyfactor ID for the certificate locally: %w", err) + } + + // store the metadata so it can be surfaced when the certificate is read. + // This is supplementary information, so a failure here is logged but does + // not fail the enrollment. + metadataEntry, err := logical.StorageEntryJSON("metadata/"+normalizedSerial, metadataMap) + if err != nil { + b.Logger().Error("unable to create certificate metadata entry", "error", err) + } else if err = req.Storage.Put(ctx, metadataEntry); err != nil { + b.Logger().Error("unable to store certificate metadata locally", "error", err) } - return certs, serial, nil + return certs, normalizedSerial, nil } // fetch the CA info from keyfactor @@ -233,7 +253,7 @@ func fetchCAInfo(ctx context.Context, req *logical.Request, b *keyfactorBackend, 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) + b.Logger().Debug(fmt.Sprintf("local storage path = %s", storagePath)) caEntry, err := req.Storage.Get(ctx, storagePath) @@ -266,37 +286,35 @@ func fetchCAInfo(ctx context.Context, req *logical.Request, b *keyfactorBackend, // 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 + issued_cert, err := fetchCertIssuedByCA(ctx, req, b, caName) // we get the ID of a cert issued by the CA if err != nil { return nil, errutil.InternalError{Err: fmt.Sprintf("failed to retreive any cert issued by the CA: %s", err)} } - if len(issued_certs) == 0 { + if issued_cert == nil { return nil, fmt.Errorf("no certificates issued by %s were found", caName) } - 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 + ca_chain, ca_cert, err := fetchChainAndCAForCert(ctx, req, b, int(*issued_cert.Id)) // we download the full cert and chain if err != nil { - b.Logger().Error("error getting full chain and CA for cert: %s", err) + b.Logger().Error("error getting full chain and CA for cert", "error", err) return nil, err } - b.Logger().Trace("extracted ca and chain from cert. chain has a length of %d \n", len(ca_chain)) + b.Logger().Trace(fmt.Sprintf("extracted ca and chain from cert. chain has a length of %d", len(ca_chain))) // 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) + b.Logger().Error("error creating ca entry", "error", err) } err = req.Storage.Put(ctx, caStorageEntry) if err != nil { - b.Logger().Error("error storing the ca locally", err) + b.Logger().Error("error storing the ca locally", "error", err) } ca_chain_combined := strings.Join(ca_chain, "") // store as a single PEM chain @@ -304,12 +322,12 @@ func fetchCAInfo(ctx context.Context, req *logical.Request, b *keyfactorBackend, // 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) + b.Logger().Error("error creating ca chain entry", "error", err) } err = req.Storage.Put(ctx, caChainStorageEntry) if err != nil { - b.Logger().Error("error storing the ca chain locally", err) + b.Logger().Error("error storing the ca chain locally", "error", err) } if includeChain { @@ -390,7 +408,7 @@ func fetchCertBySerial(ctx context.Context, req *logical.Request, prefix, serial return certEntry, nil } -func fetchCertIssuedByCA(ctx context.Context, req *logical.Request, b *keyfactorBackend, caName string) (KeyfactorCertResponse, error) { +func fetchCertIssuedByCA(ctx context.Context, req *logical.Request, b *keyfactorBackend, caName string) (*v1.CertificatesCertificateRetrievalResponse, error) { // call certificates endpoint, limit results to 1, filter by CA name config, err := b.fetchConfig(ctx, req.Storage) if err != nil { @@ -403,56 +421,37 @@ func fetchCertIssuedByCA(ctx context.Context, req *logical.Request, b *keyfactor // get the client client, err := b.getClient(ctx, req.Storage) if err != nil { - b.Logger().Error("unable to create the http client") + b.Logger().Error(fmt.Sprintf("unable to create the http client: %s", err)) + return nil, fmt.Errorf("unable to create the Keyfactor client: %w", err) } - // 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" - b.Logger().Debug("url: " + reqUrl) + // Send request and check status - httpReq, err := http.NewRequest("GET", reqUrl, nil) - if err != nil { - b.Logger().Info("Error forming request: {{err}}", err) - } + b.Logger().Debug(fmt.Sprintf("calling API with to fetch cert issued by %s", caName)) - httpReq.Header.Add("x-keyfactor-requested-with", "APIClient") - httpReq.Header.Add("content-type", "application/json") + certs, httpResponse, err := client.V1.CertificateApi.NewGetCertificatesRequest(ctx).QueryString("CA -eq \"" + caName + "\"").ReturnLimit(1).Execute() - // Send request and check status - 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 cert: {{err}}", err) + b.Logger().Error(fmt.Sprintf("failed to retreive cert: %s", err.Error())) 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 nil, fmt.Errorf("error downloading certificate. returned status = %d\n ", res.StatusCode) - } - // Read response and return certificate and key - defer res.Body.Close() + if httpResponse == nil { + return nil, errors.New("no response received from Command when fetching a cert issued by the CA") + } - body, err := io.ReadAll(res.Body) - if err != nil { - b.Logger().Info("Error reading response: {{err}}", err) - return nil, err + if httpResponse.StatusCode != 200 { + b.Logger().Error("request failed: server returned " + fmt.Sprint(httpResponse.StatusCode) + " " + httpResponse.Status) + return nil, fmt.Errorf("error downloading certificate. returned status = %d %s", httpResponse.StatusCode, httpResponse.Status) } - // Parse response - var r KeyfactorCertResponse - json.Unmarshal(body, &r) - b.Logger().Debug("response = ", r) + b.Logger().Debug(fmt.Sprintf("cert issued by CA response: %+v", certs)) - if len(r) == 0 { + if len(certs) == 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) } - return r, nil + return &certs[0], nil } func fetchChainAndCAForCert(ctx context.Context, req *logical.Request, b *keyfactorBackend, kfCertId int) ([]string, string, error) { @@ -467,58 +466,55 @@ func fetchChainAndCAForCert(ctx context.Context, req *logical.Request, b *keyfac // get the client client, err := b.getClient(ctx, req.Storage) if err != nil { - b.Logger().Error("unable to create the http client") + b.Logger().Error(fmt.Sprintf("unable to create the http client: %s", err)) + return nil, "", fmt.Errorf("unable to create the Keyfactor client: %w", err) } - // This is only needed when running as a vault extension - b.Logger().Debug("Closing idle connections") - client.httpClient.CloseIdleConnections() // Build request - 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", reqUrl, payload) - if err != nil { - b.Logger().Info("Error forming request: %s", err) + + certId := int32(kfCertId) + chainOrder := "endentityfirst" + includeChain := true + + certDownloadRequest := v1.CertificatesCertificateDownloadRequest{ + CertID: *v1.NewNullableInt32(&certId), + ChainOrder: *v1.NewNullableString(&chainOrder), + IncludeChain: &includeChain, } - httpReq.Header.Add("x-keyfactor-requested-with", "APIClient") - httpReq.Header.Add("content-type", "application/json") - httpReq.Header.Add("x-certificateformat", "P7B") + + apiRequest := client.V1.CertificateApi.NewCreateCertificatesDownloadRequest(ctx).CertificatesCertificateDownloadRequest(certDownloadRequest).XCertificateformat("P7B") + + reqMap, _ := certDownloadRequest.ToMap() // Send request and check status - b.Logger().Debug("About to connect to " + config.KeyfactorUrl + "for cert retrieval") - res, err := client.httpClient.Do(httpReq) + b.Logger().Debug(fmt.Sprintf("request parameters: %s", reqMap)) + b.Logger().Debug("making request for cert retrieval") + + response, httpResponse, err := apiRequest.Execute() + if err != nil { b.Logger().Info(fmt.Sprintf("failed getting cert: %s", err)) - return nil, "", err + return nil, "", fmt.Errorf("failed to retreive CA Chain: %w", 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 nil, "", fmt.Errorf("error downloading certificate. returned status = %d\n ", res.StatusCode) + if httpResponse == nil { + return nil, "", errors.New("no response received from Command when downloading the certificate chain") } - - // 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: %s", err) - return nil, "", err + if httpResponse.StatusCode != 200 { + b.Logger().Error("request failed: server returned " + fmt.Sprint(httpResponse.StatusCode) + " " + httpResponse.Status) + return nil, "", fmt.Errorf("error downloading certificate. returned status = %d %s", httpResponse.StatusCode, httpResponse.Status) } - var r KeyfactorCertDownloadResponse - json.Unmarshal(body, &r) - b.Logger().Debug("response = ", r) - certs, p7bErr := ConvertBase64P7BtoCertificates(r.Content) + // Read response and convert to x509 certificates + + certs, p7bErr := ConvertBase64P7BtoCertificates(response.GetContent()) if p7bErr != nil { return nil, "", p7bErr } - // first cert is leaf, next cert is CA, remaining certs are chain + // first cert is leaf, next cert is CA, remaining certs are chain + if len(certs) < 2 { + return nil, "", fmt.Errorf("expected at least a leaf and an issuing CA certificate in the downloaded chain, but got %d certificate(s)", len(certs)) + } ca_chain := certs[1:] ca_cert := certs[1] @@ -548,7 +544,7 @@ func fetchChainAndCAForCert(ctx context.Context, req *logical.Request, b *keyfac } func normalizeSerial(serial string) string { - return strings.Replace(strings.ToLower(serial), ":", "-", -1) + return strings.Replace(strings.ToUpper(serial), ":", "-", -1) } // ConvertBase64P7BtoCertificates takes a base64 encoded P7B certificate string and returns a slice of *x509.Certificate. @@ -568,98 +564,3 @@ func ConvertBase64P7BtoCertificates(base64P7B string) ([]*x509.Certificate, erro // 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) - } - - // Parse the PKCS#7 structure. - p7, err := pkcs7.Parse(decodedBytes) - - if err != nil { - return []string{}, fmt.Errorf("error parsing PKCS#7 data: %w", err) - } - - // Initialize an empty string to append the PEM encoded certificates. - var pemEncodedCerts []string - - // 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, - } - pemEncoded := pem.EncodeToMemory(pemBlock) - pemEncodedCerts = append(pemEncodedCerts, string(pemEncoded)) - } - - return pemEncodedCerts, nil -} - -type KeyfactorCertResponse []struct { - ID int `json:"Id"` - Thumbprint string `json:"Thumbprint"` - SerialNumber string `json:"SerialNumber"` - IssuedDN string `json:"IssuedDN"` - IssuedCN string `json:"IssuedCN"` - ImportDate time.Time `json:"ImportDate"` - NotBefore time.Time `json:"NotBefore"` - NotAfter time.Time `json:"NotAfter"` - IssuerDN string `json:"IssuerDN"` - PrincipalID interface{} `json:"PrincipalId"` - TemplateID interface{} `json:"TemplateId"` - CertState int `json:"CertState"` - KeySizeInBits int `json:"KeySizeInBits"` - KeyType int `json:"KeyType"` - RequesterID int `json:"RequesterId"` - IssuedOU interface{} `json:"IssuedOU"` - IssuedEmail interface{} `json:"IssuedEmail"` - KeyUsage int `json:"KeyUsage"` - SigningAlgorithm string `json:"SigningAlgorithm"` - CertStateString string `json:"CertStateString"` - KeyTypeString string `json:"KeyTypeString"` - RevocationEffDate interface{} `json:"RevocationEffDate"` - RevocationReason interface{} `json:"RevocationReason"` - RevocationComment interface{} `json:"RevocationComment"` - CertificateAuthorityID int `json:"CertificateAuthorityId"` - CertificateAuthorityName string `json:"CertificateAuthorityName"` - TemplateName interface{} `json:"TemplateName"` - ArchivedKey bool `json:"ArchivedKey"` - HasPrivateKey bool `json:"HasPrivateKey"` - PrincipalName interface{} `json:"PrincipalName"` - CertRequestID interface{} `json:"CertRequestId"` - RequesterName string `json:"RequesterName"` - ContentBytes string `json:"ContentBytes"` - ExtendedKeyUsages []interface{} `json:"ExtendedKeyUsages"` - SubjectAltNameElements []interface{} `json:"SubjectAltNameElements"` - CRLDistributionPoints []interface{} `json:"CRLDistributionPoints"` - LocationsCount []interface{} `json:"LocationsCount"` - SSLLocations []interface{} `json:"SSLLocations"` - Locations []interface{} `json:"Locations"` - Metadata Metadata `json:"Metadata"` - CertificateKeyID int `json:"CertificateKeyId"` - CARowIndex int `json:"CARowIndex"` - DetailedKeyUsage DetailedKeyUsage `json:"DetailedKeyUsage"` - KeyRecoverable bool `json:"KeyRecoverable"` -} -type Metadata struct { -} -type DetailedKeyUsage struct { - CrlSign bool `json:"CrlSign"` - DataEncipherment bool `json:"DataEncipherment"` - DecipherOnly bool `json:"DecipherOnly"` - DigitalSignature bool `json:"DigitalSignature"` - EncipherOnly bool `json:"EncipherOnly"` - KeyAgreement bool `json:"KeyAgreement"` - KeyCertSign bool `json:"KeyCertSign"` - KeyEncipherment bool `json:"KeyEncipherment"` - NonRepudiation bool `json:"NonRepudiation"` - HexCode string `json:"HexCode"` -} - -type KeyfactorCertDownloadResponse struct { - Content string `json:"Content"` -} diff --git a/cert_util_test.go b/cert_util_test.go new file mode 100644 index 0000000..1d5bf01 --- /dev/null +++ b/cert_util_test.go @@ -0,0 +1,148 @@ +/* + * Copyright 2026 Keyfactor + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package kfbackend + +import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "testing" + "time" + + "go.mozilla.org/pkcs7" +) + +func TestCheckAllowedDomains(t *testing.T) { + cases := []struct { + name string + allowed []string + subdomains bool + domains []string + wantValid bool + }{ + {"exact match", []string{"example.com"}, false, []string{"example.com"}, true}, + {"subdomain blocked", []string{"example.com"}, false, []string{"a.example.com"}, false}, + {"subdomain allowed", []string{"example.com"}, true, []string{"a.example.com"}, true}, + {"suffix bypass blocked", []string{"example.com"}, true, []string{"evilexample.com"}, false}, + {"wildcard", []string{"*"}, false, []string{"anything.net"}, true}, + {"empty allowed list", nil, false, []string{"x.com"}, false}, + {"case insensitive", []string{"Example.com"}, false, []string{"EXAMPLE.COM"}, true}, + {"multiple one disallowed", []string{"example.com"}, true, []string{"a.example.com", "b.other.com"}, false}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + role := &roleEntry{AllowedDomains: tc.allowed, AllowSubdomains: tc.subdomains} + valid, err := checkAllowedDomains(role, "testrole", tc.domains) + if valid != tc.wantValid { + t.Errorf("valid = %v, want %v (err=%v)", valid, tc.wantValid, err) + } + if tc.wantValid && err != nil { + t.Errorf("unexpected error for allowed case: %v", err) + } + if !tc.wantValid && err == nil { + t.Errorf("expected error for disallowed case, got nil") + } + }) + } +} + +func TestNormalizeSerial(t *testing.T) { + cases := map[string]string{ + "ab:cd:ef": "AB-CD-EF", + "AB-CD": "AB-CD", + "abcdef": "ABCDEF", + "12:Ab:3c": "12-AB-3C", + } + for in, want := range cases { + if got := normalizeSerial(in); got != want { + t.Errorf("normalizeSerial(%q) = %q, want %q", in, got, want) + } + } +} + +func TestParseOtherSANs(t *testing.T) { + m, err := parseOtherSANs([]string{"1.3.6.1.4.1;utf8:hello"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := m["1.3.6.1.4.1"]; len(got) != 1 || got[0] != "hello" { + t.Errorf("parsed map = %v", m) + } + + if _, err := parseOtherSANs([]string{"missing-semicolon"}); err == nil { + t.Error("expected error for missing semicolon") + } + if _, err := parseOtherSANs([]string{"1.2.3;der:xx"}); err == nil { + t.Error("expected error for unsupported (non-utf8) type") + } +} + +func TestGenerateCSR(t *testing.T) { + b, _ := getTestBackend(t) + + csrPEM, keyDER, err := b.generateCSR( + "host.example.com", + []string{"10.0.0.1"}, + []string{"host.example.com", "alt.example.com"}, + ) + if err != nil { + t.Fatalf("generateCSR err: %v", err) + } + + block, _ := pem.Decode([]byte(csrPEM)) + if block == nil || block.Type != "CERTIFICATE REQUEST" { + t.Fatalf("expected a CERTIFICATE REQUEST PEM block, got %v", block) + } + csr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + t.Fatalf("failed to parse generated CSR: %v", err) + } + if err := csr.CheckSignature(); err != nil { + t.Errorf("CSR signature invalid: %v", err) + } + if csr.Subject.CommonName != "host.example.com" { + t.Errorf("CSR CN = %q, want host.example.com", csr.Subject.CommonName) + } + if len(csr.DNSNames) != 2 { + t.Errorf("CSR DNS SANs = %v, want 2 entries", csr.DNSNames) + } + if len(csr.IPAddresses) != 1 { + t.Errorf("CSR IP SANs = %v, want 1 entry", csr.IPAddresses) + } + if _, err := x509.ParsePKCS1PrivateKey(keyDER); err != nil { + t.Errorf("returned private key does not parse as PKCS1: %v", err) + } +} + +func TestConvertBase64P7BtoCertificates(t *testing.T) { + der := makeTestCertDER(t, "ca.example.com", time.Now().Add(24*time.Hour)) + + p7, err := pkcs7.DegenerateCertificate(der) + if err != nil { + t.Fatalf("failed to build degenerate PKCS7: %v", err) + } + b64 := base64.StdEncoding.EncodeToString(p7) + + certs, err := ConvertBase64P7BtoCertificates(b64) + if err != nil { + t.Fatalf("ConvertBase64P7BtoCertificates err: %v", err) + } + if len(certs) != 1 { + t.Fatalf("expected 1 certificate, got %d", len(certs)) + } + if certs[0].Subject.CommonName != "ca.example.com" { + t.Errorf("certificate CN = %q, want ca.example.com", certs[0].Subject.CommonName) + } + + if _, err := ConvertBase64P7BtoCertificates("not valid base64 !!!"); err == nil { + t.Error("expected an error decoding invalid base64") + } +} diff --git a/client.go b/client.go index bd1d75e..39a4168 100644 --- a/client.go +++ b/client.go @@ -1,5 +1,5 @@ /* - * Copyright 2024 Keyfactor + * Copyright 2026 Keyfactor * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -12,17 +12,13 @@ package kfbackend import ( "errors" "fmt" - "net/http" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" + "github.com/Keyfactor/keyfactor-go-client-sdk/v24" ) -type keyfactorClient struct { - httpClient *http.Client -} - -func newClient(config *keyfactorConfig, b *keyfactorBackend) (*keyfactorClient, error) { - client := new(keyfactorClient) +func newClient(config *keyfactorConfig, b *keyfactorBackend) (*keyfactor.APIClient, error) { + b.Logger().Trace("creating a new Keyfactor API client..") if config == nil { return nil, errors.New("client configuration was nil") @@ -40,16 +36,17 @@ func newClient(config *keyfactorConfig, b *keyfactorBackend) (*keyfactorClient, if !isBasicAuth && !isOAuth { return nil, errors.New( "invalid Keyfactor Command client configuration, " + - "please provide a valid Basic auth or OAuth configuration", + "please provide a valid Basic (username/password) or OAuth configuration", ) } - oAuthConfig := &auth_providers.CommandConfigOauth{} - basicAuthConfig := &auth_providers.CommandAuthConfigBasic{} + var conf *auth_providers.Server if isBasicAuth { b.Logger().Debug(fmt.Sprintf("using basic auth with username %s, domain %s and password (hidden)", config.Username, config.Domain)) + basicAuthConfig := &auth_providers.CommandAuthConfigBasic{} + basicAuthConfig.WithCommandHostName(hostname). WithCommandAPIPath(config.CommandAPIPath). WithSkipVerify(config.SkipTLSVerify). @@ -64,21 +61,18 @@ func newClient(config *keyfactorConfig, b *keyfactorBackend) (*keyfactorClient, errMsg := fmt.Sprintf("[ERROR] unable to authenticate with provided basic auth credentials: %s", bErr.Error()) b.Logger().Error(errMsg) return nil, bErr - } else { - b.Logger().Debug("successfully authenticated using basic auth") } + b.Logger().Debug("successfully authenticated using basic auth") - client.httpClient, bErr = basicAuthConfig.GetHttpClient() - - if bErr != nil { - errMsg := fmt.Sprintf("[ERROR] there was an error retreiving the basic auth http client: %s", bErr.Error()) - b.Logger().Error(errMsg) - return nil, bErr - } + conf = basicAuthConfig.GetServerConfig() } else if isOAuth { + oAuthConfig := &auth_providers.CommandConfigOauth{} + b.Logger().Debug(fmt.Sprintf("using oAuth authentication with client_id: %s, token_url %s and client_secret: (hidden)", config.ClientId, config.TokenUrl)) - _ = oAuthConfig.WithCommandHostName(hostname). + + oAuthConfig.CommandAuthConfig. + WithCommandHostName(hostname). WithCommandAPIPath(config.CommandAPIPath). WithSkipVerify(config.SkipTLSVerify). WithCommandCACert(config.CommandCertPath) @@ -92,18 +86,22 @@ func newClient(config *keyfactorConfig, b *keyfactorBackend) (*keyfactorClient, WithAudience(config.Audience). Authenticate() + conf = oAuthConfig.GetServerConfig() + if oErr != nil { errMsg := fmt.Sprintf("[ERROR] unable to authenticate with provided oAuth credentials: %s", oErr.Error()) b.Logger().Error(errMsg) return nil, oErr } + } - client.httpClient, oErr = oAuthConfig.GetHttpClient() - if oErr != nil { - errMsg := fmt.Sprintf("[ERROR] there was an error retreiving the oAuth http client: %s", oErr.Error()) - b.Logger().Error(errMsg) - return nil, oErr - } + c, err := keyfactor.NewAPIClient(conf) + + if err != nil { + errMsg := fmt.Sprintf("[ERROR] there was an error creating the Keyfactor client: %s", err.Error()) + b.Logger().Error(errMsg) + return nil, err } - return client, nil + + return c, nil } diff --git a/cmd/keyfactor/main.go b/cmd/keyfactor/main.go index 5c9e255..62413ab 100644 --- a/cmd/keyfactor/main.go +++ b/cmd/keyfactor/main.go @@ -1,5 +1,5 @@ /* - * Copyright 2024 Keyfactor + * Copyright 2026 Keyfactor * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -26,10 +26,11 @@ func main() { tlsConfig := apiClientMeta.GetTLSConfig() tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) - if err := plugin.Serve(&plugin.ServeOpts{ + err := plugin.ServeMultiplex(&plugin.ServeOpts{ BackendFactoryFunc: kfbackend.Factory, TLSProviderFunc: tlsProviderFunc, - }); err != nil { + }) + if err != nil { logger := hclog.New(&hclog.LoggerOptions{}) logger.Error("plugin shutting down", "error", err) os.Exit(1) diff --git a/fields.go b/fields.go index a493d9a..70333a3 100644 --- a/fields.go +++ b/fields.go @@ -1,5 +1,5 @@ /* - * Copyright 2024 Keyfactor + * Copyright 2026 Keyfactor * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -45,7 +45,7 @@ request`, Type: framework.TypeString, Description: `The requested common name; if you want more than one, specify the alternative names in the -alt_names map. If email protection is enabled +dns_sans. If email protection is enabled in the role, this may be an email address.`, Required: true, } diff --git a/go.mod b/go.mod index f5d39b5..9de44ee 100644 --- a/go.mod +++ b/go.mod @@ -1,68 +1,124 @@ module github.com/keyfactor/hashicorp-vault-secrets-engine -go 1.23 +go 1.24.0 -toolchain go1.23.3 +toolchain go1.24.3 require ( 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 + github.com/Keyfactor/keyfactor-go-client-sdk/v24 v24.0.0 + github.com/hashicorp/errwrap v1.1.0 + github.com/hashicorp/go-hclog v1.6.3 + github.com/hashicorp/vault/api v1.16.0 + github.com/hashicorp/vault/sdk v0.16.0 go.mozilla.org/pkcs7 v0.9.0 ) require ( + cloud.google.com/go/auth v0.14.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect + cloud.google.com/go/cloudsqlconn v1.4.3 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // 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/Microsoft/go-winio v0.6.2 // indirect + github.com/armon/go-metrics v0.4.1 // 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/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v27.2.1+incompatible // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-jose/go-jose/v4 v4.0.5 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogo/protobuf v1.3.2 // 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/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/certificate-transparency-go v1.3.1 // indirect + github.com/google/s2a-go v0.1.9 // 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 - github.com/hashicorp/go-multierror v1.1.0 // indirect - github.com/hashicorp/go-plugin v1.0.1 // indirect - github.com/hashicorp/go-retryablehttp v0.6.6 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6 // indirect + github.com/hashicorp/go-immutable-radix v1.3.1 // indirect + github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.1 // indirect + github.com/hashicorp/go-kms-wrapping/v2 v2.0.18 // indirect + github.com/hashicorp/go-metrics v0.5.4 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-plugin v1.6.1 // indirect + github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/hashicorp/go-rootcerts v1.0.2 // indirect - github.com/hashicorp/go-sockaddr v1.0.2 // indirect - github.com/hashicorp/go-uuid v1.0.2 // indirect - 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/yamux v0.0.0-20180604194846-3520598351bb // indirect + github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1 // indirect + github.com/hashicorp/go-secure-stdlib/mlock v0.1.3 // indirect + github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 // indirect + github.com/hashicorp/go-secure-stdlib/permitpool v1.0.0 // indirect + github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.1 // indirect + github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect + github.com/hashicorp/go-sockaddr v1.0.7 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/hcl v1.0.1-vault-5 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgconn v1.14.3 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.3 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgtype v1.14.3 // indirect + github.com/jackc/pgx/v4 v4.18.3 // indirect + github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // 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 + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/mitchellh/mapstructure v1.3.2 // indirect - 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/mitchellh/mapstructure v1.5.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/oklog/run v1.1.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect + github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect - go.uber.org/atomic v1.6.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 - google.golang.org/protobuf v1.25.0 // indirect - gopkg.in/square/go-jose.v2 v2.5.1 // indirect + github.com/sasha-s/go-deadlock v0.3.5 // indirect + github.com/stretchr/testify v1.10.0 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + go.uber.org/atomic v1.11.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/oauth2 v0.28.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.10.0 // indirect + google.golang.org/api v0.221.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3566476..1e9111a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,15 @@ -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= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0= +cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go/cloudsqlconn v1.4.3 h1:/WYFbB1NtMtoMxCbqpzzTFPDkxxlLTPme390KEGaEPc= +cloud.google.com/go/cloudsqlconn v1.4.3/go.mod h1:QL3tuStVOO70txb3rs4G8j5uMfo5ztZii8K3oGD3VYA= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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= @@ -12,7 +22,8 @@ github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 h1:WLUI 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/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 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= @@ -21,26 +32,29 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 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/Keyfactor/keyfactor-go-client-sdk/v24 v24.0.0 h1:b7jYhcw4ENQC/VdFiikquI9lkUBt5C84TM9VnH3jzFI= +github.com/Keyfactor/keyfactor-go-client-sdk/v24 v24.0.0/go.mod h1:xIbpgJ9eYfcYeSM0VzP5Q3ifgLCf3yGKKyZtWmqqOi8= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/armon/go-metrics v0.3.0/go.mod h1:zXjbSimjXTd7vOpY8B0/2LpvNvDoXBuplAD+gJD3GYs= -github.com/armon/go-metrics v0.3.3 h1:a9F4rlj7EWWrbj7BYw8J8+x+ZZkJeqzNyRk8hdPF+ro= -github.com/armon/go-metrics v0.3.3/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= -github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= +github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/cenkalti/backoff/v3 v3.0.0 h1:ske+9nBpD9qZsTBoF41nW5L+AIuFBKMeze18XQ3eG1c= -github.com/cenkalti/backoff/v3 v3.0.0/go.mod h1:cIeZDE3IrqwwJl6VUwCN6trj1oXrTS4rc0ij+ULvLYs= +github.com/bufbuild/protocompile v0.10.0 h1:+jW/wnLMLxaCEG8AX9lD0bQ5v9h1RUiMKOBOT5ll9dM= +github.com/bufbuild/protocompile v0.10.0/go.mod h1:G9qQIQo0xZ6Uyj6CMNz0saGmx2so+KONo8/KrELABiY= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 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= @@ -49,375 +63,583 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= -github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= -github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= -github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20200709052629-daa8e1ccc0bc/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= -github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= -github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= -github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= -github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/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= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.1+incompatible h1:fQdiLfW7VLscyoeYEBz7/J8soYFDZV1u6VW6gJEjNMI= +github.com/docker/docker v27.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE= -github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= +github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= +github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= +github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-ldap/ldap/v3 v3.1.3/go.mod h1:3rbOH3jRS2u6jg2rJnKAMLE/xQyCKIveG2Sa/Cohzb8= -github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw= -github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc= +github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 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-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 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= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/certificate-transparency-go v1.3.1 h1:akbcTfQg0iZlANZLn0L9xOeWtyCIdeoYhKrqi5iH3Go= +github.com/google/certificate-transparency-go v1.3.1/go.mod h1:gg+UQlx6caKEDQ9EElFOujyxEQEfOiQzAt6782Bvi8k= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -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/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= -github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI= -github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= -github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v0.16.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6 h1:kBoJV4Xl5FLtBfnBjDvBxeNSy2IRITSGs73HQsFUEjY= +github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6/go.mod h1:y+HSOcOGB48PkUxNyLAiCiY6rEENu+E+Ss4LG8QHwf4= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-immutable-radix v1.1.0 h1:vN9wG1D6KG6YHRTWr8512cxGOVgTMEfgEdSj/hr8MPc= -github.com/hashicorp/go-immutable-radix v1.1.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-kms-wrapping/entropy v0.1.0 h1:xuTi5ZwjimfpvpL09jDE71smCBRpnF5xfo871BSX4gs= -github.com/hashicorp/go-kms-wrapping/entropy v0.1.0/go.mod h1:d1g9WGtAunDNpek8jUIEJnBlbgKS1N2Q61QkHiZyR1g= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE= -github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= +github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.1 h1:KIge4FHZEDb2/xjaWgmBheCTgRL6HV4sgTfDsH876L8= +github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.1/go.mod h1:aHO1EoFD0kBYLBedqxXgalfFT8lrWfP7kpuSoaqGjH0= +github.com/hashicorp/go-kms-wrapping/v2 v2.0.18 h1:DLfC677GfKEpSAFpEWvl1vXsGpEcSHmbhBaPLrdDQHc= +github.com/hashicorp/go-kms-wrapping/v2 v2.0.18/go.mod h1:t/eaR/mi2mw3klfl1WEAuiLKrlZ/Q8cosmsT+RIPLu0= +github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= +github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI= +github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.6.2/go.mod h1:gEx6HMUGxYYhJScX7W1Il64m6cc2C1mDaW3NQ9sY1FY= -github.com/hashicorp/go-retryablehttp v0.6.6 h1:HJunrbHTDDbBb/ay4kxa1n+dLmttUlnP3V9oNE4hmsM= -github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= -github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= -github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= -github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= +github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1 h1:VaLXp47MqD1Y2K6QVrA9RooQiPyCgAbnfeJg44wKuJk= +github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1/go.mod h1:hH8rgXHh9fPSDPerG6WzABHsHF+9ZpLhRI1LPk4JZ8c= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.3 h1:kH3Rhiht36xhAfhuHyWJDgdXXEx9IIZhDGRk24CDhzg= +github.com/hashicorp/go-secure-stdlib/mlock v0.1.3/go.mod h1:ov1Q0oEDjC3+A4BwsG2YdKltrmEw8sf9Pau4V9JQ4Vo= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9 h1:FW0YttEnUNDJ2WL9XcrrfteS1xW8u+sh4ggM8pN5isQ= +github.com/hashicorp/go-secure-stdlib/parseutil v0.1.9/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/permitpool v1.0.0 h1:U6y5MXGiDVOOtkWJ6o/tu1TxABnI0yKTQWJr7z6BpNk= +github.com/hashicorp/go-secure-stdlib/permitpool v1.0.0/go.mod h1:ecDb3o+8D4xtP0nTCufJaAVawHavy5M2eZ64Nq/8/LM= +github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.1 h1:JY+zGg8gOmslwif1fiCqT5Hu1SikLZQcHkmQhCoA9gY= +github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.1/go.mod h1:jW3KCTvdPyAdVecOUwiiO2XaYgUJ/isigt++ISkszkY= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E= -github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk= -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/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= -github.com/hashicorp/vault/sdk v0.1.14-0.20200519221530-14615acda45f/go.mod h1:WX57W2PwkrOPQ6rVQk+dy5/htHIaB4aBM70EwKThu10= -github.com/hashicorp/vault/sdk v0.2.1 h1:S4O6Iv/dyKlE9AUTXGa7VOvZmsCvg36toPKgV4f2P4M= -github.com/hashicorp/vault/sdk v0.2.1/go.mod h1:WfUiO1vYzfBkz1TmoE4ZGU7HD0T0Cl/rZwaxjBkgN4U= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= -github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= +github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/vault/api v1.16.0 h1:nbEYGJiAPGzT9U4oWgaaB0g+Rj8E59QuHKyA5LhwQN4= +github.com/hashicorp/vault/api v1.16.0/go.mod h1:KhuUhzOD8lDSk29AtzNjgAu2kxRA9jL9NAbkFlqvkBA= +github.com/hashicorp/vault/sdk v0.16.0 h1:aHxyMbmIckEt1BjymvD5T71vM0dDu/3ES1cSSJ7ZLJs= +github.com/hashicorp/vault/sdk v0.16.0/go.mod h1:LeU/f5riOSiM012EqXlU+Le++2csDwgiuWWVNH9EWFs= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= +github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= +github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= +github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= +github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= +github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= +github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= +github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= +github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= +github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= +github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= +github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= +github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= +github.com/jackc/pgtype v1.14.3 h1:h6W9cPuHsRWQFTWUZMAKMgG5jSwQI0Zurzdvlx3Plus= +github.com/jackc/pgtype v1.14.3/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= +github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= +github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= +github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= +github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= +github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jhump/protoreflect v1.16.0 h1:54fZg+49widqXYQ0b+usAFHbMkBGR4PpXrsHc8+TBDg= +github.com/jhump/protoreflect v1.16.0/go.mod h1:oYPd7nPvcBw/5wlDfm/AVmU9zH9BgqGCI469pGxfj/8= +github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4= +github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg= +github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY= +github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 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/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 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/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 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/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/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/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/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 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/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= -github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/microsoft/go-mssqldb v1.5.0 h1:CgENxkwtOBNj3Jg6T1X209y2blCfTTcwuOlznd2k9fk= +github.com/microsoft/go-mssqldb v1.5.0/go.mod h1:lmWsjHD8XX/Txr0f8ZqgbEZSC+BZjmEQy/Ms+rLrvho= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= -github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= -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/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= +github.com/pierrec/lz4 v2.6.1+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= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -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/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 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/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= +github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 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= -github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= +github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 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/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= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= +go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 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= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -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.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -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/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 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= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.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/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 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.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= -golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 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-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -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/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.221.0 h1:qzaJfLhDsbMeFee8zBRdt/Nc+xmOuafD/dbdgGfutOU= +google.golang.org/api v0.221.0/go.mod h1:7sOU2+TL4TxUTdbi0gWgAIg7tH5qBXxoyhtL+9x3biQ= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 h1:ToEetK57OidYuqD4Q5w+vfEnPvPpuTwedCNVohYJfNk= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6 h1:2duwAxN2+k0xLNpjnHTXoMUgnv6VPSp5fiqTuwSxjmI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250207221924-e9438ea467c6/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -426,31 +648,30 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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/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= -gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= -gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/installation.txt b/installation.txt index ab3a246..a8a4d65 100644 --- a/installation.txt +++ b/installation.txt @@ -23,8 +23,7 @@ sha256 checksum: > vault secrets enable -6) test the connection by requesting the ca - +6) test the connection by requesting the CA (the CA certificate will need to exist in the Command database) > vault read /ca diff --git a/integration-manifest.json b/integration-manifest.json index 64f1b81..b9c67f0 100644 --- a/integration-manifest.json +++ b/integration-manifest.json @@ -1,10 +1,11 @@ { "$schema": "https://keyfactor.github.io/integration-manifest-schema.json", "integration_type": "api-client", - "name": "keyfactor-vault-secrets-engine", + "name": "Keyfactor Hashicorp Vault Secrets Engine", "status": "production", "support_level": "community", "link_github": false, "update_catalog": false, - "description": "A Vault plugin that allows Vault to use Keyfactor Command as a CA and issue certificates." + "description": "A Vault plugin that allows Vault to use Keyfactor Command as a CA and issue certificates.", + "release_dir": "bin" } \ No newline at end of file diff --git a/path_ca.go b/path_ca.go index 54e17ea..429d9d1 100644 --- a/path_ca.go +++ b/path_ca.go @@ -1,5 +1,5 @@ /* - * Copyright 2024 Keyfactor + * Copyright 2026 Keyfactor * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -61,7 +61,13 @@ func (b *keyfactorBackend) pathFetchCa(ctx context.Context, req *logical.Request caName := data.Get("ca").(string) if caName == "" { b.Logger().Debug("no ca passed, retreiving from config") - caName = b.cachedConfig.CertAuthority + config, err := b.fetchConfig(ctx, req.Storage) + if err != nil { + return nil, err + } + if config != nil { + caName = config.CertAuthority + } } b.Logger().Debug(fmt.Sprintf("ca name = %s", caName)) if caName == "" { @@ -77,7 +83,13 @@ func (b *keyfactorBackend) pathFetchCaChain(ctx context.Context, req *logical.Re caName := data.Get("ca").(string) if caName == "" { b.Logger().Debug("no ca passed, retreiving from config") - caName = b.cachedConfig.CertAuthority + config, err := b.fetchConfig(ctx, req.Storage) + if err != nil { + return nil, err + } + if config != nil { + caName = config.CertAuthority + } } b.Logger().Debug(fmt.Sprintf("ca name = %s", caName)) if caName == "" { diff --git a/path_certs.go b/path_certs.go index 47e7379..e25b103 100644 --- a/path_certs.go +++ b/path_certs.go @@ -1,5 +1,5 @@ /* - * Copyright 2024 Keyfactor + * Copyright 2026 Keyfactor * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -11,14 +11,16 @@ package kfbackend import ( "context" + "crypto/x509" "encoding/base64" "encoding/json" + "encoding/pem" + "errors" "fmt" - "io" - "net/http" "strings" "time" + v1 "github.com/Keyfactor/keyfactor-go-client-sdk/v24/api/keyfactor/v1" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/helper/errutil" @@ -102,7 +104,7 @@ func pathCerts(b *keyfactorBackend) []*framework.Path { }, Callbacks: map[logical.Operation]framework.OperationFunc{ logical.UpdateOperation: b.pathRevokeCert, - logical.CreateOperation: b.pathRevokeCert, + logical.RevokeOperation: b.pathRevokeCert, }, HelpSynopsis: pathRevokeHelpSyn, @@ -117,7 +119,26 @@ func (b *keyfactorBackend) pathFetchCertList(ctx context.Context, req *logical.R return nil, err } - return logical.ListResponse(entries), nil + // Build a key_info map so that listing shows the common name alongside each + // serial number. The common name is parsed from the stored certificate. + keyInfo := map[string]interface{}{} + for _, serial := range entries { + entry, gErr := req.Storage.Get(ctx, "certs/"+serial) + if gErr != nil || entry == nil { + continue + } + commonName := "" + if block, _ := pem.Decode(entry.Value); block != nil { + if parsed, pErr := x509.ParseCertificate(block.Bytes); pErr == nil { + commonName = parsed.Subject.CommonName + } + } + keyInfo[serial] = map[string]interface{}{ + "common_name": commonName, + } + } + + return logical.ListResponseWithInfo(entries, keyInfo), nil } func (b *keyfactorBackend) pathFetchCert(ctx context.Context, req *logical.Request, data *framework.FieldData) (response *logical.Response, retErr error) { @@ -212,8 +233,34 @@ reply: case response.IsError(): return response, nil default: - response.Data["certificate"] = string(certificate) + response.Data["serial_number"] = normalizeSerial(serial) + response.Data["certificate"] = certificate response.Data["revocation_time"] = revocationTime + + // Parse the stored certificate to surface the common name and + // expiration date. Parse failures are non-fatal; we still return what + // we have. + if block, _ := pem.Decode([]byte(certificate)); block != nil { + if parsed, pErr := x509.ParseCertificate(block.Bytes); pErr == nil { + response.Data["common_name"] = parsed.Subject.CommonName + response.Data["expiration"] = parsed.NotAfter.UTC().Format(time.RFC3339) + } else { + b.Logger().Warn("unable to parse stored certificate", "serial", serial, "error", pErr) + } + } + + // Include any Command metadata stored for this certificate. Omit the + // field entirely when there is none, since most certificates won't + // have metadata. + metadata := map[string]interface{}{} + if metaEntry, mErr := req.Storage.Get(ctx, "metadata/"+normalizeSerial(serial)); mErr == nil && metaEntry != nil { + if dErr := metaEntry.DecodeJSON(&metadata); dErr != nil { + b.Logger().Warn("unable to decode stored certificate metadata", "serial", serial, "error", dErr) + } + } + if len(metadata) > 0 { + response.Data["metadata"] = metadata + } } return @@ -271,8 +318,7 @@ func (b *keyfactorBackend) pathSign(ctx context.Context, req *logical.Request, d 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) + if ok && dns_sans_string != nil && dns_sans_string.(string) != "" { dns_sans = strings.Split(dns_sans_string.(string), ",") b.Logger().Debug(fmt.Sprintf("dns_sans = %s", dns_sans)) @@ -300,12 +346,21 @@ func (b *keyfactorBackend) pathSign(ctx context.Context, req *logical.Request, d b.Logger().Debug("no IP SANs provided") } + // load the configuration so we can fall back to configured defaults + config, err := b.fetchConfig(ctx, req.Storage) + if err != nil { + return nil, err + } + if config == nil { + return logical.ErrorResponse("the backend is not configured; write to the config path first"), nil + } + // 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 + caName = config.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") @@ -317,7 +372,7 @@ func (b *keyfactorBackend) pathSign(ctx context.Context, req *logical.Request, d templateName := data.Get("template").(string) if templateName == "" { b.Logger().Debug("no template name in parameters, retrieving from config") - templateName = b.cachedConfig.CertTemplate + templateName = config.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") } @@ -333,9 +388,8 @@ 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_resp := fmt.Errorf("'%s' is not a valid JSON string", metadata) + if !json.Valid([]byte(metadata)) { + err_resp = fmt.Errorf("'%s' is not a valid JSON string", metadata) b.Logger().Error(err_resp.Error()) } @@ -348,6 +402,10 @@ func (b *keyfactorBackend) pathSign(ctx context.Context, req *logical.Request, d if errr != nil { return nil, fmt.Errorf("could not sign csr: %s", errr) } + if len(certs) < 2 { + return nil, fmt.Errorf("expected a certificate and an issuing CA in the enrollment response, but got %d certificate(s)", len(certs)) + } + response := &logical.Response{ Data: map[string]interface{}{ "certificate": certs[0], @@ -356,6 +414,14 @@ func (b *keyfactorBackend) pathSign(ctx context.Context, req *logical.Request, d }, } + // echo the submitted metadata back in the response (it was validated as + // JSON above and parsed by submitCSR); omit it entirely when empty. + metadataMap := map[string]interface{}{} + _ = json.Unmarshal([]byte(metadata), &metadataMap) + if len(metadataMap) > 0 { + response.Data["metadata"] = metadataMap + } + return response, nil } @@ -437,12 +503,21 @@ func (b *keyfactorBackend) pathIssueSignCert(ctx context.Context, req *logical.R ip_sans = strings.Split(ip_sans_string.(string), ",") } + // load the configuration so we can fall back to configured defaults + config, cfgErr := b.fetchConfig(ctx, req.Storage) + if cfgErr != nil { + return nil, cfgErr + } + if config == nil { + return logical.ErrorResponse("the backend is not configured; write to the config path first"), nil + } + // 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 + caName = config.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") @@ -454,11 +529,11 @@ func (b *keyfactorBackend) pathIssueSignCert(ctx context.Context, req *logical.R templateName := data.Get("template").(string) if templateName == "" { b.Logger().Debug("no template name in parameters, retrieving from config") - templateName = b.cachedConfig.CertTemplate + templateName = config.CertTemplate } b.Logger().Debug(fmt.Sprintf("template name: %s", templateName)) - //check role permissions + // verify that any passed metadata string is valid JSON metadata := data.Get("metadata").(string) @@ -466,10 +541,10 @@ func (b *keyfactorBackend) pathIssueSignCert(ctx context.Context, req *logical.R metadata = "{}" } - // verify that any passed metadata string is valid JSON + b.Logger().Debug(fmt.Sprintf("checking validity of metadata JSON... %s", metadata)) - if !b.isValidJSON(metadata) { - err_resp := fmt.Errorf("'%s' is not a valid JSON string", metadata) + if !json.Valid([]byte(metadata)) { + err_resp = fmt.Errorf("'%s' is not a valid JSON string", metadata) b.Logger().Error(err_resp.Error()) } @@ -479,12 +554,18 @@ 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) + csr, key, csrErr := b.generateCSR(cn.(string), ip_sans, dns_sans) + if csrErr != nil { + return nil, fmt.Errorf("could not generate CSR: %w", csrErr) + } 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) } + if len(certs) < 2 { + return nil, fmt.Errorf("expected a certificate and an issuing CA in the enrollment response, but got %d certificate(s)", len(certs)) + } // Conform response to Vault PKI API response := &logical.Response{ @@ -498,14 +579,18 @@ func (b *keyfactorBackend) pathIssueSignCert(ctx context.Context, req *logical.R }, } + // echo the submitted metadata back in the response (it was validated as + // JSON above and parsed by submitCSR); omit it entirely when empty. + metadataMap := map[string]interface{}{} + _ = json.Unmarshal([]byte(metadata), &metadataMap) + if len(metadataMap) > 0 { + response.Data["metadata"] = metadataMap + } + return response, nil } 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) @@ -520,71 +605,102 @@ func (b *keyfactorBackend) pathRevokeCert(ctx context.Context, req *logical.Requ return revokeCert(ctx, b, req, serial, false) } +// decodeStoredKeyfactorID decodes a Keyfactor certificate ID from its stored +// entry. It intentionally uses a plain json.Unmarshal rather than the +// compression-aware logical.StorageEntry.DecodeJSON: the ID is stored as a bare +// JSON number, and DecodeJSON inspects the leading byte for a Vault compression +// canary. An ID whose decimal form begins with a canary character (notably '4', +// the LZ4 canary) would otherwise be misread as compressed data and fail with +// "lz4: bad magic number". The value is always stored uncompressed, so a plain +// unmarshal is correct and backward-compatible. +func decodeStoredKeyfactorID(entry *logical.StorageEntry) (int32, error) { + if entry == nil { + return 0, fmt.Errorf("keyfactor ID storage entry is nil") + } + var id int32 + if err := json.Unmarshal(entry.Value, &id); err != nil { + return 0, err + } + return id, nil +} + // 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) { if b.System().Tainted() { return nil, nil } + serial = strings.ToUpper(serial) + // 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() + b.Logger().Debug(fmt.Sprintf("retreiving the keyfactor ID for cert stored at path: %s", "kfId/"+serial)) 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) + b.Logger().Error("unable to retrieve Keyfactor certificate ID for cert with serial: "+serial, "error", err) return nil, err } - - var keyfactorId int - err = kfId.DecodeJSON(&keyfactorId) - + if kfId == nil { + return logical.ErrorResponse(fmt.Sprintf("no Keyfactor certificate ID is stored for serial %s; it cannot be revoked through this plugin", serial)), nil + } + b.Logger().Debug("retrieved the logical storage entry, decoding...") + keyfactorId, err := decodeStoredKeyfactorID(kfId) if err != nil { - b.Logger().Error("Unable to parse stored certificate ID for cert with serial: "+serial, err) + b.Logger().Error("Unable to parse stored certificate ID for cert with serial: "+serial, "error", err) return nil, err } + b.Logger().Debug(fmt.Sprintf("decoded keyfactor ID value: %d", keyfactorId)) + // set up keyfactor api request - url := b.cachedConfig.KeyfactorUrl + "/" + b.cachedConfig.CommandAPIPath + kf_revoke_path - payload := fmt.Sprintf(`{ - "CertificateIds": [ - %d - ], - "Reason": 0, - "Comment": "%s", - "EffectiveDate": "%s"}, - "CollectionId": 0 - }`, keyfactorId, "via HashiCorp Vault", time.Now().Format(time.RFC3339)) - b.Logger().Debug("Sending revocation request. payload = " + payload) - httpReq, _ := http.NewRequest("POST", url, strings.NewReader(payload)) - - httpReq.Header.Add("x-keyfactor-requested-with", "APIClient") - httpReq.Header.Add("content-type", "application/json") - - res, err := client.httpClient.Do(httpReq) - if err != nil { - b.Logger().Error("Revoke failed: {{err}}", err) - return nil, err + //url := b.cachedConfig.KeyfactorUrl + "/" + b.cachedConfig.CommandAPIPath + kf_revoke_path + + certIds := []int32{keyfactorId} + revokeReason := v1.KeyfactorPKIEnumsRevokeCode(0) + effectiveDate := time.Now().UTC() + revokeComment := "via Hashicorp Vault" + collectionId := int32(0) + + revokeReq := v1.CertificatesRevokeCertificateRequest{ + CertificateIds: certIds, + Reason: &revokeReason, + EffectiveDate: &effectiveDate, + Comment: *v1.NewNullableString(&revokeComment), + CollectionId: *v1.NewNullableInt32(&collectionId), } - r, _ := io.ReadAll(res.Body) - b.Logger().Debug("response received. Status code " + fmt.Sprint(res.StatusCode) + " response body: \n " + string(r[:])) - if res.StatusCode != 204 && res.StatusCode != 200 { - b.Logger().Info("revocation failed: server returned" + fmt.Sprint(res.StatusCode)) - b.Logger().Info("error response = " + string(r[:])) - return nil, fmt.Errorf("revocation failed: server returned %s\n ", res.Status) + // create the api call wrapper object + apiReq := client.V1.CertificateApi.NewCreateCertificatesRevokeRequest(ctx).CertificatesRevokeCertificateRequest(revokeReq) + + // execute request + + _, httpResponse, err := apiReq.Execute() + + if err != nil { + b.Logger().Error(fmt.Sprintf("revocation failed: %s", err)) + return nil, fmt.Errorf("revocation failed: %w", err) + } + if httpResponse == nil { + return nil, errors.New("revocation failed: no response received from Command") } - defer res.Body.Close() + if httpResponse.StatusCode != 204 && httpResponse.StatusCode != 200 { + b.Logger().Info("revocation failed: server returned " + httpResponse.Status) + return nil, fmt.Errorf("revocation failed: server returned %s", httpResponse.Status) + } alreadyRevoked := false var revInfo revocationInfo + b.Logger().Debug("revocation request was successful.") + + b.Logger().Debug("updating values if previously revoked..") + revEntry, err := fetchCertBySerial(ctx, req, "revoked/", serial) if err != nil { switch err.(type) { @@ -603,6 +719,7 @@ func revokeCert(ctx context.Context, b *keyfactorBackend, req *logical.Request, } } + b.Logger().Debug("updating local storage entry..") if !alreadyRevoked { certEntry, err := fetchCertBySerial(ctx, req, "certs/", serial) if err != nil { @@ -614,13 +731,6 @@ func revokeCert(ctx context.Context, b *keyfactorBackend, req *logical.Request, } } if certEntry == nil { - if fromLease { - // We can't write to revoked/ or update the CRL anyway because we don't have the cert, - // and there's no reason to expect this will work on a subsequent - // retry. Just give up and let the lease get deleted. - b.Logger().Warn("expired certificate revoke failed because not found in storage, treating as success", "serial", serial) - return nil, nil - } return logical.ErrorResponse(fmt.Sprintf("certificate with serial %s not found", serial)), nil } b.Logger().Debug("certEntry key = " + certEntry.Key) @@ -653,57 +763,49 @@ func revokeCert(ctx context.Context, b *keyfactorBackend, req *logical.Request, return resp, nil } +// checkAllowedDomains verifies that every requested domain is permitted by the +// role. A domain is allowed when: +// - the role allows any domain ("*" in allowed_domains), or +// - it exactly matches one of the role's allowed_domains, or +// - the role allows subdomains and the domain is a proper subdomain of an +// allowed domain (i.e. ends with "."+allowed, guarding against matches like +// "evilexample.com" against "example.com"). 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 { + allowed := false 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 v == "*" { + allowed = true + break + } + if strings.EqualFold(d, v) { // exact match + allowed = true + break + } + if role.AllowSubdomains && strings.HasSuffix(strings.ToLower(d), "."+strings.ToLower(v)) { + allowed = true + break } } - } - 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 !allowed { + disallowed = append(disallowed, d) + } } - if err_resp != nil { - return false, err_resp + if len(disallowed) > 0 { + return false, fmt.Errorf( + "domain name(s) not allowed for role %s: %s. allowed domains are: %s", + roleName, + strings.Join(disallowed, ","), + strings.Join(role.AllowedDomains, ","), + ) } 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_certs_test.go b/path_certs_test.go new file mode 100644 index 0000000..5badf13 --- /dev/null +++ b/path_certs_test.go @@ -0,0 +1,160 @@ +/* + * Copyright 2026 Keyfactor + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package kfbackend + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/vault/sdk/logical" +) + +func TestFetchCert_Fields(t *testing.T) { + b, storage := getTestBackend(t) + ctx := context.Background() + + serial := "AABBCCDD" + notAfter := time.Now().Add(48 * time.Hour) + + if err := storage.Put(ctx, &logical.StorageEntry{ + Key: "certs/" + serial, + Value: makeTestCertPEM(t, "host.example.com", notAfter), + }); err != nil { + t.Fatal(err) + } + metaEntry, err := logical.StorageEntryJSON("metadata/"+serial, map[string]interface{}{"environment": "prod"}) + if err != nil { + t.Fatal(err) + } + if err := storage.Put(ctx, metaEntry); err != nil { + t.Fatal(err) + } + + resp, err := b.HandleRequest(ctx, &logical.Request{ + Operation: logical.ReadOperation, + Path: "certs/" + serial, + Storage: storage, + }) + if err != nil { + t.Fatalf("cert read err: %v", err) + } + if resp == nil { + t.Fatal("expected cert read response, got nil") + } + + if got := resp.Data["serial_number"]; got != serial { + t.Errorf("serial_number = %v, want %s", got, serial) + } + if got := resp.Data["common_name"]; got != "host.example.com" { + t.Errorf("common_name = %v, want host.example.com", got) + } + if _, ok := resp.Data["certificate"]; !ok { + t.Error("response missing certificate content") + } + if exp, ok := resp.Data["expiration"].(string); !ok || exp == "" { + t.Errorf("expiration = %v, want non-empty RFC3339 string", resp.Data["expiration"]) + } + md, ok := resp.Data["metadata"].(map[string]interface{}) + if !ok { + t.Fatalf("metadata = %v, want map", resp.Data["metadata"]) + } + if md["environment"] != "prod" { + t.Errorf("metadata[environment] = %v, want prod", md["environment"]) + } +} + +func TestFetchCert_MissingMetadataIsEmpty(t *testing.T) { + b, storage := getTestBackend(t) + ctx := context.Background() + + serial := "AB12CD34" + if err := storage.Put(ctx, &logical.StorageEntry{ + Key: "certs/" + serial, + Value: makeTestCertPEM(t, "no-meta.example.com", time.Now().Add(24*time.Hour)), + }); err != nil { + t.Fatal(err) + } + + resp, err := b.HandleRequest(ctx, &logical.Request{ + Operation: logical.ReadOperation, + Path: "certs/" + serial, + Storage: storage, + }) + if err != nil { + t.Fatalf("cert read err: %v", err) + } + if _, ok := resp.Data["metadata"]; ok { + t.Errorf("expected metadata to be omitted when none is stored, got %v", resp.Data["metadata"]) + } +} + +// TestDecodeStoredKeyfactorID is a regression guard for the revoke bug where a +// Keyfactor certificate ID whose JSON representation begins with Vault's LZ4 +// compression canary ('4') failed to decode with "lz4: bad magic number". The +// IDs below include several '4'-prefixed values; if the decode ever reverts to +// the compression-aware logical.StorageEntry.DecodeJSON, those cases will fail. +func TestDecodeStoredKeyfactorID(t *testing.T) { + ids := []int32{4, 42, 456789, 1, 123, 2147483647} + for _, id := range ids { + entry, err := logical.StorageEntryJSON("kfId/TEST", id) + if err != nil { + t.Fatalf("id %d: StorageEntryJSON err: %v", id, err) + } + got, err := decodeStoredKeyfactorID(entry) + if err != nil { + t.Errorf("id %d: decode err: %v", id, err) + continue + } + if got != id { + t.Errorf("id %d: round-trip decoded to %d", id, got) + } + } + + if _, err := decodeStoredKeyfactorID(nil); err == nil { + t.Error("expected an error decoding a nil entry") + } +} + +func TestFetchCertList_IncludesCommonName(t *testing.T) { + b, storage := getTestBackend(t) + ctx := context.Background() + + if err := storage.Put(ctx, &logical.StorageEntry{ + Key: "certs/AA11", + Value: makeTestCertPEM(t, "host1.example.com", time.Now().Add(24*time.Hour)), + }); err != nil { + t.Fatal(err) + } + + resp, err := b.HandleRequest(ctx, &logical.Request{ + Operation: logical.ListOperation, + Path: "certs/", + Storage: storage, + }) + if err != nil { + t.Fatalf("cert list err: %v", err) + } + keys, ok := resp.Data["keys"].([]string) + if !ok || len(keys) != 1 || keys[0] != "AA11" { + t.Fatalf("list keys = %v", resp.Data["keys"]) + } + keyInfo, ok := resp.Data["key_info"].(map[string]interface{}) + if !ok { + t.Fatalf("key_info missing or wrong type: %v", resp.Data["key_info"]) + } + info, ok := keyInfo["AA11"].(map[string]interface{}) + if !ok { + t.Fatalf("key_info[AA11] wrong type: %v", keyInfo["AA11"]) + } + if info["common_name"] != "host1.example.com" { + t.Errorf("key_info common_name = %v, want host1.example.com", info["common_name"]) + } +} diff --git a/path_config.go b/path_config.go index 04daac2..1a47f6c 100644 --- a/path_config.go +++ b/path_config.go @@ -1,5 +1,5 @@ /* - * Copyright 2024 Keyfactor + * Copyright 2026 Keyfactor * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, @@ -38,14 +38,19 @@ type keyfactorConfig struct { CertTemplate string `json:"template"` CertAuthority string `json:"ca"` CommandCertPath string `json:"command_cert_path"` + // Automatic tidy (expired-certificate cleanup) settings. + TidyEnabled bool `json:"tidy_enabled"` + TidyInterval int `json:"tidy_interval"` // seconds between automatic sweeps + TidySafetyBuffer int `json:"tidy_safety_buffer"` // seconds to retain a cert past its expiry before deleting } 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 + // Fast path: return the cached config under a read lock. + b.configLock.RLock() + cached := b.cachedConfig + b.configLock.RUnlock() + if cached != nil { + return cached, nil } entry, err := s.Get(ctx, configPath) @@ -60,7 +65,17 @@ func (b *keyfactorBackend) fetchConfig(ctx context.Context, s logical.Storage) ( if err := entry.DecodeJSON(config); err != nil { return nil, err } + if config.CommandAPIPath == "" { + config.CommandAPIPath = "KeyfactorAPI" + } + // Populate the cache under the write lock, re-checking in case another + // goroutine loaded it while we were reading from storage. + b.configLock.Lock() + defer b.configLock.Unlock() + if b.cachedConfig != nil { + return b.cachedConfig, nil + } b.cachedConfig = config return config, nil @@ -173,6 +188,24 @@ func pathConfig(b *keyfactorBackend) []*framework.Path { Description: "Set this flag to show sensitive values in the output", Required: false, }, + "tidy_enabled": { + Type: framework.TypeBool, + Description: "If set, the plugin periodically removes locally-stored certificates that have expired. Defaults to false.", + Required: false, + Default: false, + }, + "tidy_interval": { + Type: framework.TypeDurationSecond, + Description: "How often the automatic tidy sweep runs when tidy_enabled is true. Defaults to 24h.", + Required: false, + Default: 86400, + }, + "tidy_safety_buffer": { + Type: framework.TypeDurationSecond, + Description: "How long a certificate is retained past its expiry before the tidy sweep removes it. Defaults to 72h.", + Required: false, + Default: 259200, + }, }, Callbacks: map[logical.Operation]framework.OperationFunc{ @@ -212,6 +245,11 @@ func (b *keyfactorBackend) pathConfigRead( password = "(hidden)" } + accessToken := config.AccessToken + if accessToken != "" && !showSensitiveData { + accessToken = "(hidden)" + } + return &logical.Response{ Data: map[string]interface{}{ "url": config.KeyfactorUrl, @@ -223,12 +261,15 @@ func (b *keyfactorBackend) pathConfigRead( "token_url": config.TokenUrl, "scopes": config.Scopes, "audience": config.Audience, - "access_token": config.AccessToken, + "access_token": accessToken, "ca": config.CertAuthority, "template": config.CertTemplate, - "command_cert_path": config.CommandCertPath, - "skip_verify": config.SkipTLSVerify, - "domain": config.Domain, + "command_cert_path": config.CommandCertPath, + "skip_verify": config.SkipTLSVerify, + "domain": config.Domain, + "tidy_enabled": config.TidyEnabled, + "tidy_interval": config.TidyInterval, + "tidy_safety_buffer": config.TidySafetyBuffer, }, }, nil } @@ -240,8 +281,6 @@ func (b *keyfactorBackend) pathConfigUpdate( data *framework.FieldData, ) (*logical.Response, error) { b.Logger().Debug("Calling pathConfigUpdate") - b.configLock.RLock() - defer b.configLock.RUnlock() newConfig := &keyfactorConfig{ KeyfactorUrl: data.Get("url").(string), @@ -256,21 +295,30 @@ func (b *keyfactorBackend) pathConfigUpdate( AccessToken: data.Get("access_token").(string), Scopes: data.Get("scopes").([]string), Audience: data.Get("audience").(string), - Domain: data.Get("domain").(string), - CommandCertPath: data.Get("command_cert_path").(string), - SkipTLSVerify: data.Get("skip_verify").(bool), + Domain: data.Get("domain").(string), + CommandCertPath: data.Get("command_cert_path").(string), + SkipTLSVerify: data.Get("skip_verify").(bool), + TidyEnabled: data.Get("tidy_enabled").(bool), + TidyInterval: data.Get("tidy_interval").(int), + TidySafetyBuffer: data.Get("tidy_safety_buffer").(int), } // Check if the config already exists, to determine if this is a create or // an update, since req.Operation is always 'update' in this handler, and // there's no existence check defined. - existingConfig, err := b.fetchConfig(ctx, req.Storage) + existing, err := b.fetchConfig(ctx, req.Storage) if err != nil { return nil, err } - if existingConfig == nil { - existingConfig = newConfig + // Work on a copy so we never mutate the cached config in place (other + // goroutines may be reading it), and so a failed write doesn't corrupt + // the in-memory cache. + existingConfig := &keyfactorConfig{} + if existing != nil { + *existingConfig = *existing + } else { + *existingConfig = *newConfig } if username, ok := data.GetOk("username"); ok { @@ -333,20 +381,35 @@ func (b *keyfactorBackend) pathConfigUpdate( existingConfig.CommandCertPath = caCertPath.(string) } + if tidyEnabled, ok := data.GetOk("tidy_enabled"); ok { + existingConfig.TidyEnabled = tidyEnabled.(bool) + } + + if tidyInterval, ok := data.GetOk("tidy_interval"); ok { + existingConfig.TidyInterval = tidyInterval.(int) + } + + if tidySafetyBuffer, ok := data.GetOk("tidy_safety_buffer"); ok { + existingConfig.TidySafetyBuffer = tidySafetyBuffer.(int) + } + entry, err := logical.StorageEntryJSON(configPath, existingConfig) if err != nil { - b.Logger().Error("[ERROR] there was an error converting the values to JSON for storage: %s", err) + b.Logger().Error("there was an error converting the values to JSON for storage", "error", err) return nil, err } if err := req.Storage.Put(ctx, entry); err != nil { - b.Logger().Error("[ERROR] there was an error writing the configuration to the backend: %s", err) + b.Logger().Error("there was an error writing the configuration to the backend", "error", err) return nil, err } - // reset the client so the next invocation will pick up the new configuration + // reset the client so the next invocation will pick up the new + // configuration, then prime the cache with the value we just stored. b.reset() + b.configLock.Lock() b.cachedConfig = existingConfig + b.configLock.Unlock() return nil, nil } @@ -357,6 +420,11 @@ func (b *keyfactorBackend) pathConfigDelete( data *framework.FieldData, ) (*logical.Response, error) { err := req.Storage.Delete(ctx, configPath) + if err == nil { + // Clear the cached config and client so subsequent operations don't + // keep using the deleted configuration from memory. + b.reset() + } return nil, err } diff --git a/path_config_test.go b/path_config_test.go new file mode 100644 index 0000000..2c82120 --- /dev/null +++ b/path_config_test.go @@ -0,0 +1,144 @@ +/* + * Copyright 2026 Keyfactor + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package kfbackend + +import ( + "context" + "testing" + + "github.com/hashicorp/vault/sdk/logical" +) + +func writeConfig(t *testing.T, b *keyfactorBackend, storage logical.Storage, data map[string]interface{}) { + t.Helper() + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config", + Storage: storage, + Data: data, + }) + if err != nil { + t.Fatalf("config write returned error: %v", err) + } + if resp != nil && resp.IsError() { + t.Fatalf("config write returned error response: %v", resp.Error()) + } +} + +func readConfig(t *testing.T, b *keyfactorBackend, storage logical.Storage, showHidden bool) *logical.Response { + t.Helper() + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: "config", + Storage: storage, + Data: map[string]interface{}{"show_hidden": showHidden}, + }) + if err != nil { + t.Fatalf("config read returned error: %v", err) + } + return resp +} + +func TestConfig_WriteReadDelete(t *testing.T) { + b, storage := getTestBackend(t) + ctx := context.Background() + + writeConfig(t, b, storage, map[string]interface{}{ + "url": "https://command.example.com", + "username": "svc", + "password": "secret", + "domain": "EXAMPLE", + }) + + resp := readConfig(t, b, storage, false) + if resp == nil { + t.Fatal("expected config read response, got nil") + } + if got := resp.Data["url"]; got != "https://command.example.com" { + t.Errorf("url = %v", got) + } + if got := resp.Data["username"]; got != "svc" { + t.Errorf("username = %v", got) + } + if got := resp.Data["password"]; got != "(hidden)" { + t.Errorf("password should be masked, got %v", got) + } + if got := resp.Data["api_path"]; got != "KeyfactorAPI" { + t.Errorf("api_path default = %v, want KeyfactorAPI", got) + } + + // show_hidden reveals the secret + resp = readConfig(t, b, storage, true) + if got := resp.Data["password"]; got != "secret" { + t.Errorf("password with show_hidden = %v, want secret", got) + } + + // delete then confirm gone + if _, err := b.HandleRequest(ctx, &logical.Request{ + Operation: logical.DeleteOperation, Path: "config", Storage: storage, + }); err != nil { + t.Fatalf("config delete err: %v", err) + } + resp = readConfig(t, b, storage, false) + if resp != nil { + t.Errorf("expected nil response after delete, got %v", resp.Data) + } +} + +// TestConfig_MergePreservesExisting exercises the update-merge logic: writing a +// single new field must not clear previously-set fields. +func TestConfig_MergePreservesExisting(t *testing.T) { + b, storage := getTestBackend(t) + + writeConfig(t, b, storage, map[string]interface{}{ + "url": "https://command.example.com", + "username": "svc", + }) + writeConfig(t, b, storage, map[string]interface{}{ + "password": "pw", + }) + + resp := readConfig(t, b, storage, true) + if got := resp.Data["username"]; got != "svc" { + t.Errorf("username lost after partial update: %v", got) + } + if got := resp.Data["url"]; got != "https://command.example.com" { + t.Errorf("url lost after partial update: %v", got) + } + if got := resp.Data["password"]; got != "pw" { + t.Errorf("password = %v, want pw", got) + } +} + +// TestFetchConfig_APIPathDefault verifies fetchConfig applies the KeyfactorAPI +// default when a stored config has an empty api_path. +func TestFetchConfig_APIPathDefault(t *testing.T) { + b, storage := getTestBackend(t) + ctx := context.Background() + + entry, err := logical.StorageEntryJSON(configPath, &keyfactorConfig{KeyfactorUrl: "https://c.example.com"}) + if err != nil { + t.Fatal(err) + } + if err := storage.Put(ctx, entry); err != nil { + t.Fatal(err) + } + + cfg, err := b.fetchConfig(ctx, storage) + if err != nil { + t.Fatalf("fetchConfig err: %v", err) + } + if cfg == nil { + t.Fatal("expected config, got nil") + } + if cfg.CommandAPIPath != "KeyfactorAPI" { + t.Errorf("CommandAPIPath = %q, want KeyfactorAPI", cfg.CommandAPIPath) + } +} diff --git a/path_revoke.go b/path_revoke.go deleted file mode 100644 index e7932f1..0000000 --- a/path_revoke.go +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2024 Keyfactor - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions - * and limitations under the License. - */ - -package kfbackend - -// import ( -// "context" -// "strings" - -// "github.com/hashicorp/vault/sdk/framework" -// "github.com/hashicorp/vault/sdk/helper/consts" -// "github.com/hashicorp/vault/sdk/logical" -// ) - -// func pathRevoke(b *keyfactorBackend) *framework.Path { -// return &framework.Path{ -// Pattern: `revoke/?$`, - -// Fields: map[string]*framework.FieldSchema{ -// "serial": { -// Type: framework.TypeString, -// Description: `The cerial number of the certificate to revoke`, -// }, -// }, -// Callbacks: map[logical.Operation]framework.OperationFunc{ -// logical.UpdateOperation: b.pathRevokeWrite, -// logical.CreateOperation: b.pathRevokeWrite, -// }, - -// HelpSynopsis: pathRevokeHelpSyn, -// HelpDescription: pathRevokeHelpDesc, -// } -// } - -// func (b *keyfactorBackend) pathRevokeWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { -// //path := data.Get("path").(string) -// //b.Logger().Debug("path = " + path) - -// 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 -// } - -// // We store and identify by lowercase colon-separated hex, but other -// // utilities use dashes and/or uppercase, so normalize -// serial = strings.Replace(strings.ToLower(serial), "-", ":", -1) - -// return revokeCert(ctx, b, req, serial, false) -// } - -// const pathRevokeHelpSyn = ` -// Revoke a certificate by serial number. -// ` - -// const pathRevokeHelpDesc = ` -// This allows certificates to be revoked using its serial number. A root token is required. -// ` diff --git a/path_roles.go b/path_roles.go index 90f6f8c..f3c5bdf 100644 --- a/path_roles.go +++ b/path_roles.go @@ -1,5 +1,5 @@ /* - * Copyright 2024 Keyfactor + * Copyright 2026 Keyfactor * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/path_roles_test.go b/path_roles_test.go new file mode 100644 index 0000000..5bd171e --- /dev/null +++ b/path_roles_test.go @@ -0,0 +1,125 @@ +/* + * Copyright 2026 Keyfactor + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package kfbackend + +import ( + "context" + "testing" + + "github.com/hashicorp/vault/sdk/logical" +) + +func TestRole_CRUD(t *testing.T) { + b, storage := getTestBackend(t) + ctx := context.Background() + + // create + resp, err := b.HandleRequest(ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "roles/web", + Storage: storage, + Data: map[string]interface{}{ + "allowed_domains": "example.com", + "allow_subdomains": true, + }, + }) + if err != nil { + t.Fatalf("role create err: %v", err) + } + if resp != nil && resp.IsError() { + t.Fatalf("role create error response: %v", resp.Error()) + } + + // read + resp, err = b.HandleRequest(ctx, &logical.Request{ + Operation: logical.ReadOperation, Path: "roles/web", Storage: storage, + }) + if err != nil { + t.Fatalf("role read err: %v", err) + } + if resp == nil { + t.Fatal("expected role read response, got nil") + } + ad, ok := resp.Data["allowed_domains"].([]string) + if !ok || len(ad) != 1 || ad[0] != "example.com" { + t.Errorf("allowed_domains = %v", resp.Data["allowed_domains"]) + } + if resp.Data["allow_subdomains"] != true { + t.Errorf("allow_subdomains = %v, want true", resp.Data["allow_subdomains"]) + } + + // list + resp, err = b.HandleRequest(ctx, &logical.Request{ + Operation: logical.ListOperation, Path: "roles/", Storage: storage, + }) + if err != nil { + t.Fatalf("role list err: %v", err) + } + keys, ok := resp.Data["keys"].([]string) + if !ok || len(keys) != 1 || keys[0] != "web" { + t.Errorf("list keys = %v", resp.Data["keys"]) + } + + // delete + if _, err = b.HandleRequest(ctx, &logical.Request{ + Operation: logical.DeleteOperation, Path: "roles/web", Storage: storage, + }); err != nil { + t.Fatalf("role delete err: %v", err) + } + resp, err = b.HandleRequest(ctx, &logical.Request{ + Operation: logical.ReadOperation, Path: "roles/web", Storage: storage, + }) + if err != nil { + t.Fatalf("role read-after-delete err: %v", err) + } + if resp != nil { + t.Errorf("expected nil response after delete, got %v", resp.Data) + } +} + +func TestRole_Validation(t *testing.T) { + b, storage := getTestBackend(t) + ctx := context.Background() + + cases := []struct { + name string + data map[string]interface{} + }{ + { + name: "weak RSA key", + data: map[string]interface{}{"key_type": "rsa", "key_bits": 1024}, + }, + { + name: "ttl greater than max_ttl", + data: map[string]interface{}{"ttl": 3600, "max_ttl": 60}, + }, + { + name: "invalid ext_key_usage_oid", + data: map[string]interface{}{"ext_key_usage_oids": "not-an-oid"}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + resp, err := b.HandleRequest(ctx, &logical.Request{ + Operation: logical.UpdateOperation, + Path: "roles/badrole", + Storage: storage, + Data: tc.data, + }) + if err != nil { + t.Fatalf("unexpected transport error: %v", err) + } + if resp == nil || !resp.IsError() { + t.Fatalf("expected an error response, got %v", resp) + } + }) + } +} diff --git a/path_tidy.go b/path_tidy.go new file mode 100644 index 0000000..e69c13c --- /dev/null +++ b/path_tidy.go @@ -0,0 +1,291 @@ +/* + * Copyright 2026 Keyfactor + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package kfbackend + +import ( + "context" + "crypto/x509" + "encoding/pem" + "fmt" + "time" + + "github.com/hashicorp/vault/sdk/framework" + "github.com/hashicorp/vault/sdk/logical" +) + +const ( + // defaultTidyInterval is used when automatic tidy is enabled but no + // interval has been configured. + defaultTidyInterval = 24 * time.Hour + // defaultTidySafetyBuffer is how long an expired certificate is retained + // before the sweep removes it, when none is configured. + defaultTidySafetyBuffer = 72 * time.Hour +) + +// tidyStatus captures the outcome of the most recent tidy sweep so it can be +// surfaced through the tidy/status read endpoint. +type tidyStatus struct { + state string // "running", "finished", or "error" + started time.Time + finished time.Time + examined int + deleted int + skipped int + errString string +} + +func pathTidy(b *keyfactorBackend) []*framework.Path { + return []*framework.Path{ + { + Pattern: "tidy", + Fields: map[string]*framework.FieldSchema{ + "safety_buffer": { + Type: framework.TypeDurationSecond, + Description: "The amount of time an expired certificate is retained before it is removed. Defaults to 72h.", + Default: 259200, + }, + }, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathTidyWrite, + }, + HelpSynopsis: pathTidyHelpSyn, + HelpDescription: pathTidyHelpDesc, + }, + { + Pattern: "tidy/status", + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathTidyStatusRead, + }, + HelpSynopsis: pathTidyStatusHelpSyn, + HelpDescription: pathTidyStatusHelpDesc, + }, + } +} + +// pathTidyWrite triggers a one-off tidy sweep on demand. +func (b *keyfactorBackend) pathTidyWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + safetyBuffer := time.Duration(data.Get("safety_buffer").(int)) * time.Second + if safetyBuffer < 0 { + return logical.ErrorResponse("safety_buffer must not be negative"), nil + } + return b.startTidy(req.Storage, safetyBuffer), nil +} + +// pathTidyStatusRead returns the outcome of the most recent tidy sweep. +func (b *keyfactorBackend) pathTidyStatusRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + b.tidyLock.Lock() + defer b.tidyLock.Unlock() + + resp := &logical.Response{ + Data: map[string]interface{}{ + "running": b.tidyRunning, + }, + } + if !b.lastTidy.IsZero() { + resp.Data["last_run"] = b.lastTidy.UTC().Format(time.RFC3339) + } + if b.tidyResult != nil { + s := b.tidyResult + resp.Data["state"] = s.state + resp.Data["certs_examined"] = s.examined + resp.Data["certs_deleted"] = s.deleted + resp.Data["certs_skipped"] = s.skipped + if !s.started.IsZero() { + resp.Data["time_started"] = s.started.UTC().Format(time.RFC3339) + } + if !s.finished.IsZero() { + resp.Data["time_finished"] = s.finished.UTC().Format(time.RFC3339) + } + if s.errString != "" { + resp.Data["error"] = s.errString + } + } + return resp, nil +} + +// startTidy launches the sweep in the background unless one is already running. +// It returns a response with a warning describing what happened; the sweep +// itself runs asynchronously so it never blocks the request or the periodic +// loop. +func (b *keyfactorBackend) startTidy(storage logical.Storage, safetyBuffer time.Duration) *logical.Response { + resp := &logical.Response{} + + b.tidyLock.Lock() + if b.tidyRunning { + b.tidyLock.Unlock() + resp.AddWarning("Tidy operation already in progress; ignoring this request.") + return resp + } + b.tidyRunning = true + b.tidyResult = &tidyStatus{ + state: "running", + started: time.Now(), + } + b.tidyLock.Unlock() + + go b.doTidy(storage, safetyBuffer) + + resp.AddWarning("Tidy operation successfully started. Progress is reported in Vault's server logs and via the tidy/status endpoint.") + return resp +} + +// doTidy walks the locally-stored certificates and removes any that have +// expired (past their NotAfter plus the safety buffer), along with their +// associated Keyfactor ID and revocation entries. Certificates that cannot be +// parsed are skipped, never deleted, to avoid removing data we don't fully +// understand. +func (b *keyfactorBackend) doTidy(storage logical.Storage, safetyBuffer time.Duration) { + // The request context ends when the triggering request returns, so use a + // fresh background context for the async work. + ctx := context.Background() + + var examined, deleted, skipped int + var tidyErr error + + defer func() { + b.tidyLock.Lock() + b.tidyRunning = false + b.lastTidy = time.Now() + if b.tidyResult != nil { + b.tidyResult.finished = b.lastTidy + b.tidyResult.examined = examined + b.tidyResult.deleted = deleted + b.tidyResult.skipped = skipped + if tidyErr != nil { + b.tidyResult.state = "error" + b.tidyResult.errString = tidyErr.Error() + } else { + b.tidyResult.state = "finished" + } + } + b.tidyLock.Unlock() + }() + + serials, err := storage.List(ctx, "certs/") + if err != nil { + tidyErr = err + b.Logger().Error("tidy: failed to list stored certificates", "error", err) + return + } + + now := time.Now() + b.Logger().Info(fmt.Sprintf("tidy: examining %d stored certificate(s)", len(serials))) + + for _, serial := range serials { + // Skip directory-style keys, if any ever appear in this prefix. + if serial == "" || serial[len(serial)-1] == '/' { + continue + } + examined++ + + entry, err := storage.Get(ctx, "certs/"+serial) + if err != nil { + b.Logger().Error("tidy: error reading certificate", "serial", serial, "error", err) + skipped++ + continue + } + if entry == nil || len(entry.Value) == 0 { + skipped++ + continue + } + + block, _ := pem.Decode(entry.Value) + if block == nil { + b.Logger().Warn("tidy: stored value is not PEM; skipping", "serial", serial) + skipped++ + continue + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + b.Logger().Warn("tidy: unable to parse stored certificate; skipping", "serial", serial, "error", err) + skipped++ + continue + } + + if now.After(cert.NotAfter.Add(safetyBuffer)) { + if err := b.deleteCertEntries(ctx, storage, serial); err != nil { + b.Logger().Error("tidy: failed to delete expired certificate", "serial", serial, "error", err) + skipped++ + continue + } + deleted++ + } + } + + b.Logger().Info(fmt.Sprintf("tidy: complete. examined=%d deleted=%d skipped=%d", examined, deleted, skipped)) +} + +// deleteCertEntries removes a certificate and all of its associated storage +// entries. +func (b *keyfactorBackend) deleteCertEntries(ctx context.Context, storage logical.Storage, serial string) error { + for _, key := range []string{"certs/" + serial, "kfId/" + serial, "revoked/" + serial, "metadata/" + serial} { + if err := storage.Delete(ctx, key); err != nil { + return fmt.Errorf("error deleting %s: %w", key, err) + } + } + return nil +} + +// periodicFunc is invoked by Vault (roughly once a minute, on the active node +// only) and runs the tidy sweep when it is enabled and the configured interval +// has elapsed. +func (b *keyfactorBackend) periodicFunc(ctx context.Context, req *logical.Request) error { + config, err := b.fetchConfig(ctx, req.Storage) + if err != nil { + b.Logger().Error("tidy: unable to load configuration for periodic sweep", "error", err) + return nil + } + if config == nil || !config.TidyEnabled { + return nil + } + + interval := time.Duration(config.TidyInterval) * time.Second + if interval <= 0 { + interval = defaultTidyInterval + } + + b.tidyLock.Lock() + due := !b.tidyRunning && (b.lastTidy.IsZero() || time.Since(b.lastTidy) >= interval) + b.tidyLock.Unlock() + if !due { + return nil + } + + safetyBuffer := time.Duration(config.TidySafetyBuffer) * time.Second + if safetyBuffer <= 0 { + safetyBuffer = defaultTidySafetyBuffer + } + + b.Logger().Info("tidy: starting scheduled sweep") + b.startTidy(req.Storage, safetyBuffer) + return nil +} + +const pathTidyHelpSyn = `Tidy up the locally-stored certificate store by removing expired certificates.` + +const pathTidyHelpDesc = ` +This endpoint removes certificates that have expired from the plugin's local +storage, reclaiming space. A certificate is removed once its expiration date +(NotAfter) plus the safety_buffer has passed. The associated Keyfactor ID and +revocation records are removed along with it. + +The sweep runs in the background; use the tidy/status endpoint to view its +progress and results. Automatic, scheduled tidying can be enabled via the +tidy_enabled, tidy_interval, and tidy_safety_buffer settings on the config +endpoint. +` + +const pathTidyStatusHelpSyn = `Return the status of the most recent tidy operation.` + +const pathTidyStatusHelpDesc = ` +This endpoint returns information about the most recent tidy sweep, including +whether one is currently running, when it last ran, and how many certificates +were examined, deleted, and skipped. +` diff --git a/path_tidy_test.go b/path_tidy_test.go new file mode 100644 index 0000000..52900fa --- /dev/null +++ b/path_tidy_test.go @@ -0,0 +1,110 @@ +/* + * Copyright 2026 Keyfactor + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ + +package kfbackend + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/vault/sdk/logical" +) + +func TestDoTidy(t *testing.T) { + b, storage := getTestBackend(t) + ctx := context.Background() + + put := func(key string, val []byte) { + t.Helper() + if err := storage.Put(ctx, &logical.StorageEntry{Key: key, Value: val}); err != nil { + t.Fatalf("seed put %s: %v", key, err) + } + } + + put("certs/EXPIRED", makeTestCertPEM(t, "expired", time.Now().Add(-48*time.Hour))) + put("certs/VALID", makeTestCertPEM(t, "valid", time.Now().Add(72*time.Hour))) + put("certs/GARBAGE", []byte("this is not a PEM certificate")) + + kfEntry, err := logical.StorageEntryJSON("kfId/EXPIRED", int32(42)) + if err != nil { + t.Fatal(err) + } + if err := storage.Put(ctx, kfEntry); err != nil { + t.Fatal(err) + } + metaEntry, err := logical.StorageEntryJSON("metadata/EXPIRED", map[string]interface{}{"env": "test"}) + if err != nil { + t.Fatal(err) + } + if err := storage.Put(ctx, metaEntry); err != nil { + t.Fatal(err) + } + + // Prime the status object the same way startTidy would, then run the sweep + // synchronously. + b.tidyResult = &tidyStatus{state: "running", started: time.Now()} + b.doTidy(storage, 0) + + assertGone := func(key string) { + t.Helper() + e, err := storage.Get(ctx, key) + if err != nil { + t.Fatalf("get %s: %v", key, err) + } + if e != nil { + t.Errorf("expected %s to be deleted, but it remains", key) + } + } + assertPresent := func(key string) { + t.Helper() + e, err := storage.Get(ctx, key) + if err != nil { + t.Fatalf("get %s: %v", key, err) + } + if e == nil { + t.Errorf("expected %s to remain, but it was deleted", key) + } + } + + assertGone("certs/EXPIRED") + assertGone("kfId/EXPIRED") + assertGone("metadata/EXPIRED") + assertPresent("certs/VALID") + assertPresent("certs/GARBAGE") // unparseable entries are skipped, never deleted + + if b.tidyResult.deleted != 1 { + t.Errorf("tidy deleted = %d, want 1", b.tidyResult.deleted) + } + if b.tidyRunning { + t.Error("tidyRunning should be false after doTidy completes") + } +} + +func TestTidyStatus_BeforeAnyRun(t *testing.T) { + b, storage := getTestBackend(t) + + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: "tidy/status", + Storage: storage, + }) + if err != nil { + t.Fatalf("tidy/status read err: %v", err) + } + if resp == nil { + t.Fatal("expected tidy/status response, got nil") + } + if resp.Data["running"] != false { + t.Errorf("running = %v, want false", resp.Data["running"]) + } + if _, ok := resp.Data["last_run"]; ok { + t.Errorf("did not expect last_run before any sweep, got %v", resp.Data["last_run"]) + } +} diff --git a/readme_source.md b/readme_source.md index fb42fc0..9815fc1 100644 --- a/readme_source.md +++ b/readme_source.md @@ -24,17 +24,21 @@ returned to Hashicorp Vault and stored within the Vault Secrets store to then be - [Keyfactor Requirements](#keyfactor-requirements) - [Hashicorp Vault Requirements](#hashicorp-vault-requirements) - [Installation - Keyfactor](#installation---keyfactor) - - [Create the Active Directory service account](#create-the-active-directory-service-account) + - [Create the Service Account in Command](#create-the-service-account-in-command) + - [Assign the user permissions in Keyfactor Command](#assign-the-user-permissions-in-keyfactor-command) - [Create a certificate template](#create-a-certificate-template) - - [Publish the template for the Certificate Authority](#publish-the-template-for-the-certificate-authority) + - [Allow the template to be used for CSR enrollment](#allow-the-template-to-be-used-for-csr-enrollment-through-keyfactor) - [Installation - Vault](#installation---vault) - [Check the Vault server status](#check-the-vault-server-status) - [Install and register the plugin](#install-and-register-the-plugin) - [Configure the plugin](#configure-the-plugin) + - [Basic Authentication](#basic-authentication-configuration) + - [OpenID Connect / oAuth](#openid-connect--oauth-configuration) - [Adding Roles](#adding-roles) - [Using the plugin](#using-the-plugin) - [Issuing Certificates](#issuing-certificates) - [Viewing Certificates](#viewing-certificates) + - [Tidying expired certificates](#tidying-expired-certificates) - [Command Reference](#plugin-command-reference) - [Create/update configuration](#createupdate-configuration) - [Read configuration](#read-configuration) @@ -49,6 +53,8 @@ returned to Hashicorp Vault and stored within the Vault Secrets store to then be - [Sign a CSR](#sign-csr) - [View CA Certificate](#read-ca-cert) - [View CA Certificate Chain](#read-ca-chain) + - [Tidy expired certificates](#tidy-expired-certificates) + - [Read tidy status](#read-tidy-status) ## Overview @@ -98,6 +104,24 @@ issued. The Keyfactor Secrets Engine offers the following enterprise capabilitie > native Vault policies, the roles implemented by the secrets engine plugin, and the template > rules available in Command. +### Per-instance configuration + +The Keyfactor plugin implements a **per-instance configuration**. Because Vault mounts every enabled +secrets engine at its own path, you can register and enable multiple independent instances of this +plugin at the same time, each with its own configuration, certificate store, roles, and default CA and +template. Each instance is scoped to a single Keyfactor Command connection and a single service-account +identity. + +This is a deliberate architectural choice rather than an incidental behavior. It lets you dedicate a +separate plugin instance to each certificate issuance workflow — for example, one instance +authenticating as a service account restricted to web-server templates, and a second instance scoped to +a different CA, template, and identity for client-authentication certificates. Because Vault ACL +policies are applied per mount path, this also allows you to grant different Vault clients access to +different Command identities and issuance policies without them interfering with one another. + +The remainder of this document describes configuring a single instance; repeat the enable and configure +steps for each additional instance you require, giving each a distinct mount path. + ## Compatibility This Vault Plugin has been tested against Hashicorp Vault version 1.10+ and the Keyfactor Platform 9.6+. We provide @@ -133,21 +157,28 @@ document. ### Create the Service Account in Command -This plugin can authenticate via username/password, TLS certificate authentication, and oAuth/openIDConnect. +This plugin authenticates to the Keyfactor Command API using one of two methods: + +- **Basic** — an Active Directory username, password, and domain. +- **OAuth / OpenID Connect** — a client ID, client secret, and token endpoint (or a pre-obtained access token). For the purposes of this document, we will not go into the details of how to create each type of service entity. -Refer to the Keyfactor platform documentation for guidance on creating these service accounts. +Refer to the Keyfactor platform documentation for guidance on creating these service accounts. -The configuration of the plugin will differ slightly for each different approach. Here is a table with the values +The configuration of the plugin will differ slightly for each approach. Here is a table with the values needed for authentication for each approach: -| basic | oAuth | TLS | -| ----------------- | ----- | --- | -| Username | Client ID | Certificate Path | -| Password | Client Secret | | -| AD Domain | Token Endpoint | | +| Basic | oAuth | +| ----- | ----- | +| Username (`username`) | Client ID (`client_id`) | +| Password (`password`) | Client Secret (`client_secret`) | +| AD Domain (`domain`) | Token Endpoint (`token_url`) | + +These values are discussed in greater detail in the [Configure the plugin](#configure-the-plugin) section of this document. -These values will be discussed in greater detail in the [Configure the plugin](#configure-the-plugin) section of this document. +> [!NOTE] +> `command_cert_path` and `skip_verify` control how the plugin *trusts Command's server TLS certificate*; +> they are not authentication credentials. See [Configure the plugin](#configure-the-plugin). ### Assign the user permissions in Keyfactor Command @@ -342,6 +373,12 @@ you may or may not be able to access certain paths. Request certificates using a certain role with the provided details. example: vault write keyfactor/sign/ csr= + ^tidy$ + Tidy up the locally-stored certificate store by removing expired certificates. + + ^tidy/status$ + Return the status of the most recent tidy operation. + ``` If you see this, you have successfully installed the plugin. Now we can configure it for connecting with Command for certificate enrollment. @@ -351,9 +388,9 @@ If you see this, you have successfully installed the plugin. Now we can configu Once the plugin has been successfully installed, the next step is to set the configuration values that will allow it to interact with the Keyfactor platform. -The Keyfactor plugin implements a per-instance configuration which allows multiple instances of the plugin to exist -simultaneously. This could be useful for creating multiple instances of the plugin; each scoped to a specific Command service account identity that -is specific to a particular issuance workflow. +The Keyfactor plugin uses a per-instance configuration (see +[Per-instance configuration](#per-instance-configuration) in the Overview). The settings described below +apply to the single plugin instance you are configuring; repeat these steps for each additional instance. To set a configuration value: @@ -375,8 +412,11 @@ Here is a table of the available configuration paramaters | **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 | 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 +| **skip_verify** | bool | no | _false_ | set this to true to skip verification of the Command **server's** TLS certificate. Intended for test environments only; leave false in production | +| **command_cert_path** | string | no | | the local path to a PEM-encoded CA certificate used to **trust the Command server's TLS certificate**, when it is not already trusted by the host and skip_verify is false. This establishes server trust only; it is not a client authentication credential | +| **tidy_enabled** | bool | no | _false_ | when true, the plugin periodically removes locally-stored certificates that have expired. See [Tidying expired certificates](#tidying-expired-certificates) | +| **tidy_interval** | duration | no | _24h_ | how often the automatic tidy sweep runs when tidy_enabled is true | +| **tidy_safety_buffer** | duration | no | _72h_ | how long an expired certificate is retained past its expiry before the tidy sweep removes it | [^1]: The **ca** and **template** fields can be provided via command line parameters. If they are not provided, the plugin will default to what is set in the configuration values. If neither are available an error will occur. @@ -467,11 +507,16 @@ the Vault secrets store. After certificates are stored in the secrets store, you can then retrieve those certificates at a later time if necessary. To list the certificates that exist within the Vault store, use the LIST option with vault. The only parameter that you need to include is the secrets store name for the store that you would like to read. The system will -then return a list of all of the serial numbers for certificates that are present in that secrets store. +then return the serial numbers of the certificates present in that secrets store. `vault list keyfactor/certs` -The results of the command will be a list of serial numbers for the certificates in that store location: +To also see the common name of each certificate, add the `-detailed` flag (the common name is returned as +additional key information that the standard `vault list` output does not display): + +`vault list -detailed keyfactor/certs` + +The plain command returns a list of serial numbers for the certificates in that store location: ``` Keys @@ -490,7 +535,10 @@ example: `vault read keyfactor/cert/750000276546d818cbe70231b6000000002765` -The response will show the value for that certificate. +The response includes the certificate's serial number, common name, the PEM-encoded certificate content, the +expiration date, and the revocation time (`0` if the certificate has not been revoked). If metadata was submitted +when the certificate was issued, it is also included; the metadata field is omitted when none was provided. For +example: ``` Key Value @@ -542,6 +590,38 @@ LS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ0K revocation_time 0 ``` +### Tidying expired certificates + +Unless a role is configured with `no_store=true`, the plugin keeps a copy of every issued certificate in its +Vault storage (along with the certificate's Keyfactor ID and any revocation record). Over time, expired +certificates accumulate and consume storage. The `tidy` operation reclaims that space by removing certificates +whose expiration date (`NotAfter`), plus a safety buffer, has passed. The associated Keyfactor ID and revocation +records are removed along with each certificate. + +To run a tidy sweep on demand: + +`vault write keyfactor/tidy safety_buffer=72h` + +The sweep runs in the background so it does not block the request. To view its progress and the results of the +most recent run: + +`vault read keyfactor/tidy/status` + +The status includes whether a sweep is currently running, when it last ran, and how many certificates were +examined, deleted, and skipped. + +You can also enable automatic, scheduled tidying through the configuration: + +`vault write keyfactor/config tidy_enabled=true tidy_interval=24h tidy_safety_buffer=72h` + +When enabled, the plugin runs the sweep automatically on the configured interval (on the active node of the +primary cluster). Automatic tidy is **disabled by default**, so no certificates are ever removed until you either +run `keyfactor/tidy` manually or set `tidy_enabled=true`. + +> [!NOTE] +> Certificates that cannot be parsed are skipped and never deleted. Certificates issued using a role with +> `no_store=true` are not stored in the first place, so they are unaffected by tidy. + ## Plugin command reference The following commands are supported by the Keyfactor Hashicorp Vault Secrets Engine plugin. These examples assume the @@ -596,7 +676,19 @@ instance of the plugin is named "keyfactor". ### Read CA cert `vault read keyfactor/ca ca=` +> Note: The certificate for the CA needs to have been imported into Command for this endpoint to return the CA Certificate ### Read CA chain `vault read keyfactor/ca_chain ca=` +> Note: _All_ certificates in the chain need to have been imported into Command for this endpoint to return the CA Certificate Chain + +### Tidy expired certificates + +`vault write keyfactor/tidy safety_buffer=` +> Note: `safety_buffer` is optional and defaults to 72h. The sweep runs in the background. + +### Read tidy status + +`vault read keyfactor/tidy/status` +