ceremonyclient/node/keys/file.go
2025-11-06 22:40:58 -06:00

616 lines
15 KiB
Go

package keys
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"io/ioutil"
"os"
"path/filepath"
"slices"
"sync"
"github.com/btcsuite/btcd/btcec"
"github.com/cloudflare/circl/sign/ed448"
"github.com/pkg/errors"
"go.uber.org/zap"
"golang.org/x/crypto/sha3"
"gopkg.in/yaml.v2"
"source.quilibrium.com/quilibrium/monorepo/config"
qcrypto "source.quilibrium.com/quilibrium/monorepo/types/crypto"
"source.quilibrium.com/quilibrium/monorepo/types/keys"
)
type FileKeyManager struct {
// needed due to historic artifact of peerkey
keyConfig *config.Config
logger *zap.Logger
key keys.ByteString
store map[string]keys.Key
storeMx sync.Mutex
blsKeyConstructor qcrypto.BlsConstructor
decafConstructor qcrypto.DecafConstructor
}
var UnsupportedKeyTypeErr = errors.New("unsupported key type")
var KeyNotFoundErr = errors.New("key not found")
func NewFileKeyManager(
keyConfig *config.Config,
blsKeyConstructor qcrypto.BlsConstructor,
decafConstructor qcrypto.DecafConstructor,
logger *zap.Logger,
) *FileKeyManager {
if keyConfig.Key.KeyStoreFile == nil {
logger.Panic("key store config missing")
}
key, err := hex.DecodeString(keyConfig.Key.KeyStoreFile.EncryptionKey)
if err != nil {
logger.Panic("could not decode encryption key", zap.Error(err))
}
store := make(map[string]keys.Key)
flag := os.O_RDONLY
if keyConfig.Key.KeyStoreFile.CreateIfMissing {
flag |= os.O_CREATE
}
file, err := os.OpenFile(
keyConfig.Key.KeyStoreFile.Path,
flag,
os.FileMode(0600),
)
if err != nil {
logger.Panic("could not open store", zap.Error(err))
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
logger.Panic("could not get key file info", zap.Error(err))
}
if fileInfo.Size() != 0 {
d := yaml.NewDecoder(file)
if err := d.Decode(store); err != nil {
logger.Panic("could not decode store", zap.Error(err))
}
}
keyManager := &FileKeyManager{
keyConfig: keyConfig,
logger: logger,
key: key,
store: store,
blsKeyConstructor: blsKeyConstructor,
decafConstructor: decafConstructor,
}
_, err = keyManager.GetSigningKey("q-prover-key")
if err != nil {
_, _, err = keyManager.CreateSigningKey(
"q-prover-key",
qcrypto.KeyTypeBLS48581G1,
)
if err != nil {
logger.Panic("could not establish prover key", zap.Error(err))
}
}
return keyManager
}
// ValidateSignature implements KeyManager.
func (f *FileKeyManager) ValidateSignature(
keyType qcrypto.KeyType,
publicKey []byte,
message []byte,
signature []byte,
domain []byte,
) (bool, error) {
switch keyType {
case qcrypto.KeyTypeEd448:
return ed448.Verify(
ed448.PublicKey(publicKey),
slices.Concat(domain, message),
signature,
"",
), nil
case qcrypto.KeyTypeBLS48581G1:
fallthrough
case qcrypto.KeyTypeBLS48581G2:
return f.blsKeyConstructor.VerifySignatureRaw(
publicKey,
signature,
message,
domain,
), nil
case qcrypto.KeyTypeSecp256K1SHA256:
digest := sha256.Sum256(slices.Concat(domain, message))
pubkey, _, err := btcec.RecoverCompact(btcec.S256(), signature, digest[:])
if err != nil {
return false, errors.Wrap(err, "validate signature")
}
return bytes.Equal(publicKey, pubkey.SerializeCompressed()), nil
case qcrypto.KeyTypeSecp256K1SHA3:
digest := sha3.Sum256(slices.Concat(domain, message))
pubkey, _, err := btcec.RecoverCompact(btcec.S256(), signature, digest[:])
if err != nil {
return false, errors.Wrap(err, "validate signature")
}
return bytes.Equal(publicKey, pubkey.SerializeCompressed()), nil
case qcrypto.KeyTypeEd25519:
return ed25519.Verify(
ed25519.PublicKey(publicKey),
slices.Concat(domain, message),
signature,
), nil
}
return false, nil
}
// CreateSigningKey implements KeyManager
func (f *FileKeyManager) CreateSigningKey(
id string,
keyType qcrypto.KeyType,
) (qcrypto.Signer, []byte, error) {
if id == "q-peer-key" {
return nil, nil, errors.Wrap(
errors.New("invalid request"),
"create signing key",
)
}
switch keyType {
case qcrypto.KeyTypeEd448:
ed448Key, err := NewEd448Key()
if err != nil {
return nil, nil, errors.Wrap(err, "create signing key")
}
if err = f.save(
id,
keys.Key{
Id: id,
Type: keyType,
PublicKey: keys.ByteString(ed448Key.Public().([]byte)),
PrivateKey: keys.ByteString(ed448Key.Private()),
},
); err != nil {
return nil, nil, errors.Wrap(err, "create signing key")
}
return ed448Key, nil, nil
case qcrypto.KeyTypeBLS48581G1:
fallthrough
case qcrypto.KeyTypeBLS48581G2:
blskey, popk, err := f.blsKeyConstructor.New()
if err != nil {
return nil, nil, errors.Wrap(err, "create signing key")
}
if err = f.save(
id,
keys.Key{
Id: id,
Type: keyType,
PublicKey: keys.ByteString(blskey.Public().([]byte)),
PrivateKey: keys.ByteString(blskey.Private()),
},
); err != nil {
return nil, nil, errors.Wrap(err, "create signing key")
}
return blskey, popk, nil
// case KeyTypePCAS:
// _, privkey, err := addressing.GenerateKey(rand.Reader)
// if err != nil {
// return nil, errors.Wrap(err, "could not generate key")
// }
// if err = f.save(id, privkey); err != nil {
// return nil, errors.Wrap(err, "could not save")
// }
// return privkey, nil
}
return nil, nil, UnsupportedKeyTypeErr
}
// CreateAgreementKey implements KeyManager
func (f *FileKeyManager) CreateAgreementKey(
id string,
keyType qcrypto.KeyType,
) (qcrypto.Agreement, error) {
switch keyType {
case qcrypto.KeyTypeX448:
x448Key := NewX448Key()
if err := f.save(
id,
keys.Key{
Id: id,
Type: qcrypto.KeyTypeX448,
PublicKey: x448Key.Public(),
PrivateKey: x448Key.Private(),
},
); err != nil {
return nil, errors.Wrap(err, "create agreement key")
}
return x448Key, nil
case qcrypto.KeyTypeDecaf448:
decafKey, err := f.decafConstructor.New()
if err != nil {
return nil, errors.Wrap(err, "create agreement key")
}
if err := f.save(
id,
keys.Key{
Id: id,
Type: qcrypto.KeyTypeDecaf448,
PublicKey: decafKey.Public(),
PrivateKey: decafKey.Private(),
},
); err != nil {
return nil, errors.Wrap(err, "create agreement key")
}
return decafKey, nil
}
return nil, UnsupportedKeyTypeErr
}
// GetAgreementKey implements KeyManager
func (f *FileKeyManager) GetAgreementKey(id string) (qcrypto.Agreement, error) {
key, err := f.read(id)
if err != nil {
return nil, err
}
switch key.Type {
case qcrypto.KeyTypeX448:
x448Key, err := X448KeyFromBytes(key.PrivateKey)
return x448Key, errors.Wrap(err, "get agreement key")
case qcrypto.KeyTypeDecaf448:
decafKey, err := f.decafConstructor.FromBytes(key.PrivateKey, key.PublicKey)
return decafKey, errors.Wrap(err, "get agreement key")
}
return nil, UnsupportedKeyTypeErr
}
// GetRawKey implements KeyManager
func (f *FileKeyManager) GetRawKey(id string) (*keys.Key, error) {
key, err := f.read(id)
return &key, err
}
// GetSigningKey implements KeyManager
func (f *FileKeyManager) GetSigningKey(id string) (qcrypto.Signer, error) {
if id == "q-peer-key" {
key, err := hex.DecodeString(f.keyConfig.P2P.PeerPrivKey)
if err != nil {
return nil, err
}
// special case, peer key is stored as seed value
privateKey := ed448.NewKeyFromSeed(key[:57])
ed448Key, err := Ed448KeyFromBytes(privateKey, key[57:])
return ed448Key, errors.Wrap(err, "get signing key")
}
key, err := f.read(id)
if err != nil {
return nil, err
}
switch key.Type {
case qcrypto.KeyTypeEd448:
ed448Key, err := Ed448KeyFromBytes(key.PrivateKey, key.PublicKey)
return ed448Key, errors.Wrap(err, "get signing key")
case qcrypto.KeyTypeBLS48581G1:
fallthrough
case qcrypto.KeyTypeBLS48581G2:
blskey, err := f.blsKeyConstructor.FromBytes(key.PrivateKey, key.PublicKey)
return blskey, errors.Wrap(err, "get signing key")
// case KeyTypePCAS:
// privkey := (addressing.PCAS)(key.PrivateKey)
// return privkey, err
}
return nil, UnsupportedKeyTypeErr
}
// PutRawKey implements KeyManager
func (f *FileKeyManager) PutRawKey(key *keys.Key) error {
return f.save(key.Id, *key)
}
// DeleteKey implements KeyManager
func (f *FileKeyManager) DeleteKey(id string) error {
return f.delete(id)
}
// GetKey implements KeyManager
func (f *FileKeyManager) GetKey(id string) (key *keys.Key, err error) {
storeKey, err := f.read(id)
if err != nil {
return nil, err
}
return &storeKey, nil
}
// ListKeys implements KeyManager
func (f *FileKeyManager) ListKeys() ([]*keys.Key, error) {
keys := []*keys.Key{}
for k := range f.store {
if len(k) == 0 {
continue
}
storeKey, err := f.read(k)
if err != nil {
return nil, err
}
keys = append(keys, &storeKey)
}
return keys, nil
}
func (f *FileKeyManager) Aggregate(
publicKeys [][]byte,
signatures [][]byte,
) (
qcrypto.BlsAggregateOutput,
error,
) {
aggregate, err := f.blsKeyConstructor.Aggregate(publicKeys, signatures)
if err != nil {
return nil, errors.Wrap(err, "aggregate")
}
return aggregate, nil
}
var _ keys.KeyManager = (*FileKeyManager)(nil)
func (f *FileKeyManager) save(id string, key keys.Key) error {
encKey := []byte{}
var err error
if encKey, err = f.encrypt(key.PrivateKey); err != nil {
return errors.Wrap(err, "could not encrypt")
}
f.storeMx.Lock()
defer f.storeMx.Unlock()
// Create a copy of the current store with the new key
updatedStore := make(map[string]keys.Key)
for k, v := range f.store {
updatedStore[k] = v
}
updatedStore[id] = keys.Key{
Id: key.Id,
Type: key.Type,
PublicKey: key.PublicKey,
PrivateKey: encKey,
}
// Create a temporary file in the same directory as the original
originalPath := f.keyConfig.Key.KeyStoreFile.Path
dir := filepath.Dir(originalPath)
tempFile, err := ioutil.TempFile(dir, "keystore-*.tmp")
if err != nil {
return errors.Wrap(err, "could not create temporary file")
}
tempPath := tempFile.Name()
// Ensure temporary file is removed in case of failure
defer func() {
tempFile.Close()
// Only remove the temp file if it still exists (wasn't renamed)
if _, err := os.Stat(tempPath); err == nil {
os.Remove(tempPath)
}
}()
// Set proper permissions on the temporary file
if err := os.Chmod(tempPath, 0600); err != nil {
return errors.Wrap(err, "could not set file permissions")
}
// Write the updated store to the temporary file
encoder := yaml.NewEncoder(tempFile)
if err := encoder.Encode(updatedStore); err != nil {
return errors.Wrap(err, "could not encode to temporary file")
}
// Ensure data is written to disk
if err := tempFile.Sync(); err != nil {
return errors.Wrap(err, "could not sync temporary file")
}
// Close the file before renaming
if err := tempFile.Close(); err != nil {
return errors.Wrap(err, "could not close temporary file")
}
// Atomically replace the original file with the new one
if err := os.Rename(tempPath, originalPath); err != nil {
return errors.Wrap(err, "could not replace key store file")
}
// Update the in-memory store only after successful file update
f.store = updatedStore
return nil
}
func (f *FileKeyManager) read(id string) (keys.Key, error) {
f.storeMx.Lock()
defer f.storeMx.Unlock()
flag := os.O_RDONLY
if f.keyConfig.Key.KeyStoreFile.CreateIfMissing {
flag |= os.O_CREATE
}
file, err := os.OpenFile(
f.keyConfig.Key.KeyStoreFile.Path,
flag,
os.FileMode(0600),
)
if err != nil {
return keys.Key{}, errors.Wrap(err, "could not open store")
}
defer file.Close()
d := yaml.NewDecoder(file)
if err = d.Decode(f.store); err != nil {
return keys.Key{}, errors.Wrap(err, "could not decode")
}
if _, ok := f.store[id]; !ok {
return keys.Key{}, KeyNotFoundErr
}
data, err := f.decrypt(f.store[id].PrivateKey)
if err != nil {
return keys.Key{}, errors.Wrap(err, "could not decrypt")
}
key := keys.Key{
Id: f.store[id].Id,
Type: f.store[id].Type,
PublicKey: f.store[id].PublicKey,
PrivateKey: data,
}
return key, nil
}
func (f *FileKeyManager) delete(id string) error {
f.storeMx.Lock()
defer f.storeMx.Unlock()
// Check if the key exists in the store
if _, exists := f.store[id]; !exists {
return KeyNotFoundErr
}
// Create a copy of the store without the key to delete
updatedStore := make(map[string]keys.Key)
for k, v := range f.store {
if k != id {
updatedStore[k] = v
}
}
// Create a temporary file in the same directory as the original
originalPath := f.keyConfig.Key.KeyStoreFile.Path
dir := filepath.Dir(originalPath)
tempFile, err := ioutil.TempFile(dir, "keystore-*.tmp")
if err != nil {
return errors.Wrap(err, "could not create temporary file")
}
tempPath := tempFile.Name()
// Ensure temporary file is removed in case of failure
defer func() {
tempFile.Close()
// Only remove the temp file if it still exists (wasn't renamed)
if _, err := os.Stat(tempPath); err == nil {
os.Remove(tempPath)
}
}()
// Set proper permissions on the temporary file
if err := os.Chmod(tempPath, 0600); err != nil {
return errors.Wrap(err, "could not set file permissions")
}
// Write the updated store to the temporary file
encoder := yaml.NewEncoder(tempFile)
if err := encoder.Encode(updatedStore); err != nil {
return errors.Wrap(err, "could not encode to temporary file")
}
// Ensure data is written to disk
if err := tempFile.Sync(); err != nil {
return errors.Wrap(err, "could not sync temporary file")
}
// Close the file before renaming
if err := tempFile.Close(); err != nil {
return errors.Wrap(err, "could not close temporary file")
}
// Atomically replace the original file with the new one
if err := os.Rename(tempPath, originalPath); err != nil {
return errors.Wrap(err, "could not replace key store file")
}
// Update the in-memory store only after successful file update
f.store = updatedStore
return nil
}
func (f *FileKeyManager) encrypt(data []byte) ([]byte, error) {
iv := [12]byte{}
rand.Read(iv[:])
aesCipher, err := aes.NewCipher(f.key)
if err != nil {
return nil, errors.Wrap(err, "could not construct cipher")
}
gcm, err := cipher.NewGCM(aesCipher)
if err != nil {
return nil, errors.Wrap(err, "could not construct block")
}
ciphertext := []byte{}
ciphertext = gcm.Seal(nil, iv[:], data, nil)
ciphertext = append(append([]byte{}, iv[:]...), ciphertext...)
return ciphertext, nil
}
func (f *FileKeyManager) decrypt(data []byte) ([]byte, error) {
iv := data[:12]
aesCipher, err := aes.NewCipher(f.key)
if err != nil {
return nil, errors.Wrap(err, "could not construct cipher")
}
gcm, err := cipher.NewGCM(aesCipher)
if err != nil {
return nil, errors.Wrap(err, "could not construct block")
}
ciphertext := data[12:]
plaintext, err := gcm.Open(nil, iv[:], ciphertext, nil)
return plaintext, errors.Wrap(err, "could not decrypt ciphertext")
}