From beafe255084747f70e13d1cef37ad1bdaacbe7d4 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 15 Jan 2016 14:02:51 -0500 Subject: [PATCH] Initial transit key archiving work --- builtin/logical/transit/path_config.go | 4 + builtin/logical/transit/policy.go | 189 ++++++++++++++++++++++++- 2 files changed, 187 insertions(+), 6 deletions(-) diff --git a/builtin/logical/transit/path_config.go b/builtin/logical/transit/path_config.go index d68726b4e..6e1331c08 100644 --- a/builtin/logical/transit/path_config.go +++ b/builtin/logical/transit/path_config.go @@ -57,6 +57,10 @@ func pathConfigWrite( minDecryptionVersion := d.Get("min_decryption_version").(int) if minDecryptionVersion != 0 && minDecryptionVersion != policy.MinDecryptionVersion { + if minDecryptionVersion > policy.LatestVersion { + return logical.ErrorResponse( + fmt.Sprintf("cannot set min decryption version of %d, latest key version is %d", minDecryptionVersion, policy.LatestVersion)), nil + } policy.MinDecryptionVersion = minDecryptionVersion persistNeeded = true } diff --git a/builtin/logical/transit/policy.go b/builtin/logical/transit/policy.go index fbda93111..f395f3f7a 100644 --- a/builtin/logical/transit/policy.go +++ b/builtin/logical/transit/policy.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "encoding/base64" "encoding/json" + "fmt" "strconv" "strings" "time" @@ -72,11 +73,174 @@ type Policy struct { // for decryption MinDecryptionVersion int `json:"min_decryption_version"` + // The latest key version in this policy + LatestVersion int `json:"latest_version"` + + // The latest key version in the archive. We never delete these, so this is a max. + ArchiveVersion int `json:"archive_version"` + // Whether the key is allowed to be deleted DeletionAllowed bool `json:"deletion_allowed"` } +// ArchivedKeys stores old keys. This is used to keep the key loading time sane when +// there are huge numbers of rotations. +type ArchivedKeys struct { + Keys []KeyEntry `json:"keys"` +} + +func (p *Policy) loadArchive(storage logical.Storage, name string) (*ArchivedKeys, error) { + archive := &ArchivedKeys{} + + raw, err := storage.Get("policy/" + name + "/archive") + if err != nil { + return nil, err + } + if raw == nil { + archive.Keys = make([]KeyEntry, 0) + return archive, nil + } + + if err := json.Unmarshal(raw.Value, archive); err != nil { + return nil, err + } + + return archive, nil +} + +func (p *Policy) storeArchive(archive *ArchivedKeys, storage logical.Storage, name string) error { + // Encode the policy + buf, err := json.Marshal(archive) + if err != nil { + return err + } + + // Write the policy into storage + err = storage.Put(&logical.StorageEntry{ + Key: "policy/" + name + "/archive", + Value: buf, + }) + if err != nil { + return err + } + + return nil +} + +// handleArchiving manages the movement of keys to and from the policy archive. +// This should *ONLY* be called from Persist() since it assumes that the policy +// will be persisted afterwards. +func (p *Policy) handleArchiving(storage logical.Storage, name string) error { + // We need to move keys that are no longer accessible to ArchivedKeys, and keys + // that now need to be accessible back here. + // + // For safety, because there isn't really a good reason to, we never delete + // keys from the archive even when we move them back. + + // 0/1 are aliases, so don't deal with this code path unless we're past that + if p.MinDecryptionVersion < 2 { + return nil + } + + // Check if we have the latest minimum version in the current set of keys + _, keysContainsMinimum := p.Keys[p.MinDecryptionVersion] + + // If keys contains the minimum value, we are moving keys *to* the archive, + // but we only need to do this if the archive doesn't contain those key + // versions, since we don't remove key versions from the archive. + if keysContainsMinimum && + p.ArchiveVersion >= p.MinDecryptionVersion-1 { + return nil + } + + archive, err := p.loadArchive(storage, name) + if err != nil { + return err + } + + if keysContainsMinimum { + // Need to move keys *to* archive + + if len(archive.Keys) < p.MinDecryptionVersion-1 { + // Increase the size of the archive slice. We need a size that is + // equivalent to the minimum decryption version minus 1, but adding + // one since slice numbering starts at 0 and we're indexing by key + // version + newKeys := make([]KeyEntry, p.MinDecryptionVersion) + copy(newKeys, archive.Keys) + archive.Keys = newKeys + } + + // As we are archiving progressively, we should only have to archive + // from the min version down to the latest version minus however many + // keys are in the policy's map. For example, if we have never + // archived, the latest version is 10, and we move the min decryption + // version to 5, we will archive from 4 down to (10-10) non-inclusive. + // If the latest version now becomes 8, we will archive from 7 down to + // (10-6) non-inclusive, e.g. keys 5, 6, and 7. + for i := p.LatestVersion - len(p.Keys) + 1; i < p.MinDecryptionVersion; i-- { + archive.Keys[i] = p.Keys[i] + } + + err = p.storeArchive(archive, storage, name) + if err != nil { + return err + } + + // Perform deletion afterwards so that if there is an error saving we + // haven't messed with the current policy + for i := p.LatestVersion - len(p.Keys) + 1; i < p.MinDecryptionVersion; i-- { + delete(p.Keys, i) + } + + // Update the archive max key version. This also corresponds to the + // maximum safe index into the slice. Continuing our example from + // before, p.ArchiveVersion will now be 7. + p.ArchiveVersion = p.MinDecryptionVersion - 1 + + } else { + // Need to move keys *from* archive + + // If we've been archiving, keys should have been archived + // sequentially. So we can perform a sanity check. First test the + // actual latest version in the policy, so continuing the previous + // example, if the key version is 10 and the minimum was 8, p.Keys + // should hold 8, 9, and 10. Now if we move the minimum back, e.g. to + // 5, we need to load keys 5, 6, and 7, so should load everything up to + // (10-3), inclusive. If p.ArchiveVersion is less than this (which it + // shouldn't be, as set earlier in the example), we have a problem. + // Also, we should never have a situation where the Archive version is + // less than the minimum decryption version but we also do not have the + // minimum version in p.Keys (which is the only way we'd be in this + // code path to begin with). That's also a problem. + // + // Note that we *should never have these problems*. If we do it's + // serious. + if p.ArchiveVersion < p.LatestVersion-len(p.Keys) || + p.ArchiveVersion < p.MinDecryptionVersion { + return fmt.Errorf("latest archived key version not high enough to satisfy request") + } + + for i := p.MinDecryptionVersion; i <= p.LatestVersion-len(p.Keys); i++ { + _, ok := p.Keys[i] + if ok { + // We hit the beginning of the values currently in the keyset, + // so break + break + } + p.Keys[i] = archive.Keys[i] + } + } + + return nil +} + func (p *Policy) Persist(storage logical.Storage, name string) error { + err := p.handleArchiving(storage, name) + if err != nil { + return err + } + // Encode the policy buf, err := p.Serialize() if err != nil { @@ -104,18 +268,18 @@ func (p *Policy) Serialize() ([]byte, error) { // raw key is used and no context is required, otherwise the KDF // mode is used with the context to derive the proper key. func (p *Policy) DeriveKey(context []byte, ver int) ([]byte, error) { - if p.Keys == nil || len(p.Keys) == 0 { + if p.Keys == nil || p.LatestVersion == 0 { if p.Key == nil || len(p.Key) == 0 { return nil, certutil.InternalError{Err: "unable to access the key; no key versions found"} } p.migrateKeyToKeysMap() } - if len(p.Keys) == 0 { + if p.LatestVersion == 0 { return nil, certutil.InternalError{Err: "unable to access the key; no key versions found"} } - if ver <= 0 || ver > len(p.Keys) { + if ver <= 0 || ver > p.LatestVersion { return nil, certutil.UserError{Err: "invalid key version"} } @@ -147,7 +311,7 @@ func (p *Policy) Encrypt(context []byte, value string) (string, error) { } // Derive the key that should be used - key, err := p.DeriveKey(context, len(p.Keys)) + key, err := p.DeriveKey(context, p.LatestVersion) if err != nil { return "", certutil.InternalError{Err: err.Error()} } @@ -188,7 +352,7 @@ func (p *Policy) Encrypt(context []byte, value string) (string, error) { encoded := base64.StdEncoding.EncodeToString(full) // Prepend some information - encoded = "vault:v" + strconv.Itoa(len(p.Keys)) + ":" + encoded + encoded = "vault:v" + strconv.Itoa(p.LatestVersion) + ":" + encoded return encoded, nil } @@ -273,7 +437,10 @@ func (p *Policy) rotate(storage logical.Storage) error { if err != nil { return err } - p.Keys[len(p.Keys)+1] = KeyEntry{ + + p.LatestVersion += 1 + + p.Keys[p.LatestVersion] = KeyEntry{ Key: newKey, CreationTime: time.Now().Unix(), } @@ -324,10 +491,20 @@ func getPolicy(req *logical.Request, name string) (*Policy, error) { return nil, err } + persistNeeded := false // Ensure we've moved from Key -> Keys if p.Key != nil && len(p.Key) > 0 { p.migrateKeyToKeysMap() + persistNeeded = true + } + // With archiving, past assumptions about the length of the keys map are no longer valid + if p.LatestVersion == 0 && len(p.Keys) != 0 { + p.LatestVersion = len(p.Keys) + persistNeeded = true + } + + if persistNeeded { err = p.Persist(req.Storage, name) if err != nil { return nil, err