mirror of
https://github.com/QuilibriumNetwork/ceremonyclient.git
synced 2026-02-21 18:37:26 +08:00
* wip: conversion of hotstuff from flow into Q-oriented model * bulk of tests * remaining non-integration tests * add integration test, adjust log interface, small tweaks * further adjustments, restore full pacemaker shape * add component lifecycle management+supervisor * further refinements * resolve timeout hanging * mostly finalized state for consensus * bulk of engine swap out * lifecycle-ify most types * wiring nearly complete, missing needed hooks for proposals * plugged in, vetting message validation paths * global consensus, plugged in and verified * app shard now wired in too * do not decode empty keys.yml (#456) * remove obsolete engine.maxFrames config parameter (#454) * default to Info log level unless debug is enabled (#453) * respect config's "logging" section params, remove obsolete single-file logging (#452) * Trivial code cleanup aiming to reduce Go compiler warnings (#451) * simplify range traversal * simplify channel read for single select case * delete rand.Seed() deprecated in Go 1.20 and no-op as of Go 1.24 * simplify range traversal * simplify channel read for single select case * remove redundant type from array * simplify range traversal * simplify channel read for single select case * RC slate * finalize 2.1.0.5 * Update comments in StrictMonotonicCounter Fix comment formatting and clarify description. --------- Co-authored-by: Black Swan <3999712+blacks1ne@users.noreply.github.com>
616 lines
15 KiB
Go
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")
|
|
}
|