peridot/peridot/keykeeper/v1/keywarming.go

188 lines
5.3 KiB
Go

// Copyright (c) All respective contributors to the Peridot Project. All rights reserved.
// Copyright (c) 2021-2022 Rocky Enterprise Software Foundation, Inc. All rights reserved.
// Copyright (c) 2021-2022 Ctrl IQ, Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors
// may be used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
package keykeeperv1
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"fmt"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/google/uuid"
"io/ioutil"
"os"
"os/exec"
"peridot.resf.org/peridot/db/models"
"peridot.resf.org/utils"
"strings"
"sync"
)
// LoadedKey keeps the key and some other information in memory
// todo(mustafa): Add TTL, rotation check, etc.
type LoadedKey struct {
sync.Mutex
keyUuid uuid.UUID
gpgId string
}
func logCmdRun(cmd *exec.Cmd) (*bytes.Buffer, error) {
var outBuf bytes.Buffer
cmd.Stdout = &outBuf
cmd.Stderr = &outBuf
return &outBuf, cmd.Run()
}
func gpgCmdEnv(cmd *exec.Cmd) *exec.Cmd {
cmd.Env = append(cmd.Env, "GNUPGHOME=/keykeeper/gpg")
return cmd
}
func (s *Server) importGpgKey(armoredKey string) error {
cmd := gpgCmdEnv(exec.Command("gpg", "--batch", "--yes", "--import", "-"))
cmd.Stdin = strings.NewReader(armoredKey)
out, err := logCmdRun(cmd)
if err != nil {
s.log.Errorf("failed to import gpg key: %s", out.String())
}
return err
}
func (s *Server) importRpmKey(publicKey string) error {
tmpFile, err := ioutil.TempFile("/tmp", "peridot-key-")
if err != nil {
return err
}
defer os.Remove(tmpFile.Name())
_, err = tmpFile.Write([]byte(publicKey))
if err != nil {
return err
}
cmd := gpgCmdEnv(exec.Command("rpm", "--import", tmpFile.Name()))
out, err := logCmdRun(cmd)
if err != nil {
s.log.Errorf("failed to import rpm key: %s", out.String())
}
return err
}
// WarmGPGKey warms up a specific GPG key
// This involves shelling out to GPG to import the key
func (s *Server) WarmGPGKey(key string, armoredKey string, gpgKey *crypto.Key, db *models.Key) (*LoadedKey, error) {
cachedKeyAny, ok := s.keys.Load(key)
// This means that the key is already loaded
if ok {
return cachedKeyAny.(*LoadedKey), nil
}
err := s.importGpgKey(armoredKey)
if err != nil {
return nil, err
}
err = s.importRpmKey(db.PublicKey)
if err != nil {
return nil, err
}
cachedKey := &LoadedKey{
keyUuid: db.ID,
gpgId: gpgKey.GetHexKeyID(),
}
s.keys.Store(key, cachedKey)
return cachedKey, nil
}
// EnsureGPGKey ensures that the key is loaded
func (s *Server) EnsureGPGKey(key string) (*LoadedKey, error) {
cachedKeyAny, ok := s.keys.Load(key)
if ok {
return cachedKeyAny.(*LoadedKey), nil
}
// Key not found in cache, fetch from database
// Fetch the encryption key, nonce and external key ID from the database
k, err := s.db.GetKeyByName(key)
if err != nil {
return nil, err
}
encBytes, err := hex.DecodeString(k.EncKey)
if err != nil {
return nil, err
}
nonce, err := hex.DecodeString(k.Nonce)
if err != nil {
return nil, err
}
// Fetch the encrypted key from secret store
store := s.stores[k.ExtStoreType]
if store == nil {
return nil, fmt.Errorf("no store found for type %s, which key with name \"%s\" relies on. manual rotation may be required #TODO-DOCS", k.ExtStoreType, k.Name)
}
encryptedArmoredKeyHex, err := store.Get(k.ExtStoreId)
if err != nil {
return nil, err
}
encryptedArmoredKey, err := hex.DecodeString(encryptedArmoredKeyHex)
if err != nil {
return nil, err
}
// Decrypt the key
block, err := aes.NewCipher(encBytes)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
armoredKey, err := gcm.Open(nil, nonce, encryptedArmoredKey, nil)
if err != nil {
return nil, err
}
keyObj, err := crypto.NewKeyFromArmored(string(armoredKey))
if err != nil {
s.log.Errorf("could not get key from armored string: %v", err)
return nil, utils.InternalError
}
loadedKey, err := s.WarmGPGKey(key, string(armoredKey), keyObj, k)
if err != nil {
return nil, err
}
return loadedKey, nil
}