// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package pki import ( "bytes" "context" "crypto" "crypto/x509" "errors" "fmt" "strings" "sync" "time" "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/builtin/logical/pki/issuing" "github.com/hashicorp/vault/builtin/logical/pki/managed_key" "github.com/hashicorp/vault/builtin/logical/pki/pki_backend" "github.com/hashicorp/vault/builtin/logical/pki/revocation" "github.com/hashicorp/vault/helper/constants" "github.com/hashicorp/vault/sdk/helper/certutil" "github.com/hashicorp/vault/sdk/helper/errutil" "github.com/hashicorp/vault/sdk/logical" ) var ErrStorageItemNotFound = errors.New("storage item not found") const ( storageKeyConfig = issuing.StorageKeyConfig storageIssuerConfig = issuing.StorageIssuerConfig keyPrefix = issuing.KeyPrefix issuerPrefix = issuing.IssuerPrefix storageLocalCRLConfig = issuing.StorageLocalCRLConfig storageUnifiedCRLConfig = issuing.StorageUnifiedCRLConfig legacyMigrationBundleLogKey = "config/legacyMigrationBundleLog" legacyCertBundlePath = issuing.LegacyCertBundlePath legacyCertBundleBackupPath = "config/ca_bundle.bak" legacyCRLPath = issuing.LegacyCRLPath deltaCRLPath = issuing.DeltaCRLPath deltaCRLPathSuffix = issuing.DeltaCRLPathSuffix unifiedCRLPath = issuing.UnifiedCRLPath unifiedDeltaCRLPath = issuing.UnifiedDeltaCRLPath unifiedCRLPathPrefix = issuing.UnifiedCRLPathPrefix autoTidyConfigPath = "config/auto-tidy" clusterConfigPath = "config/cluster" autoTidyLastRunPath = "config/auto-tidy-last-run" maxRolesToScanOnIssuerChange = 100 maxRolesToFindOnIssuerChange = 10 ) func ToURLEntries(sc *storageContext, issuer issuing.IssuerID, c *issuing.AiaConfigEntry) (*certutil.URLEntries, error) { return issuing.ToURLEntries(sc.Context, sc.Storage, issuer, c) } type storageContext struct { Context context.Context Storage logical.Storage Backend *backend } var _ pki_backend.StorageContext = (*storageContext)(nil) func (b *backend) makeStorageContext(ctx context.Context, s logical.Storage) *storageContext { return &storageContext{ Context: ctx, Storage: s, Backend: b, } } func (sc *storageContext) WithFreshTimeout(timeout time.Duration) (*storageContext, context.CancelFunc) { ctx, cancel := context.WithTimeout(context.Background(), timeout) return &storageContext{ Context: ctx, Storage: sc.Storage, Backend: sc.Backend, }, cancel } func (sc *storageContext) GetContext() context.Context { return sc.Context } func (sc *storageContext) GetStorage() logical.Storage { return sc.Storage } func (sc *storageContext) Logger() hclog.Logger { return sc.Backend.Logger() } func (sc *storageContext) System() logical.SystemView { return sc.Backend.System() } func (sc *storageContext) CrlBuilder() pki_backend.CrlBuilderType { return sc.Backend.CrlBuilder() } func (sc *storageContext) GetUnifiedTransferStatus() *UnifiedTransferStatus { return sc.Backend.GetUnifiedTransferStatus() } func (sc *storageContext) GetPkiManagedView() managed_key.PkiManagedKeyView { return sc.Backend } func (sc *storageContext) GetCertificateCounter() issuing.CertificateCounter { return sc.Backend.GetCertificateCounter() } func (sc *storageContext) UseLegacyBundleCaStorage() bool { return sc.Backend.UseLegacyBundleCaStorage() } func (sc *storageContext) GetRevokeStorageLock() *sync.RWMutex { return sc.Backend.GetRevokeStorageLock() } func (sc *storageContext) GetRole(name string) (*issuing.RoleEntry, error) { return sc.Backend.GetRole(sc.Context, sc.Storage, name) } func (sc *storageContext) listKeys() ([]issuing.KeyID, error) { return issuing.ListKeys(sc.Context, sc.Storage) } func (sc *storageContext) fetchKeyById(keyId issuing.KeyID) (*issuing.KeyEntry, error) { return issuing.FetchKeyById(sc.Context, sc.Storage, keyId) } func (sc *storageContext) writeKey(key issuing.KeyEntry) error { return issuing.WriteKey(sc.Context, sc.Storage, key) } func (sc *storageContext) deleteKey(id issuing.KeyID) (bool, error) { return issuing.DeleteKey(sc.Context, sc.Storage, id) } func (sc *storageContext) importKey(keyValue string, keyName string, keyType certutil.PrivateKeyType) (*issuing.KeyEntry, bool, error) { // importKey imports the specified PEM-format key (from keyValue) into // the new PKI storage format. The first return field is a reference to // the new key; the second is whether or not the key already existed // during import (in which case, *key points to the existing key reference // and identifier); the last return field is whether or not an error // occurred. // // Normalize whitespace before beginning. See note in importIssuer as to // why we do this. keyValue = strings.TrimSpace(keyValue) + "\n" // // Before we can import a known key, we first need to know if the key // exists in storage already. This means iterating through all known // keys and comparing their private value against this value. knownKeys, err := sc.listKeys() if err != nil { return nil, false, err } // Get our public key from the current inbound key, to compare against all the other keys. var pkForImportingKey crypto.PublicKey if keyType == certutil.ManagedPrivateKey { pkForImportingKey, err = managed_key.GetPublicKeyFromKeyBytes(sc.Context, sc.Backend, []byte(keyValue)) if err != nil { return nil, false, err } } else { pkForImportingKey, err = getPublicKeyFromBytes([]byte(keyValue)) if err != nil { return nil, false, err } } foundExistingKeyWithName := false for _, identifier := range knownKeys { existingKey, err := sc.fetchKeyById(identifier) if err != nil { return nil, false, err } areEqual, err := comparePublicKey(sc, existingKey, pkForImportingKey) if err != nil { return nil, false, err } if areEqual { // Here, we don't need to stitch together the issuer entries, // because the last run should've done that for us (or, when // importing an issuer). return existingKey, true, nil } // Allow us to find an existing matching key with a different name before erroring out if keyName != "" && existingKey.Name == keyName { foundExistingKeyWithName = true } } // Another key with a different value is using the keyName so reject this request. if foundExistingKeyWithName { return nil, false, errutil.UserError{Err: fmt.Sprintf("an existing key is using the requested key name value: %s", keyName)} } // Haven't found a key, so we've gotta create it and write it into storage. var result issuing.KeyEntry result.ID = genKeyId() result.Name = keyName result.PrivateKey = keyValue result.PrivateKeyType = keyType // Finally, we can write the key to storage. if err := sc.writeKey(result); err != nil { return nil, false, err } // Before we return below, we need to iterate over _all_ issuers and see if // one of them has a missing KeyId link, and if so, point it back to // ourselves. We fetch the list of issuers up front, even when don't need // it, to give ourselves a better chance of succeeding below. knownIssuers, err := sc.listIssuers() if err != nil { return nil, false, err } issuerDefaultSet, err := sc.isDefaultIssuerSet() if err != nil { return nil, false, err } // Now, for each issuer, try and compute the issuer<->key link if missing. for _, identifier := range knownIssuers { existingIssuer, err := sc.fetchIssuerById(identifier) if err != nil { return nil, false, err } // If the KeyID value is already present, we can skip it. if len(existingIssuer.KeyID) > 0 { continue } // Otherwise, compare public values. Note that there might be multiple // certificates (e.g., cross-signed) with the same key. cert, err := existingIssuer.GetCertificate() if err != nil { // Malformed issuer. return nil, false, err } equal, err := certutil.ComparePublicKeysAndType(cert.PublicKey, pkForImportingKey) if err != nil { return nil, false, err } if equal { // These public keys are equal, so this key entry must be the // corresponding private key to this issuer; update it accordingly. existingIssuer.KeyID = result.ID if err := sc.writeIssuer(existingIssuer); err != nil { return nil, false, err } // If there was no prior default value set and/or we had no known // issuers when we started, set this issuer as default. if !issuerDefaultSet { err = sc.updateDefaultIssuerId(existingIssuer.ID) if err != nil { return nil, false, err } issuerDefaultSet = true } } } // If there was no prior default value set and/or we had no known // keys when we started, set this key as default. keyDefaultSet, err := sc.isDefaultKeySet() if err != nil { return nil, false, err } if len(knownKeys) == 0 || !keyDefaultSet { if err = sc.updateDefaultKeyId(result.ID); err != nil { return nil, false, err } } // All done; return our new key reference. return &result, false, nil } func GetAIAURLs(sc *storageContext, i *issuing.IssuerEntry) (*certutil.URLEntries, error) { return issuing.GetAIAURLs(sc.Context, sc.Storage, i) } func (sc *storageContext) listIssuers() ([]issuing.IssuerID, error) { return issuing.ListIssuers(sc.Context, sc.Storage) } func (sc *storageContext) resolveKeyReference(reference string) (issuing.KeyID, error) { return issuing.ResolveKeyReference(sc.Context, sc.Storage, reference) } // fetchIssuerById returns an IssuerEntry based on issuerId, if none found an error is returned. func (sc *storageContext) fetchIssuerById(issuerId issuing.IssuerID) (*issuing.IssuerEntry, error) { return issuing.FetchIssuerById(sc.Context, sc.Storage, issuerId) } func (sc *storageContext) writeIssuer(issuer *issuing.IssuerEntry) error { return issuing.WriteIssuer(sc.Context, sc.Storage, issuer) } func (sc *storageContext) deleteIssuer(id issuing.IssuerID) (bool, error) { return issuing.DeleteIssuer(sc.Context, sc.Storage, id) } func (sc *storageContext) importIssuer(certValue string, issuerName string) (*issuing.IssuerEntry, bool, error) { // importIssuers imports the specified PEM-format certificate (from // certValue) into the new PKI storage format. The first return field is a // reference to the new issuer; the second is whether or not the issuer // already existed during import (in which case, *issuer points to the // existing issuer reference and identifier); the last return field is // whether or not an error occurred. // Before we begin, we need to ensure the PEM formatted certificate looks // good. Restricting to "just" `CERTIFICATE` entries is a little // restrictive, as it could be a `X509 CERTIFICATE` entry or a custom // value wrapping an actual DER cert. So validating the contents of the // PEM header is out of the question (and validating the contents of the // PEM block is left to our GetCertificate call below). // // However, we should trim all leading and trailing spaces and add a // single new line. This allows callers to blindly concatenate PEM // blobs from the API and get roughly what they'd expect. // // Discussed further in #11960 and RFC 7468. certValue = strings.TrimSpace(certValue) + "\n" // Extracting the certificate is necessary for two reasons: first, it lets // us fetch the serial number; second, for the public key comparison with // known keys. issuerCert, err := parseCertificateFromBytes([]byte(certValue)) if err != nil { return nil, false, err } // Ensure this certificate is a usable as a CA certificate. if !issuerCert.BasicConstraintsValid || !issuerCert.IsCA { return nil, false, errutil.UserError{Err: "Refusing to import non-CA certificate"} } // Ensure this certificate has a parsed public key. Otherwise, we've // likely been given a bad certificate. if issuerCert.PublicKeyAlgorithm == x509.UnknownPublicKeyAlgorithm || issuerCert.PublicKey == nil { return nil, false, errutil.UserError{Err: "Refusing to import CA certificate with empty PublicKey. This usually means the SubjectPublicKeyInfo field has an OID not recognized by Go, such as 1.2.840.113549.1.1.10 for rsaPSS."} } // Before we can import a known issuer, we first need to know if the issuer // exists in storage already. This means iterating through all known // issuers and comparing their private value against this value. knownIssuers, err := sc.listIssuers() if err != nil { return nil, false, err } foundExistingIssuerWithName := false for _, identifier := range knownIssuers { existingIssuer, err := sc.fetchIssuerById(identifier) if err != nil { return nil, false, err } existingIssuerCert, err := existingIssuer.GetCertificate() if err != nil { return nil, false, err } if areCertificatesEqual(existingIssuerCert, issuerCert) { // Here, we don't need to stitch together the key entries, // because the last run should've done that for us (or, when // importing a key). return existingIssuer, true, nil } // Allow us to find an existing matching issuer with a different name before erroring out if issuerName != "" && existingIssuer.Name == issuerName { foundExistingIssuerWithName = true } } if foundExistingIssuerWithName { return nil, false, errutil.UserError{Err: fmt.Sprintf("another issuer is using the requested name: %s", issuerName)} } // Haven't found an issuer, so we've gotta create it and write it into // storage. var result issuing.IssuerEntry result.ID = genIssuerId() result.Name = issuerName result.Certificate = certValue result.LeafNotAfterBehavior = certutil.ErrNotAfterBehavior result.Usage.ToggleUsage(issuing.AllIssuerUsages) result.Version = issuing.LatestIssuerVersion // If we lack relevant bits for CRL, prohibit it from being set // on the usage side. if (issuerCert.KeyUsage&x509.KeyUsageCRLSign) == 0 && result.Usage.HasUsage(issuing.CRLSigningUsage) { result.Usage.ToggleUsage(issuing.CRLSigningUsage) } // We shouldn't add CSRs or multiple certificates in this countCertificates := strings.Count(result.Certificate, "-BEGIN ") if countCertificates != 1 { return nil, false, fmt.Errorf("bad issuer: potentially multiple PEM blobs in one certificate storage entry:\n%v", result.Certificate) } result.SerialNumber = serialFromCert(issuerCert) // Before we return below, we need to iterate over _all_ keys and see if // one of them a public key matching this certificate, and if so, update our // link accordingly. We fetch the list of keys up front, even may not need // it, to give ourselves a better chance of succeeding below. knownKeys, err := sc.listKeys() if err != nil { return nil, false, err } // Now, for each key, try and compute the issuer<->key link. We delay // writing issuer to storage as we won't need to update the key, only // the issuer. for _, identifier := range knownKeys { existingKey, err := sc.fetchKeyById(identifier) if err != nil { return nil, false, err } equal, err := comparePublicKey(sc, existingKey, issuerCert.PublicKey) if err != nil { return nil, false, err } if equal { result.KeyID = existingKey.ID // Here, there's exactly one stored key with the same public key // as us, per guarantees in importKey; as we're importing an // issuer, there's no other keys or issuers we'd need to read or // update, so exit. break } } // Finally, rebuild the chains. In this process, because the provided // reference issuer is non-nil, we'll save this issuer to storage. if err := sc.rebuildIssuersChains(&result); err != nil { return nil, false, err } // If there was no prior default value set and/or we had no known // issuers when we started, set this issuer as default. issuerDefaultSet, err := sc.isDefaultIssuerSet() if err != nil { return nil, false, err } if (len(knownIssuers) == 0 || !issuerDefaultSet) && len(result.KeyID) != 0 { if err = sc.updateDefaultIssuerId(result.ID); err != nil { return nil, false, err } } // All done; return our new key reference. return &result, false, nil } func areCertificatesEqual(cert1 *x509.Certificate, cert2 *x509.Certificate) bool { return bytes.Equal(cert1.Raw, cert2.Raw) } func (sc *storageContext) setLocalCRLConfig(mapping *issuing.InternalCRLConfigEntry) error { return issuing.SetLocalCRLConfig(sc.Context, sc.Storage, mapping) } func (sc *storageContext) setUnifiedCRLConfig(mapping *issuing.InternalCRLConfigEntry) error { return issuing.SetUnifiedCRLConfig(sc.Context, sc.Storage, mapping) } func (sc *storageContext) getLocalCRLConfig() (*issuing.InternalCRLConfigEntry, error) { return issuing.GetLocalCRLConfig(sc.Context, sc.Storage) } func (sc *storageContext) getUnifiedCRLConfig() (*issuing.InternalCRLConfigEntry, error) { return issuing.GetUnifiedCRLConfig(sc.Context, sc.Storage) } func (sc *storageContext) setKeysConfig(config *issuing.KeyConfigEntry) error { return issuing.SetKeysConfig(sc.Context, sc.Storage, config) } func (sc *storageContext) getKeysConfig() (*issuing.KeyConfigEntry, error) { return issuing.GetKeysConfig(sc.Context, sc.Storage) } func (sc *storageContext) setIssuersConfig(config *issuing.IssuerConfigEntry) error { return issuing.SetIssuersConfig(sc.Context, sc.Storage, config) } func (sc *storageContext) getIssuersConfig() (*issuing.IssuerConfigEntry, error) { return issuing.GetIssuersConfig(sc.Context, sc.Storage) } // Lookup within storage the value of reference, assuming the string is a reference to an issuer entry, // returning the converted IssuerID or an error if not found. This method will not properly resolve the // special legacyBundleShimID value as we do not want to confuse our special value and a user-provided name of the // same value. func (sc *storageContext) resolveIssuerReference(reference string) (issuing.IssuerID, error) { return issuing.ResolveIssuerReference(sc.Context, sc.Storage, reference) } // Builds a certutil.CertBundle from the specified issuer identifier, // optionally loading the key or not. This method supports loading legacy // bundles using the legacyBundleShimID issuerId, and if no entry is found will return an error. func (sc *storageContext) fetchCertBundleByIssuerId(id issuing.IssuerID, loadKey bool) (*issuing.IssuerEntry, *certutil.CertBundle, error) { return issuing.FetchCertBundleByIssuerId(sc.Context, sc.Storage, id, loadKey) } func (sc *storageContext) writeCaBundle(caBundle *certutil.CertBundle, issuerName string, keyName string) (*issuing.IssuerEntry, *issuing.KeyEntry, error) { myKey, _, err := sc.importKey(caBundle.PrivateKey, keyName, caBundle.PrivateKeyType) if err != nil { return nil, nil, err } // We may have existing mounts that only contained a key with no certificate yet as a signed CSR // was never setup within the mount. if caBundle.Certificate == "" { return &issuing.IssuerEntry{}, myKey, nil } myIssuer, _, err := sc.importIssuer(caBundle.Certificate, issuerName) if err != nil { return nil, nil, err } for _, cert := range caBundle.CAChain { if _, _, err = sc.importIssuer(cert, ""); err != nil { return nil, nil, err } } return myIssuer, myKey, nil } func genIssuerId() issuing.IssuerID { return issuing.IssuerID(genUuid()) } func genKeyId() issuing.KeyID { return issuing.KeyID(genUuid()) } func genCRLId() issuing.CrlID { return issuing.CrlID(genUuid()) } func genUuid() string { aUuid, err := uuid.GenerateUUID() if err != nil { panic(err) } return aUuid } func (sc *storageContext) isKeyInUse(keyId string) (inUse bool, issuerId string, err error) { knownIssuers, err := sc.listIssuers() if err != nil { return true, "", err } for _, issuerId := range knownIssuers { issuerEntry, err := sc.fetchIssuerById(issuerId) if err != nil { return true, issuerId.String(), errutil.InternalError{Err: fmt.Sprintf("unable to fetch pki issuer: %v", err)} } if issuerEntry == nil { return true, issuerId.String(), errutil.InternalError{Err: fmt.Sprintf("Issuer listed: %s does not exist", issuerId.String())} } if issuerEntry.KeyID.String() == keyId { return true, issuerId.String(), nil } } return false, "", nil } func (sc *storageContext) checkForRolesReferencing(issuerId string) (timeout bool, inUseBy int32, err error) { roleEntries, err := sc.Storage.List(sc.Context, "role/") if err != nil { return false, 0, err } inUseBy = 0 checkedRoles := 0 for _, roleName := range roleEntries { entry, err := sc.Storage.Get(sc.Context, "role/"+roleName) if err != nil { return false, 0, err } if entry != nil { // If nil, someone deleted an entry since we haven't taken a lock here so just continue var role issuing.RoleEntry err = entry.DecodeJSON(&role) if err != nil { return false, inUseBy, err } if role.Issuer == issuerId { inUseBy = inUseBy + 1 if inUseBy >= maxRolesToFindOnIssuerChange { return true, inUseBy, nil } } } checkedRoles = checkedRoles + 1 if checkedRoles >= maxRolesToScanOnIssuerChange { return true, inUseBy, nil } } return false, inUseBy, nil } func (sc *storageContext) getRevocationConfig() (*pki_backend.CrlConfig, error) { entry, err := sc.Storage.Get(sc.Context, "config/crl") if err != nil { return nil, err } var result pki_backend.CrlConfig if entry == nil { result = pki_backend.DefaultCrlConfig return &result, nil } if err = entry.DecodeJSON(&result); err != nil { return nil, err } if result.Version == 0 { // Automatically update existing configurations. result.OcspDisable = pki_backend.DefaultCrlConfig.OcspDisable result.OcspExpiry = pki_backend.DefaultCrlConfig.OcspExpiry result.AutoRebuild = pki_backend.DefaultCrlConfig.AutoRebuild result.AutoRebuildGracePeriod = pki_backend.DefaultCrlConfig.AutoRebuildGracePeriod result.Version = 1 } if result.Version == 1 { if result.DeltaRebuildInterval == "" { result.DeltaRebuildInterval = pki_backend.DefaultCrlConfig.DeltaRebuildInterval } result.Version = 2 } // Depending on client version, it's possible that the expiry is unset. // This sets the default value to prevent issues in downstream code. if result.Expiry == "" { result.Expiry = pki_backend.DefaultCrlConfig.Expiry } isLocalMount := sc.System().LocalMount() if (!constants.IsEnterprise || isLocalMount) && (result.UnifiedCRLOnExistingPaths || result.UnifiedCRL || result.UseGlobalQueue) { // An end user must have had Enterprise, enabled the unified config args and then downgraded to OSS. sc.Logger().Warn("Not running Vault Enterprise or using a local mount, " + "disabling unified_crl, unified_crl_on_existing_paths and cross_cluster_revocation config flags.") result.UnifiedCRLOnExistingPaths = false result.UnifiedCRL = false result.UseGlobalQueue = false } return &result, nil } func (sc *storageContext) setRevocationConfig(config *pki_backend.CrlConfig) error { entry, err := logical.StorageEntryJSON("config/crl", config) if err != nil { return fmt.Errorf("failed building storage entry JSON: %w", err) } err = sc.Storage.Put(sc.Context, entry) if err != nil { return fmt.Errorf("failed writing storage entry: %w", err) } return nil } func (sc *storageContext) getAutoTidyConfig() (*tidyConfig, error) { entry, err := sc.Storage.Get(sc.Context, autoTidyConfigPath) if err != nil { return nil, err } var result tidyConfig if entry == nil { result = defaultTidyConfig return &result, nil } if err = entry.DecodeJSON(&result); err != nil { return nil, err } if result.IssuerSafetyBuffer == 0 { result.IssuerSafetyBuffer = defaultTidyConfig.IssuerSafetyBuffer } if result.MinStartupBackoff == 0 { result.MinStartupBackoff = defaultTidyConfig.MinStartupBackoff } if result.MaxStartupBackoff == 0 { result.MaxStartupBackoff = defaultTidyConfig.MaxStartupBackoff } return &result, nil } func (sc *storageContext) writeAutoTidyConfig(config *tidyConfig) error { entry, err := logical.StorageEntryJSON(autoTidyConfigPath, config) if err != nil { return err } err = sc.Storage.Put(sc.Context, entry) if err != nil { return err } certCounter := sc.Backend.GetCertificateCounter() certCounter.ReconfigureWithTidyConfig(config) return nil } func (sc *storageContext) listRevokedCerts() ([]string, error) { list, err := sc.Storage.List(sc.Context, revokedPath) if err != nil { return nil, fmt.Errorf("failed listing revoked certs: %w", err) } return list, err } func (sc *storageContext) getClusterConfig() (*issuing.ClusterConfigEntry, error) { entry, err := sc.Storage.Get(sc.Context, clusterConfigPath) if err != nil { return nil, err } var result issuing.ClusterConfigEntry if entry == nil { return &result, nil } if err = entry.DecodeJSON(&result); err != nil { return nil, err } return &result, nil } func (sc *storageContext) writeClusterConfig(config *issuing.ClusterConfigEntry) error { entry, err := logical.StorageEntryJSON(clusterConfigPath, config) if err != nil { return err } return sc.Storage.Put(sc.Context, entry) } // tidyLastRun Track the various pieces of information around tidy on a specific cluster type tidyLastRun struct { LastRunTime time.Time } func (sc *storageContext) getAutoTidyLastRun() (time.Time, error) { entry, err := sc.Storage.Get(sc.Context, autoTidyLastRunPath) if err != nil { return time.Time{}, fmt.Errorf("failed getting auto tidy last run: %w", err) } if entry == nil { return time.Time{}, nil } var result tidyLastRun if err = entry.DecodeJSON(&result); err != nil { return time.Time{}, fmt.Errorf("failed parsing auto tidy last run: %w", err) } return result.LastRunTime, nil } func (sc *storageContext) writeAutoTidyLastRun(lastRunTime time.Time) error { lastRun := tidyLastRun{LastRunTime: lastRunTime} entry, err := logical.StorageEntryJSON(autoTidyLastRunPath, lastRun) if err != nil { return fmt.Errorf("failed generating json for auto tidy last run: %w", err) } if err := sc.Storage.Put(sc.Context, entry); err != nil { return fmt.Errorf("failed writing auto tidy last run: %w", err) } return nil } func fetchRevocationInfo(sc pki_backend.StorageContext, serial string) (*revocation.RevocationInfo, error) { var revInfo *revocation.RevocationInfo revEntry, err := fetchCertBySerial(sc, revocation.RevokedPath, serial) if err != nil { return nil, err } if revEntry != nil { err = revEntry.DecodeJSON(&revInfo) if err != nil { return nil, fmt.Errorf("error decoding existing revocation info: %w", err) } } return revInfo, nil }