blob: b89bb0e64bc94d75d79b75a9d7680efcc4974d82 [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package pki
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"time"
"github.com/hashicorp/vault/sdk/helper/certutil"
"github.com/hashicorp/vault/sdk/logical"
)
// This allows us to record the version of the migration code within the log entry
// in case we find out in the future that something was horribly wrong with the migration,
// and we need to perform it again...
const (
latestMigrationVersion = 2
legacyBundleShimID = issuerID("legacy-entry-shim-id")
legacyBundleShimKeyID = keyID("legacy-entry-shim-key-id")
)
type legacyBundleMigrationLog struct {
Hash string `json:"hash"`
Created time.Time `json:"created"`
CreatedIssuer issuerID `json:"issuer_id"`
CreatedKey keyID `json:"key_id"`
MigrationVersion int `json:"migrationVersion"`
}
type migrationInfo struct {
isRequired bool
legacyBundle *certutil.CertBundle
legacyBundleHash string
migrationLog *legacyBundleMigrationLog
}
func getMigrationInfo(ctx context.Context, s logical.Storage) (migrationInfo, error) {
migrationInfo := migrationInfo{
isRequired: false,
legacyBundle: nil,
legacyBundleHash: "",
migrationLog: nil,
}
var err error
_, migrationInfo.legacyBundle, err = getLegacyCertBundle(ctx, s)
if err != nil {
return migrationInfo, err
}
migrationInfo.migrationLog, err = getLegacyBundleMigrationLog(ctx, s)
if err != nil {
return migrationInfo, err
}
migrationInfo.legacyBundleHash, err = computeHashOfLegacyBundle(migrationInfo.legacyBundle)
if err != nil {
return migrationInfo, err
}
// Even if there isn't anything to migrate, we always want to write out the log entry
// as that will trigger the secondary clusters to toggle/wake up
if (migrationInfo.migrationLog == nil) ||
(migrationInfo.migrationLog.Hash != migrationInfo.legacyBundleHash) ||
(migrationInfo.migrationLog.MigrationVersion != latestMigrationVersion) {
migrationInfo.isRequired = true
}
return migrationInfo, nil
}
func migrateStorage(ctx context.Context, b *backend, s logical.Storage) error {
migrationInfo, err := getMigrationInfo(ctx, s)
if err != nil {
return err
}
if !migrationInfo.isRequired {
// No migration was deemed to be required.
return nil
}
var issuerIdentifier issuerID
var keyIdentifier keyID
sc := b.makeStorageContext(ctx, s)
if migrationInfo.legacyBundle != nil {
// When the legacy bundle still exists, there's three scenarios we
// need to worry about:
//
// 1. When we have no migration log, we definitely want to migrate.
haveNoLog := migrationInfo.migrationLog == nil
// 2. When we have an (empty) log and the version is zero, we want to
// migrate.
haveOldVersion := !haveNoLog && migrationInfo.migrationLog.MigrationVersion == 0
// 3. When we have a log and the version is at least 1 (where this
// migration was introduced), we want to run the migration again
// only if the legacy bundle hash has changed.
isCurrentOrBetterVersion := !haveNoLog && migrationInfo.migrationLog.MigrationVersion >= 1
haveChange := !haveNoLog && migrationInfo.migrationLog.Hash != migrationInfo.legacyBundleHash
haveVersionWithChange := isCurrentOrBetterVersion && haveChange
if haveNoLog || haveOldVersion || haveVersionWithChange {
// Generate a unique name for the migrated items in case things were to be re-migrated again
// for some weird reason in the future...
migrationName := fmt.Sprintf("current-%d", time.Now().Unix())
b.Logger().Info("performing PKI migration to new keys/issuers layout")
anIssuer, aKey, err := sc.writeCaBundle(migrationInfo.legacyBundle, migrationName, migrationName)
if err != nil {
return err
}
b.Logger().Info("Migration generated the following ids and set them as defaults",
"issuer id", anIssuer.ID, "key id", aKey.ID)
issuerIdentifier = anIssuer.ID
keyIdentifier = aKey.ID
// Since we do not have all the mount information available we must schedule
// the CRL to be rebuilt at a later time.
b.crlBuilder.requestRebuildIfActiveNode(b)
}
}
if migrationInfo.migrationLog != nil && migrationInfo.migrationLog.MigrationVersion == 1 {
// We've seen a bundle with migration version 1; this means an
// earlier version of the code ran which didn't have the fix for
// correct write order in rebuildIssuersChains(...). Rather than
// having every user read the migrated active issuer and see if
// their chains need rebuilding, we'll schedule a one-off chain
// migration here.
b.Logger().Info(fmt.Sprintf("%v: performing maintenance rebuild of ca_chains", b.backendUUID))
if err := sc.rebuildIssuersChains(nil); err != nil {
return err
}
}
// We always want to write out this log entry as the secondary clusters leverage this path to wake up
// if they were upgraded prior to the primary cluster's migration occurred.
err = setLegacyBundleMigrationLog(ctx, s, &legacyBundleMigrationLog{
Hash: migrationInfo.legacyBundleHash,
Created: time.Now(),
CreatedIssuer: issuerIdentifier,
CreatedKey: keyIdentifier,
MigrationVersion: latestMigrationVersion,
})
if err != nil {
return err
}
b.Logger().Info(fmt.Sprintf("%v: succeeded in migrating to issuer storage version %v", b.backendUUID, latestMigrationVersion))
return nil
}
func computeHashOfLegacyBundle(bundle *certutil.CertBundle) (string, error) {
hasher := sha256.New()
// Generate an empty hash if the bundle does not exist.
if bundle != nil {
// We only hash the main certificate and the certs within the CAChain,
// assuming that any sort of change that occurred would have influenced one of those two fields.
if _, err := hasher.Write([]byte(bundle.Certificate)); err != nil {
return "", err
}
for _, cert := range bundle.CAChain {
if _, err := hasher.Write([]byte(cert)); err != nil {
return "", err
}
}
}
return hex.EncodeToString(hasher.Sum(nil)), nil
}
func getLegacyBundleMigrationLog(ctx context.Context, s logical.Storage) (*legacyBundleMigrationLog, error) {
entry, err := s.Get(ctx, legacyMigrationBundleLogKey)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
lbm := &legacyBundleMigrationLog{}
err = entry.DecodeJSON(lbm)
if err != nil {
// If we can't decode our bundle, lets scrap it and assume a blank value,
// re-running the migration will at most bring back an older certificate/private key
return nil, nil
}
return lbm, nil
}
func setLegacyBundleMigrationLog(ctx context.Context, s logical.Storage, lbm *legacyBundleMigrationLog) error {
json, err := logical.StorageEntryJSON(legacyMigrationBundleLogKey, lbm)
if err != nil {
return err
}
return s.Put(ctx, json)
}
func getLegacyCertBundle(ctx context.Context, s logical.Storage) (*issuerEntry, *certutil.CertBundle, error) {
entry, err := s.Get(ctx, legacyCertBundlePath)
if err != nil {
return nil, nil, err
}
if entry == nil {
return nil, nil, nil
}
cb := &certutil.CertBundle{}
err = entry.DecodeJSON(cb)
if err != nil {
return nil, nil, err
}
// Fake a storage entry with backwards compatibility in mind.
issuer := &issuerEntry{
ID: legacyBundleShimID,
KeyID: legacyBundleShimKeyID,
Name: "legacy-entry-shim",
Certificate: cb.Certificate,
CAChain: cb.CAChain,
SerialNumber: cb.SerialNumber,
LeafNotAfterBehavior: certutil.ErrNotAfterBehavior,
}
issuer.Usage.ToggleUsage(AllIssuerUsages)
return issuer, cb, nil
}