// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package pki import ( "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/base64" "encoding/json" "errors" "fmt" "path" "strings" "testing" "time" "github.com/armon/go-metrics" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/helper/testhelpers" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/cryptoutil" "github.com/hashicorp/vault/sdk/helper/jsonutil" "github.com/hashicorp/vault/sdk/helper/testhelpers/schema" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault" "github.com/stretchr/testify/require" "golang.org/x/crypto/acme" ) func TestTidyConfigs(t *testing.T) { t.Parallel() var cfg tidyConfig operations := strings.Split(cfg.AnyTidyConfig(), " / ") require.Greater(t, len(operations), 1, "expected more than one operation") t.Logf("Got tidy operations: %v", operations) lastOp := operations[len(operations)-1] for _, operation := range operations { b, s := CreateBackendWithStorage(t) resp, err := CBWrite(b, s, "config/auto-tidy", map[string]interface{}{ "enabled": true, operation: true, }) requireSuccessNonNilResponse(t, resp, err, "expected to be able to enable auto-tidy operation "+operation) resp, err = CBRead(b, s, "config/auto-tidy") requireSuccessNonNilResponse(t, resp, err, "expected to be able to read auto-tidy operation for operation "+operation) require.True(t, resp.Data[operation].(bool), "expected operation to be enabled after reading auto-tidy config "+operation) resp, err = CBWrite(b, s, "config/auto-tidy", map[string]interface{}{ "enabled": true, operation: false, lastOp: true, }) requireSuccessNonNilResponse(t, resp, err, "expected to be able to disable auto-tidy operation "+operation) resp, err = CBRead(b, s, "config/auto-tidy") requireSuccessNonNilResponse(t, resp, err, "expected to be able to read auto-tidy operation for operation "+operation) require.False(t, resp.Data[operation].(bool), "expected operation to be disabled after reading auto-tidy config "+operation) resp, err = CBWrite(b, s, "tidy", map[string]interface{}{ operation: true, }) requireSuccessNonNilResponse(t, resp, err, "expected to be able to start tidy operation with "+operation) if len(resp.Warnings) > 0 { t.Logf("got warnings while starting manual tidy: %v", resp.Warnings) for _, warning := range resp.Warnings { if strings.Contains(warning, "Manual tidy requested but no tidy operations were set.") { t.Fatalf("expected to be able to enable tidy operation with just %v but got warning: %v / (resp=%v)", operation, warning, resp) } } } lastOp = operation } // pause_duration is tested elsewhere in other tests. type configSafetyBufferValueStr struct { Config string FirstValue int SecondValue int DefaultValue int } configSafetyBufferValues := []configSafetyBufferValueStr{ { Config: "safety_buffer", FirstValue: 1, SecondValue: 2, DefaultValue: int(defaultTidyConfig.SafetyBuffer / time.Second), }, { Config: "issuer_safety_buffer", FirstValue: 1, SecondValue: 2, DefaultValue: int(defaultTidyConfig.IssuerSafetyBuffer / time.Second), }, { Config: "acme_account_safety_buffer", FirstValue: 1, SecondValue: 2, DefaultValue: int(defaultTidyConfig.AcmeAccountSafetyBuffer / time.Second), }, { Config: "revocation_queue_safety_buffer", FirstValue: 1, SecondValue: 2, DefaultValue: int(defaultTidyConfig.QueueSafetyBuffer / time.Second), }, } for _, flag := range configSafetyBufferValues { b, s := CreateBackendWithStorage(t) resp, err := CBRead(b, s, "config/auto-tidy") requireSuccessNonNilResponse(t, resp, err, "expected to be able to read auto-tidy operation for flag "+flag.Config) require.Equal(t, resp.Data[flag.Config].(int), flag.DefaultValue, "expected initial auto-tidy config to match default value for "+flag.Config) resp, err = CBWrite(b, s, "config/auto-tidy", map[string]interface{}{ "enabled": true, "tidy_cert_store": true, flag.Config: flag.FirstValue, }) requireSuccessNonNilResponse(t, resp, err, "expected to be able to set auto-tidy config option "+flag.Config) resp, err = CBRead(b, s, "config/auto-tidy") requireSuccessNonNilResponse(t, resp, err, "expected to be able to read auto-tidy operation for config "+flag.Config) require.Equal(t, resp.Data[flag.Config].(int), flag.FirstValue, "expected value to be set after reading auto-tidy config "+flag.Config) resp, err = CBWrite(b, s, "config/auto-tidy", map[string]interface{}{ "enabled": true, "tidy_cert_store": true, flag.Config: flag.SecondValue, }) requireSuccessNonNilResponse(t, resp, err, "expected to be able to set auto-tidy config option "+flag.Config) resp, err = CBRead(b, s, "config/auto-tidy") requireSuccessNonNilResponse(t, resp, err, "expected to be able to read auto-tidy operation for config "+flag.Config) require.Equal(t, resp.Data[flag.Config].(int), flag.SecondValue, "expected value to be set after reading auto-tidy config "+flag.Config) resp, err = CBWrite(b, s, "tidy", map[string]interface{}{ "tidy_cert_store": true, flag.Config: flag.FirstValue, }) t.Logf("tidy run results: resp=%v/err=%v", resp, err) requireSuccessNonNilResponse(t, resp, err, "expected to be able to start tidy operation with "+flag.Config) if len(resp.Warnings) > 0 { for _, warning := range resp.Warnings { if strings.Contains(warning, "unrecognized parameter") && strings.Contains(warning, flag.Config) { t.Fatalf("warning '%v' claims parameter '%v' is unknown", warning, flag.Config) } } } time.Sleep(2 * time.Second) resp, err = CBRead(b, s, "tidy-status") requireSuccessNonNilResponse(t, resp, err, "expected to be able to start tidy operation with "+flag.Config) t.Logf("got response: %v for config: %v", resp, flag.Config) require.Equal(t, resp.Data[flag.Config].(int), flag.FirstValue, "expected flag to be set in tidy-status for config "+flag.Config) } } func TestAutoTidy(t *testing.T) { t.Parallel() // While we'd like to reduce this duration, we need to wait until // the rollback manager timer ticks. With the new helper, we can // modify the rollback manager timer period directly, allowing us // to shorten the total test time significantly. // // We set the delta CRL time to ensure it executes prior to the // main CRL rebuild, and the new CRL doesn't rebuild until after // we're done. newPeriod := 1 * time.Second // This test requires the periodicFunc to trigger, which requires we stand // up a full test cluster. coreConfig := &vault.CoreConfig{ LogicalBackends: map[string]logical.Factory{ "pki": Factory, }, // See notes below about usage of /sys/raw for reading cluster // storage without barrier encryption. EnableRaw: true, RollbackPeriod: newPeriod, } cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ HandlerFunc: vaulthttp.Handler, }) cluster.Start() defer cluster.Cleanup() client := cluster.Cores[0].Client // Mount PKI err := client.Sys().Mount("pki", &api.MountInput{ Type: "pki", Config: api.MountConfigInput{ DefaultLeaseTTL: "10m", MaxLeaseTTL: "60m", }, }) require.NoError(t, err) // Generate root. resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ "ttl": "40h", "common_name": "Root X1", "key_type": "ec", }) require.NoError(t, err) require.NotNil(t, resp) require.NotEmpty(t, resp.Data) require.NotEmpty(t, resp.Data["issuer_id"]) issuerId := resp.Data["issuer_id"] // Run tidy so status is not empty when we run it later... _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ "tidy_revoked_certs": true, }) require.NoError(t, err) // Setup a testing role. _, err = client.Logical().Write("pki/roles/local-testing", map[string]interface{}{ "allow_any_name": true, "enforce_hostnames": false, "key_type": "ec", }) require.NoError(t, err) // Write the auto-tidy config. _, err = client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{ "enabled": true, "interval_duration": "1s", "tidy_cert_store": true, "tidy_revoked_certs": true, "safety_buffer": "1s", "min_startup_backoff_duration": "1s", "max_startup_backoff_duration": "1s", }) require.NoError(t, err) // Issue a cert and revoke it. resp, err = client.Logical().Write("pki/issue/local-testing", map[string]interface{}{ "common_name": "example.com", "ttl": "10s", }) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, resp.Data) require.NotEmpty(t, resp.Data["serial_number"]) require.NotEmpty(t, resp.Data["certificate"]) leafSerial := resp.Data["serial_number"].(string) leafCert := parseCert(t, resp.Data["certificate"].(string)) // Read cert before revoking resp, err = client.Logical().Read("pki/cert/" + leafSerial) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, resp.Data) require.NotEmpty(t, resp.Data["certificate"]) revocationTime, err := (resp.Data["revocation_time"].(json.Number)).Int64() require.Equal(t, int64(0), revocationTime, "revocation time was not zero") require.Empty(t, resp.Data["revocation_time_rfc3339"], "revocation_time_rfc3339 was not empty") require.Empty(t, resp.Data["issuer_id"], "issuer_id was not empty") _, err = client.Logical().Write("pki/revoke", map[string]interface{}{ "serial_number": leafSerial, }) require.NoError(t, err) // Cert should still exist. resp, err = client.Logical().Read("pki/cert/" + leafSerial) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, resp.Data) require.NotEmpty(t, resp.Data["certificate"]) revocationTime, err = (resp.Data["revocation_time"].(json.Number)).Int64() require.NoError(t, err, "failed converting %s to int", resp.Data["revocation_time"]) revTime := time.Unix(revocationTime, 0) now := time.Now() if !(now.After(revTime) && now.Add(-10*time.Minute).Before(revTime)) { t.Fatalf("parsed revocation time not within the last 10 minutes current time: %s, revocation time: %s", now, revTime) } utcLoc, err := time.LoadLocation("UTC") require.NoError(t, err, "failed to parse UTC location?") rfc3339RevocationTime, err := time.Parse(time.RFC3339Nano, resp.Data["revocation_time_rfc3339"].(string)) require.NoError(t, err, "failed parsing revocation_time_rfc3339 field: %s", resp.Data["revocation_time_rfc3339"]) require.Equal(t, revTime.In(utcLoc), rfc3339RevocationTime.Truncate(time.Second), "revocation times did not match revocation_time: %s, "+"rfc3339 time: %s", revTime, rfc3339RevocationTime) require.Equal(t, issuerId, resp.Data["issuer_id"], "issuer_id on leaf cert did not match") // Wait for cert to expire and the safety buffer to elapse. time.Sleep(time.Until(leafCert.NotAfter) + 3*time.Second) // We run this twice to make absolutely sure we didn't read a previous run of tidy _, lastRun := waitForTidyToFinish(t, client, "pki") waitForTidyToFinishWithLastRun(t, client, "pki", lastRun) // Cert should no longer exist. resp, err = client.Logical().Read("pki/cert/" + leafSerial) require.Nil(t, err) require.Nil(t, resp) } // TestAutoTidyPersistsAcrossRestarts validates that on initial // startup of a mount we persisted the current auto tidy time so that // our counter that auto-tidy is based on isn't reset everytime Vault restarts func TestAutoTidyPersistsAcrossRestarts(t *testing.T) { t.Parallel() newPeriod := 1 * time.Second // This test requires the periodicFunc to trigger, which requires we stand // up a full test cluster. coreConfig := &vault.CoreConfig{ LogicalBackends: map[string]logical.Factory{ "pki": Factory, }, RollbackPeriod: newPeriod, } opts := &vault.TestClusterOptions{ HandlerFunc: vaulthttp.Handler, NumCores: 1, } cluster := vault.NewTestCluster(t, coreConfig, opts) cluster.Start() defer cluster.Cleanup() client := cluster.Cores[0].Client // Mount PKI err := client.Sys().Mount("pki", &api.MountInput{ Type: "pki", }) require.NoError(t, err, "failed mounting pki") // Run a tidy that should set us up _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ "tidy_cert_store": "true", }) require.NoError(t, err, "failed running tidy") waitForTidyToFinish(t, client, "pki") resp, err := client.Logical().Read("pki/tidy-status") require.NoError(t, err, "failed reading tidy status") require.NotNil(t, resp, "response from tidy-status was nil") lastAutoTidy, exists := resp.Data["last_auto_tidy_finished"] require.True(t, exists, "did not find last_auto_tidy_finished") cluster.StopCore(t, 0) cluster.StartCore(t, 0, opts) cluster.UnsealCore(t, cluster.Cores[0]) vault.TestWaitActive(t, cluster.Cores[0].Core) client = cluster.Cores[0].Client resp, err = client.Logical().Read("pki/tidy-status") require.NoError(t, err, "failed reading tidy status") require.NotNil(t, resp, "response from tidy-status was nil") postRestartLastAutoTidy, exists := resp.Data["last_auto_tidy_finished"] require.True(t, exists, "did not find last_auto_tidy_finished") require.Equal(t, lastAutoTidy, postRestartLastAutoTidy, "values for last_auto_tidy_finished did not match on restart") } func TestTidyCancellation(t *testing.T) { t.Parallel() numLeaves := 100 b, s := CreateBackendWithStorage(t) // Create a root, a role, and a bunch of leaves. _, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ "common_name": "root example.com", "issuer_name": "root", "ttl": "20m", "key_type": "ec", }) require.NoError(t, err) _, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{ "allow_any_name": true, "enforce_hostnames": false, "key_type": "ec", }) require.NoError(t, err) for i := 0; i < numLeaves; i++ { _, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{ "common_name": "testing", "ttl": "1s", }) require.NoError(t, err) } // Kick off a tidy operation (which runs in the background), but with // a slow-ish pause between certificates. resp, err := CBWrite(b, s, "tidy", map[string]interface{}{ "tidy_cert_store": true, "safety_buffer": "1s", "pause_duration": "1s", }) schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("tidy"), logical.UpdateOperation), resp, true) // If we wait six seconds, the operation should still be running. That's // how we check that pause_duration works. time.Sleep(3 * time.Second) resp, err = CBRead(b, s, "tidy-status") require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, resp.Data) require.Equal(t, resp.Data["state"], "Running") // If we now cancel the operation, the response should say Cancelling. cancelResp, err := CBWrite(b, s, "tidy-cancel", map[string]interface{}{}) schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("tidy-cancel"), logical.UpdateOperation), resp, true) require.NoError(t, err) require.NotNil(t, cancelResp) require.NotNil(t, cancelResp.Data) state := cancelResp.Data["state"].(string) howMany := cancelResp.Data["cert_store_deleted_count"].(uint) if state == "Cancelled" { // Rest of the test can't run; log and exit. t.Log("Went to cancel the operation but response was already cancelled") return } require.Equal(t, state, "Cancelling") // Wait a little longer, and ensure we only processed at most 2 more certs // after the cancellation respon. time.Sleep(3 * time.Second) statusResp, err := CBRead(b, s, "tidy-status") schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("tidy-status"), logical.ReadOperation), resp, true) require.NoError(t, err) require.NotNil(t, statusResp) require.NotNil(t, statusResp.Data) require.Equal(t, statusResp.Data["state"], "Cancelled") nowMany := statusResp.Data["cert_store_deleted_count"].(uint) if howMany+3 <= nowMany { t.Fatalf("expected to only process at most 3 more certificates, but processed (%v >>> %v) certs", nowMany, howMany) } } func TestTidyIssuers(t *testing.T) { t.Parallel() b, s := CreateBackendWithStorage(t) // Create a root that expires quickly and one valid for longer. _, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{ "common_name": "root1 example.com", "issuer_name": "root-expired", "ttl": "1s", "key_type": "ec", }) require.NoError(t, err) _, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{ "common_name": "root2 example.com", "issuer_name": "root-valid", "ttl": "60m", "key_type": "rsa", }) require.NoError(t, err) // Sleep long enough to expire the root. time.Sleep(2 * time.Second) // First tidy run shouldn't remove anything; too long of safety buffer. _, err = CBWrite(b, s, "tidy", map[string]interface{}{ "tidy_expired_issuers": true, "issuer_safety_buffer": "60m", }) require.NoError(t, err) // Wait for tidy to finish. time.Sleep(2 * time.Second) // Expired issuer should exist. resp, err := CBRead(b, s, "issuer/root-expired") requireSuccessNonNilResponse(t, resp, err, "expired should still be present") resp, err = CBRead(b, s, "issuer/root-valid") requireSuccessNonNilResponse(t, resp, err, "valid should still be present") // Second tidy run with shorter safety buffer shouldn't remove the // expired one, as it should be the default issuer. _, err = CBWrite(b, s, "tidy", map[string]interface{}{ "tidy_expired_issuers": true, "issuer_safety_buffer": "1s", }) require.NoError(t, err) // Wait for tidy to finish. time.Sleep(2 * time.Second) // Expired issuer should still exist. resp, err = CBRead(b, s, "issuer/root-expired") requireSuccessNonNilResponse(t, resp, err, "expired should still be present") resp, err = CBRead(b, s, "issuer/root-valid") requireSuccessNonNilResponse(t, resp, err, "valid should still be present") // Update the default issuer. _, err = CBWrite(b, s, "config/issuers", map[string]interface{}{ "default": "root-valid", }) require.NoError(t, err) // Third tidy run should remove the expired one. _, err = CBWrite(b, s, "tidy", map[string]interface{}{ "tidy_expired_issuers": true, "issuer_safety_buffer": "1s", }) require.NoError(t, err) // Wait for tidy to finish. time.Sleep(2 * time.Second) // Valid issuer should exist still; other should be removed. resp, err = CBRead(b, s, "issuer/root-expired") require.Error(t, err) require.Nil(t, resp) resp, err = CBRead(b, s, "issuer/root-valid") requireSuccessNonNilResponse(t, resp, err, "valid should still be present") // Finally, one more tidy should cause no changes. _, err = CBWrite(b, s, "tidy", map[string]interface{}{ "tidy_expired_issuers": true, "issuer_safety_buffer": "1s", }) require.NoError(t, err) // Wait for tidy to finish. time.Sleep(2 * time.Second) // Valid issuer should exist still; other should be removed. resp, err = CBRead(b, s, "issuer/root-expired") require.Error(t, err) require.Nil(t, resp) resp, err = CBRead(b, s, "issuer/root-valid") requireSuccessNonNilResponse(t, resp, err, "valid should still be present") // Ensure we have safety buffer and expired issuers set correctly. statusResp, err := CBRead(b, s, "tidy-status") require.NoError(t, err) require.NotNil(t, statusResp) require.NotNil(t, statusResp.Data) require.Equal(t, statusResp.Data["issuer_safety_buffer"], 1) require.Equal(t, statusResp.Data["tidy_expired_issuers"], true) } func TestTidyIssuerConfig(t *testing.T) { t.Parallel() b, s := CreateBackendWithStorage(t) // Ensure the default auto-tidy config matches expectations resp, err := CBRead(b, s, "config/auto-tidy") schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/auto-tidy"), logical.ReadOperation), resp, true) requireSuccessNonNilResponse(t, resp, err) jsonBlob, err := json.Marshal(&defaultTidyConfig) require.NoError(t, err) var defaultConfigMap map[string]interface{} err = json.Unmarshal(jsonBlob, &defaultConfigMap) require.NoError(t, err) // Coerce defaults to API response types. defaultConfigMap["interval_duration"] = int(time.Duration(defaultConfigMap["interval_duration"].(float64)) / time.Second) defaultConfigMap["issuer_safety_buffer"] = int(time.Duration(defaultConfigMap["issuer_safety_buffer"].(float64)) / time.Second) defaultConfigMap["safety_buffer"] = int(time.Duration(defaultConfigMap["safety_buffer"].(float64)) / time.Second) defaultConfigMap["pause_duration"] = time.Duration(defaultConfigMap["pause_duration"].(float64)).String() defaultConfigMap["revocation_queue_safety_buffer"] = int(time.Duration(defaultConfigMap["revocation_queue_safety_buffer"].(float64)) / time.Second) defaultConfigMap["acme_account_safety_buffer"] = int(time.Duration(defaultConfigMap["acme_account_safety_buffer"].(float64)) / time.Second) defaultConfigMap["min_startup_backoff_duration"] = int(time.Duration(defaultConfigMap["min_startup_backoff_duration"].(float64)) / time.Second) defaultConfigMap["max_startup_backoff_duration"] = int(time.Duration(defaultConfigMap["max_startup_backoff_duration"].(float64)) / time.Second) require.Equal(t, defaultConfigMap, resp.Data) // Ensure setting issuer-tidy related fields stick. resp, err = CBWrite(b, s, "config/auto-tidy", map[string]interface{}{ "tidy_expired_issuers": true, "issuer_safety_buffer": "5s", }) schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("config/auto-tidy"), logical.UpdateOperation), resp, true) requireSuccessNonNilResponse(t, resp, err) require.Equal(t, true, resp.Data["tidy_expired_issuers"]) require.Equal(t, 5, resp.Data["issuer_safety_buffer"]) } // TestCertStorageMetrics ensures that when enabled, metrics are able to count the number of certificates in storage and // number of revoked certificates in storage. Moreover, this test ensures that the gauge is emitted periodically, so // that the metric does not disappear or go stale. func TestCertStorageMetrics(t *testing.T) { // This tests uses the same setup as TestAutoTidy newPeriod := 1 * time.Second // We set up a metrics accumulator inmemSink := metrics.NewInmemSink( 2*newPeriod, // A short time period is ideal here to test metrics are emitted every periodic func 10*newPeriod) // Do not keep a huge amount of metrics in the sink forever, clear them out to save memory usage. metricsConf := metrics.DefaultConfig("") metricsConf.EnableHostname = false metricsConf.EnableHostnameLabel = false metricsConf.EnableServiceLabel = false metricsConf.EnableTypePrefix = false _, err := metrics.NewGlobal(metricsConf, inmemSink) if err != nil { t.Fatal(err) } // This test requires the periodicFunc to trigger, which requires we stand // up a full test cluster. coreConfig := &vault.CoreConfig{ LogicalBackends: map[string]logical.Factory{ "pki": Factory, }, // See notes below about usage of /sys/raw for reading cluster // storage without barrier encryption. EnableRaw: true, RollbackPeriod: newPeriod, } cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{ HandlerFunc: vaulthttp.Handler, NumCores: 1, }) cluster.Start() defer cluster.Cleanup() client := cluster.Cores[0].Client // Mount PKI err = client.Sys().Mount("pki", &api.MountInput{ Type: "pki", Config: api.MountConfigInput{ DefaultLeaseTTL: "10m", MaxLeaseTTL: "60m", }, }) require.NoError(t, err) // Generate root. resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{ "ttl": "40h", "common_name": "Root X1", "key_type": "ec", }) require.NoError(t, err) require.NotNil(t, resp) require.NotEmpty(t, resp.Data) require.NotEmpty(t, resp.Data["issuer_id"]) // Set up a testing role. _, err = client.Logical().Write("pki/roles/local-testing", map[string]interface{}{ "allow_any_name": true, "enforce_hostnames": false, "key_type": "ec", }) require.NoError(t, err) // Run tidy so that tidy-status is not empty _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ "tidy_revoked_certs": true, }) require.NoError(t, err) // Since certificate counts are off by default, we shouldn't see counts in the tidy status tidyStatus, err := client.Logical().Read("pki/tidy-status") require.NoError(t, err, "failed reading from tidy-status") // backendUUID should exist, we need this for metrics backendUUID := tidyStatus.Data["internal_backend_uuid"].(string) // "current_cert_store_count", "current_revoked_cert_count" countData, ok := tidyStatus.Data["current_cert_store_count"] if ok && countData != nil { t.Fatalf("Certificate counting should be off by default, but current cert store count %v appeared in tidy status in unconfigured mount", countData) } revokedCountData, ok := tidyStatus.Data["current_revoked_cert_count"] if ok && revokedCountData != nil { t.Fatalf("Certificate counting should be off by default, but revoked cert count %v appeared in tidy status in unconfigured mount", revokedCountData) } // Since certificate counts are off by default, those metrics should not exist yet stableMetric := inmemSink.Data() mostRecentInterval := stableMetric[len(stableMetric)-1] _, ok = mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_revoked_certificates_stored"] if ok { t.Fatalf("Certificate counting should be off by default, but revoked cert count was emitted as a metric in an unconfigured mount") } _, ok = mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_certificates_stored"] if ok { t.Fatalf("Certificate counting should be off by default, but total certificate count was emitted as a metric in an unconfigured mount") } // Write the auto-tidy config. _, err = client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{ "enabled": true, "interval_duration": "1s", "tidy_cert_store": true, "tidy_revoked_certs": true, "safety_buffer": "1s", "maintain_stored_certificate_counts": true, "publish_stored_certificate_count_metrics": false, "min_startup_backoff_duration": "1s", "max_startup_backoff_duration": "1s", }) require.NoError(t, err) // Reload the Mount - Otherwise Stored Certificate Counts Will Not Be Populated // Sealing cores as plugin reload triggers the race detector - VAULT-13635 testhelpers.EnsureCoresSealed(t, cluster) testhelpers.EnsureCoresUnsealed(t, cluster) // Wait until a tidy run has completed. tidyStatus, _ = waitForTidyToFinish(t, client, "pki") // Since publish_stored_certificate_count_metrics is still false, these metrics should still not exist yet stableMetric = inmemSink.Data() mostRecentInterval = stableMetric[len(stableMetric)-1] _, ok = mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_revoked_certificates_stored"] if ok { t.Fatalf("Certificate counting should be off by default, but revoked cert count was emitted as a metric in an unconfigured mount") } _, ok = mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_certificates_stored"] if ok { t.Fatalf("Certificate counting should be off by default, but total certificate count was emitted as a metric in an unconfigured mount") } // But since certificate counting is on, the metrics should exist on tidyStatus endpoint: // backendUUID should exist, we need this for metrics backendUUID = tidyStatus.Data["internal_backend_uuid"].(string) // "current_cert_store_count", "current_revoked_cert_count" certStoreCount, ok := tidyStatus.Data["current_cert_store_count"] if !ok { t.Fatalf("Certificate counting has been turned on, but current cert store count does not appear in tidy status") } if certStoreCount != json.Number("1") { t.Fatalf("Only created one certificate, but a got a certificate count of %v", certStoreCount) } revokedCertCount, ok := tidyStatus.Data["current_revoked_cert_count"] if !ok { t.Fatalf("Certificate counting has been turned on, but revoked cert store count does not appear in tidy status") } if revokedCertCount != json.Number("0") { t.Fatalf("Have not yet revoked a certificate, but got a revoked cert store count of %v", revokedCertCount) } // Write the auto-tidy config, again, this time turning on metrics _, err = client.Logical().Write("pki/config/auto-tidy", map[string]interface{}{ "enabled": true, "interval_duration": "1s", "tidy_cert_store": true, "tidy_revoked_certs": true, "safety_buffer": "1s", "maintain_stored_certificate_counts": true, "publish_stored_certificate_count_metrics": true, }) require.NoError(t, err, "failed updating auto-tidy configuration") // Issue a cert and revoke it. resp, err = client.Logical().Write("pki/issue/local-testing", map[string]interface{}{ "common_name": "example.com", "ttl": "10s", }) require.NoError(t, err, "failed to issue leaf certificate") require.NotNil(t, resp, "nil response without error on issuing leaf certificate") require.NotNil(t, resp.Data, "empty Data without error on issuing leaf certificate") require.NotEmpty(t, resp.Data["serial_number"]) require.NotEmpty(t, resp.Data["certificate"]) leafSerial := resp.Data["serial_number"].(string) leafCert := parseCert(t, resp.Data["certificate"].(string)) // Read cert before revoking resp, err = client.Logical().Read("pki/cert/" + leafSerial) require.NoError(t, err) require.NotNil(t, resp) require.NotNil(t, resp.Data) require.NotEmpty(t, resp.Data["certificate"]) revocationTime, err := (resp.Data["revocation_time"].(json.Number)).Int64() require.Equal(t, int64(0), revocationTime, "revocation time was not zero") require.Empty(t, resp.Data["revocation_time_rfc3339"], "revocation_time_rfc3339 was not empty") require.Empty(t, resp.Data["issuer_id"], "issuer_id was not empty") revokeResp, err := client.Logical().Write("pki/revoke", map[string]interface{}{ "serial_number": leafSerial, }) require.NoError(t, err, "failed revoking serial number: %s", leafSerial) for _, warning := range revokeResp.Warnings { if strings.Contains(warning, "already expired; refusing to add to CRL") { t.Skipf("Skipping test as we missed the revocation window of our leaf cert") } } // We read the auto-tidy endpoint again, to ensure any metrics logic has completed (lock on config) _, err = client.Logical().Read("/pki/config/auto-tidy") require.NoError(t, err, "failed to read auto-tidy configuration") // Check Metrics After Cert Has Be Created and Revoked tidyStatus, err = client.Logical().Read("pki/tidy-status") require.NoError(t, err, "failed to read tidy-status") backendUUID = tidyStatus.Data["internal_backend_uuid"].(string) certStoreCount, ok = tidyStatus.Data["current_cert_store_count"] if !ok { t.Fatalf("Certificate counting has been turned on, but current cert store count does not appear in tidy status") } if certStoreCount != json.Number("2") { t.Fatalf("Created root and leaf certificate, but a got a certificate count of %v", certStoreCount) } revokedCertCount, ok = tidyStatus.Data["current_revoked_cert_count"] if !ok { t.Fatalf("Certificate counting has been turned on, but revoked cert store count does not appear in tidy status") } if revokedCertCount != json.Number("1") { t.Fatalf("Revoked one certificate, but got a revoked cert store count of %v\n:%v", revokedCertCount, tidyStatus) } // This should now be initialized certCountError, ok := tidyStatus.Data["certificate_counting_error"] if ok && certCountError.(string) != "" { t.Fatalf("Expected certificate count error to disappear after initialization, but got error %v", certCountError) } testhelpers.RetryUntil(t, newPeriod*5, func() error { stableMetric = inmemSink.Data() mostRecentInterval = stableMetric[len(stableMetric)-1] revokedCertCountGaugeValue, ok := mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_revoked_certificates_stored"] if !ok { return errors.New("turned on metrics, but revoked cert count was not emitted") } if revokedCertCountGaugeValue.Value != 1 { return fmt.Errorf("revoked one certificate, but metrics emitted a revoked cert store count of %v", revokedCertCountGaugeValue) } certStoreCountGaugeValue, ok := mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_certificates_stored"] if !ok { return errors.New("turned on metrics, but total certificate count was not emitted") } if certStoreCountGaugeValue.Value != 2 { return fmt.Errorf("stored two certificiates, but total certificate count emitted was %v", certStoreCountGaugeValue.Value) } return nil }) // Wait for cert to expire and the safety buffer to elapse. sleepFor := time.Until(leafCert.NotAfter) + 3*time.Second t.Logf("%v: Sleeping for %v, leaf certificate expires: %v", time.Now().Format(time.RFC3339), sleepFor, leafCert.NotAfter) time.Sleep(sleepFor) _, lastRun := waitForTidyToFinish(t, client, "pki") tidyStatus, _ = waitForTidyToFinishWithLastRun(t, client, "pki", lastRun) // After Tidy, Cert Store Count Should Still Be Available, and Be Updated: // Check Metrics After Cert Has Be Created and Revoked backendUUID = tidyStatus.Data["internal_backend_uuid"].(string) // "current_cert_store_count", "current_revoked_cert_count" certStoreCount, ok = tidyStatus.Data["current_cert_store_count"] if !ok { t.Fatalf("Certificate counting has been turned on, but current cert store count does not appear in tidy status") } if certStoreCount != json.Number("1") { t.Fatalf("Created root and leaf certificate, deleted leaf, but a got a certificate count of %v", certStoreCount) } revokedCertCount, ok = tidyStatus.Data["current_revoked_cert_count"] if !ok { t.Fatalf("Certificate counting has been turned on, but revoked cert store count does not appear in tidy status") } if revokedCertCount != json.Number("0") { t.Fatalf("Revoked certificate has been tidied, but got a revoked cert store count of %v", revokedCertCount) } testhelpers.RetryUntil(t, newPeriod*5, func() error { stableMetric = inmemSink.Data() mostRecentInterval = stableMetric[len(stableMetric)-1] revokedCertCountGaugeValue, ok := mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_revoked_certificates_stored"] if !ok { return errors.New("turned on metrics, but revoked cert count was not emitted") } if revokedCertCountGaugeValue.Value != 0 { return fmt.Errorf("revoked certificate has been tidied, but metrics emitted a revoked cert store count of %v", revokedCertCountGaugeValue) } certStoreCountGaugeValue, ok := mostRecentInterval.Gauges["secrets.pki."+backendUUID+".total_certificates_stored"] if !ok { return errors.New("turned on metrics, but total certificate count was not emitted") } if certStoreCountGaugeValue.Value != 1 { return fmt.Errorf("only one of two certificates left after tidy, but total certificate count emitted was %v", certStoreCountGaugeValue.Value) } return nil }) } // This test uses the default safety buffer with backdating. func TestTidyAcmeWithBackdate(t *testing.T) { t.Parallel() cluster, client, _ := setupAcmeBackend(t) defer cluster.Cleanup() testCtx := context.Background() // Grab the mount UUID for sys/raw invocations. pkiMount := findStorageMountUuid(t, client, "pki") // Register an Account, do nothing with it baseAcmeURL := "/v1/pki/acme/" accountKey, err := cryptoutil.GenerateRSAKey(rand.Reader, 2048) require.NoError(t, err, "failed creating rsa key") acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) // Create new account with order/cert t.Logf("Testing register on %s", baseAcmeURL) acct, err := acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true }) require.NoError(t, err, "failed registering account") t.Logf("got account URI: %v", acct.URI) identifiers := []string{"*.localdomain"} order, err := acmeClient.AuthorizeOrder(testCtx, []acme.AuthzID{ {Type: "dns", Value: identifiers[0]}, }) require.NoError(t, err, "failed creating order") // HACK: Update authorization/challenge to completed as we can't really do it properly in this workflow test. markAuthorizationSuccess(t, client, acmeClient, acct, order) goodCr := &x509.CertificateRequest{DNSNames: []string{identifiers[0]}} csrKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) require.NoError(t, err, "failed generated key for CSR") csr, err := x509.CreateCertificateRequest(rand.Reader, goodCr, csrKey) require.NoError(t, err, "failed generating csr") certs, _, err := acmeClient.CreateOrderCert(testCtx, order.FinalizeURL, csr, true) require.NoError(t, err, "order finalization failed") require.GreaterOrEqual(t, len(certs), 1, "expected at least one cert in bundle") acmeCert, err := x509.ParseCertificate(certs[0]) require.NoError(t, err, "failed parsing acme cert") // -> Ensure we see it in storage. Since we don't have direct storage // access, use sys/raw interface. acmeThumbprintsPath := path.Join("sys/raw/logical", pkiMount, acmeThumbprintPrefix) listResp, err := client.Logical().ListWithContext(testCtx, acmeThumbprintsPath) require.NoError(t, err, "failed listing ACME thumbprints") require.NotEmpty(t, listResp.Data["keys"], "expected non-empty list response") // Run Tidy _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ "tidy_acme": true, }) require.NoError(t, err) // Wait for tidy to finish. waitForTidyToFinish(t, client, "pki") // Check that the Account is Still There, Still Valid. account, err := acmeClient.GetReg(context.Background(), "" /* legacy unused param*/) require.NoError(t, err, "received account looking up acme account") require.Equal(t, acme.StatusValid, account.Status) // Find the associated thumbprint listResp, err = client.Logical().ListWithContext(testCtx, acmeThumbprintsPath) require.NoError(t, err) require.NotNil(t, listResp) thumbprintEntries := listResp.Data["keys"].([]interface{}) require.Equal(t, len(thumbprintEntries), 1) thumbprint := thumbprintEntries[0].(string) // Let "Time Pass"; this is a HACK, this function sys-writes to overwrite the date on objects in storage duration := time.Until(acmeCert.NotAfter) + 31*24*time.Hour accountId := acmeClient.KID[strings.LastIndex(string(acmeClient.KID), "/")+1:] orderId := order.URI[strings.LastIndex(order.URI, "/")+1:] backDateAcmeOrderSys(t, testCtx, client, string(accountId), orderId, duration, pkiMount) // Run Tidy -> clean up order _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ "tidy_acme": true, }) require.NoError(t, err) // Wait for tidy to finish. tidyResp, _ := waitForTidyToFinish(t, client, "pki") require.Equal(t, tidyResp.Data["acme_orders_deleted_count"], json.Number("1"), "expected to revoke a single ACME order: %v", tidyResp) require.Equal(t, tidyResp.Data["acme_account_revoked_count"], json.Number("0"), "no ACME account should have been revoked: %v", tidyResp) require.Equal(t, tidyResp.Data["acme_account_deleted_count"], json.Number("0"), "no ACME account should have been revoked: %v", tidyResp) // Make sure our order is indeed deleted. _, err = acmeClient.GetOrder(context.Background(), order.URI) require.ErrorContains(t, err, "order does not exist") // Check that the Account is Still There, Still Valid. account, err = acmeClient.GetReg(context.Background(), "" /* legacy unused param*/) require.NoError(t, err, "received account looking up acme account") require.Equal(t, acme.StatusValid, account.Status) // Now back date the account to make sure we revoke it backDateAcmeAccountSys(t, testCtx, client, thumbprint, duration, pkiMount) // Run Tidy -> mark account revoked _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ "tidy_acme": true, }) require.NoError(t, err) // Wait for tidy to finish. tidyResp, _ = waitForTidyToFinish(t, client, "pki") require.Equal(t, tidyResp.Data["acme_orders_deleted_count"], json.Number("0"), "no ACME orders should have been deleted: %v", tidyResp) require.Equal(t, tidyResp.Data["acme_account_revoked_count"], json.Number("1"), "expected to revoke a single ACME account: %v", tidyResp) require.Equal(t, tidyResp.Data["acme_account_deleted_count"], json.Number("0"), "no ACME account should have been revoked: %v", tidyResp) // Lookup our account to make sure we get the appropriate revoked status account, err = acmeClient.GetReg(context.Background(), "" /* legacy unused param*/) require.NoError(t, err, "received account looking up acme account") require.Equal(t, acme.StatusRevoked, account.Status) // Let "Time Pass"; this is a HACK, this function sys-writes to overwrite the date on objects in storage backDateAcmeAccountSys(t, testCtx, client, thumbprint, duration, pkiMount) // Run Tidy -> remove account _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ "tidy_acme": true, }) require.NoError(t, err) // Wait for tidy to finish. waitForTidyToFinish(t, client, "pki") // Check Account No Longer Appears listResp, err = client.Logical().ListWithContext(testCtx, acmeThumbprintsPath) require.NoError(t, err) if listResp != nil { thumbprintEntries = listResp.Data["keys"].([]interface{}) require.Equal(t, 0, len(thumbprintEntries)) } // Nor Under Account _, acctKID := path.Split(acct.URI) acctPath := path.Join("sys/raw/logical", pkiMount, acmeAccountPrefix, acctKID) t.Logf("account path: %v", acctPath) getResp, err := client.Logical().ReadWithContext(testCtx, acctPath) require.NoError(t, err) require.Nil(t, getResp) } // This test uses a smaller safety buffer. func TestTidyAcmeWithSafetyBuffer(t *testing.T) { t.Parallel() // This would still be way easier if I could do both sides cluster, client, _ := setupAcmeBackend(t) defer cluster.Cleanup() testCtx := context.Background() // Grab the mount UUID for sys/raw invocations. pkiMount := findStorageMountUuid(t, client, "pki") // Register an Account, do nothing with it baseAcmeURL := "/v1/pki/acme/" accountKey, err := cryptoutil.GenerateRSAKey(rand.Reader, 2048) require.NoError(t, err, "failed creating rsa key") acmeClient := getAcmeClientForCluster(t, cluster, baseAcmeURL, accountKey) // Create new account t.Logf("Testing register on %s", baseAcmeURL) acct, err := acmeClient.Register(testCtx, &acme.Account{}, func(tosURL string) bool { return true }) require.NoError(t, err, "failed registering account") t.Logf("got account URI: %v", acct.URI) // -> Ensure we see it in storage. Since we don't have direct storage // access, use sys/raw interface. acmeThumbprintsPath := path.Join("sys/raw/logical", pkiMount, acmeThumbprintPrefix) listResp, err := client.Logical().ListWithContext(testCtx, acmeThumbprintsPath) require.NoError(t, err, "failed listing ACME thumbprints") require.NotEmpty(t, listResp.Data["keys"], "expected non-empty list response") thumbprintEntries := listResp.Data["keys"].([]interface{}) require.Equal(t, len(thumbprintEntries), 1) // Wait for the account to expire. time.Sleep(2 * time.Second) // Run Tidy -> mark account revoked _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ "tidy_acme": true, "acme_account_safety_buffer": "1s", }) require.NoError(t, err) // Wait for tidy to finish. statusResp, _ := waitForTidyToFinish(t, client, "pki") require.Equal(t, statusResp.Data["acme_account_revoked_count"], json.Number("1"), "expected to revoke a single ACME account") // Wait for the account to expire. time.Sleep(2 * time.Second) // Run Tidy -> remove account _, err = client.Logical().Write("pki/tidy", map[string]interface{}{ "tidy_acme": true, "acme_account_safety_buffer": "1s", }) require.NoError(t, err) // Wait for tidy to finish. waitForTidyToFinish(t, client, "pki") // Check Account No Longer Appears listResp, err = client.Logical().ListWithContext(testCtx, acmeThumbprintsPath) require.NoError(t, err) if listResp != nil { thumbprintEntries = listResp.Data["keys"].([]interface{}) require.Equal(t, 0, len(thumbprintEntries)) } // Nor Under Account _, acctKID := path.Split(acct.URI) acctPath := path.Join("sys/raw/logical", pkiMount, acmeAccountPrefix, acctKID) t.Logf("account path: %v", acctPath) getResp, err := client.Logical().ReadWithContext(testCtx, acctPath) require.NoError(t, err) require.Nil(t, getResp) } // The sys tests refer to all of the tests using sys/raw/logical which work off of a client func backDateAcmeAccountSys(t *testing.T, testContext context.Context, client *api.Client, thumbprintString string, backdateAmount time.Duration, mount string) { rawThumbprintPath := path.Join("sys/raw/logical/", mount, acmeThumbprintPrefix+thumbprintString) thumbprintResp, err := client.Logical().ReadWithContext(testContext, rawThumbprintPath) if err != nil { t.Fatalf("unable to fetch thumbprint response at %v: %v", rawThumbprintPath, err) } var thumbprint acmeThumbprint err = jsonutil.DecodeJSON([]byte(thumbprintResp.Data["value"].(string)), &thumbprint) if err != nil { t.Fatalf("unable to decode thumbprint response %v to find account entry: %v", thumbprintResp.Data, err) } accountPath := path.Join("sys/raw/logical", mount, acmeAccountPrefix+thumbprint.Kid) accountResp, err := client.Logical().ReadWithContext(testContext, accountPath) if err != nil { t.Fatalf("unable to fetch account entry %v: %v", thumbprint.Kid, err) } var account acmeAccount err = jsonutil.DecodeJSON([]byte(accountResp.Data["value"].(string)), &account) if err != nil { t.Fatalf("unable to decode acme account %v: %v", accountResp, err) } t.Logf("got account before update: %v", account) account.AccountCreatedDate = backDate(account.AccountCreatedDate, backdateAmount) account.MaxCertExpiry = backDate(account.MaxCertExpiry, backdateAmount) account.AccountRevokedDate = backDate(account.AccountRevokedDate, backdateAmount) t.Logf("got account after update: %v", account) encodeJSON, err := jsonutil.EncodeJSON(account) _, err = client.Logical().WriteWithContext(context.Background(), accountPath, map[string]interface{}{ "value": base64.StdEncoding.EncodeToString(encodeJSON), "encoding": "base64", }) if err != nil { t.Fatalf("error saving backdated account entry at %v: %v", accountPath, err) } ordersPath := path.Join("sys/raw/logical", mount, acmeAccountPrefix, thumbprint.Kid, "/orders/") ordersRaw, err := client.Logical().ListWithContext(context.Background(), ordersPath) require.NoError(t, err, "failed listing orders") if ordersRaw == nil { t.Logf("skipping backdating orders as there are none") return } require.NotNil(t, ordersRaw, "got no response data") require.NotNil(t, ordersRaw.Data, "got no response data") orders := ordersRaw.Data for _, orderId := range orders["keys"].([]interface{}) { backDateAcmeOrderSys(t, testContext, client, thumbprint.Kid, orderId.(string), backdateAmount, mount) } // No need to change certificates entries here - no time is stored on AcmeCertEntry } func backDateAcmeOrderSys(t *testing.T, testContext context.Context, client *api.Client, accountKid string, orderId string, backdateAmount time.Duration, mount string) { rawOrderPath := path.Join("sys/raw/logical/", mount, acmeAccountPrefix, accountKid, "orders", orderId) orderResp, err := client.Logical().ReadWithContext(testContext, rawOrderPath) if err != nil { t.Fatalf("unable to fetch order entry %v on account %v at %v", orderId, accountKid, rawOrderPath) } var order *acmeOrder err = jsonutil.DecodeJSON([]byte(orderResp.Data["value"].(string)), &order) if err != nil { t.Fatalf("error decoding order entry %v on account %v, %v produced: %v", orderId, accountKid, orderResp, err) } order.Expires = backDate(order.Expires, backdateAmount) order.CertificateExpiry = backDate(order.CertificateExpiry, backdateAmount) encodeJSON, err := jsonutil.EncodeJSON(order) _, err = client.Logical().WriteWithContext(context.Background(), rawOrderPath, map[string]interface{}{ "value": base64.StdEncoding.EncodeToString(encodeJSON), "encoding": "base64", }) if err != nil { t.Fatalf("error saving backdated order entry %v on account %v : %v", orderId, accountKid, err) } for _, authId := range order.AuthorizationIds { backDateAcmeAuthorizationSys(t, testContext, client, accountKid, authId, backdateAmount, mount) } } func backDateAcmeAuthorizationSys(t *testing.T, testContext context.Context, client *api.Client, accountKid string, authId string, backdateAmount time.Duration, mount string) { rawAuthPath := path.Join("sys/raw/logical/", mount, acmeAccountPrefix, accountKid, "/authorizations/", authId) authResp, err := client.Logical().ReadWithContext(testContext, rawAuthPath) if err != nil { t.Fatalf("unable to fetch authorization %v : %v", rawAuthPath, err) } var auth *ACMEAuthorization err = jsonutil.DecodeJSON([]byte(authResp.Data["value"].(string)), &auth) if err != nil { t.Fatalf("error decoding auth %v, auth entry %v produced %v", rawAuthPath, authResp, err) } expiry, err := auth.GetExpires() if err != nil { t.Fatalf("could not get expiry on %v: %v", rawAuthPath, err) } newExpiry := backDate(expiry, backdateAmount) auth.Expires = time.Time.Format(newExpiry, time.RFC3339) encodeJSON, err := jsonutil.EncodeJSON(auth) _, err = client.Logical().WriteWithContext(context.Background(), rawAuthPath, map[string]interface{}{ "value": base64.StdEncoding.EncodeToString(encodeJSON), "encoding": "base64", }) if err != nil { t.Fatalf("error updating authorization date on %v: %v", rawAuthPath, err) } } func backDate(original time.Time, change time.Duration) time.Time { if original.IsZero() { return original } zeroTime := time.Time{} if original.Before(zeroTime.Add(change)) { return zeroTime } return original.Add(-change) } func waitForTidyToFinish(t *testing.T, client *api.Client, mount string) (*api.Secret, time.Time) { return waitForTidyToFinishWithLastRun(t, client, mount, time.Time{}) } func waitForTidyToFinishWithLastRun(t *testing.T, client *api.Client, mount string, previousFinishTime time.Time) (*api.Secret, time.Time) { t.Helper() var statusResp *api.Secret var currentFinishTime time.Time testhelpers.RetryUntil(t, 30*time.Second, func() error { var err error tidyStatusPath := mount + "/tidy-status" statusResp, err = client.Logical().Read(tidyStatusPath) if err != nil { return fmt.Errorf("failed reading path: %s: %w", tidyStatusPath, err) } if statusResp == nil { return fmt.Errorf("got nil, nil response from: %s", tidyStatusPath) } if state, ok := statusResp.Data["state"]; !ok || state != "Finished" { return fmt.Errorf("tidy has not finished got state: %v", state) } if currentFinishTimeRaw, ok := statusResp.Data["time_finished"]; !ok { return fmt.Errorf("tidy status did not contain a time_finished field") } else { if currentFinishTimeStr, ok := currentFinishTimeRaw.(string); !ok { return fmt.Errorf("tidy status time_finished field was not a string was %T", currentFinishTimeRaw) } else { currentFinishTime, err = time.Parse(time.RFC3339, currentFinishTimeStr) if !currentFinishTime.After(previousFinishTime) { return fmt.Errorf("tidy status time_finished %v was not after previous time %v", currentFinishTime, previousFinishTime) } } } if errorOccurred, ok := statusResp.Data["error"]; !ok || !(errorOccurred == nil || errorOccurred == "") { return fmt.Errorf("tidy status returned an error: %s", errorOccurred) } return nil }) t.Logf("got tidy status: %v", statusResp.Data) return statusResp, currentFinishTime }