diff --git a/cmd/signatory/main.go b/cmd/signatory/main.go index e956b899..3f8fa358 100644 --- a/cmd/signatory/main.go +++ b/cmd/signatory/main.go @@ -133,20 +133,35 @@ func main() { log.Fatal(err) } if len(pubKeys) == 0 { - log.Error("No keys discovered in Key Valut(s), exiting..") + log.Error("No keys discovered in Key Vault(s), exiting..") os.Exit(1) } log.Info("Keys discovered in Key Vault:") var allowedKeyCount int for _, key := range pubKeys { + l := log.WithField(signatory.LogPKH, key) + + vault, _, err := s.GetCachedPublicKey(ctx, key) + if err != nil { + l.Error(err) + continue + } + + logfields := log.Fields{signatory.LogVault: vault.Name()} + if n, ok := vault.(signatory.VaultNamer); ok { + logfields[signatory.LogVaultName] = n.VaultName() + } + l = l.WithFields(logfields) + if s.IsAllowed(key) { allowedKeyCount++ - log.Infof("%s (Configured, ready for use)", key) + l.Info("Key configured, ready for use") } else { - log.Infof("%s (Found in vault, not configured for use in %s)", key, configFile) + l.Infof("Key found in vault but not configured in %s", configFile) } } + if allowedKeyCount == 0 { log.Errorf("No keys configured for signing. To allow a key add it to the tezos.keys list in %s ", configFile) os.Exit(1) diff --git a/pkg/crypto/crypto.go b/pkg/crypto/crypto.go index b6ac4bd6..6a867da1 100644 --- a/pkg/crypto/crypto.go +++ b/pkg/crypto/crypto.go @@ -36,7 +36,7 @@ func GetCurve(name string) elliptic.Curve { return nil } -// ECCoordinateFromPrivateKey given an elliptic curve name it will produce X and Y coordiante from D +// ECCoordinateFromPrivateKey given an elliptic curve name it will produce X and Y coordinate from D func ECCoordinateFromPrivateKey(d []byte, curveName string) (xBytes, yBytes []byte) { curve := GetCurve(curveName) diff --git a/pkg/metrics/vaultmetrics.go b/pkg/metrics/vaultmetrics.go index 551a2589..a6a0db44 100644 --- a/pkg/metrics/vaultmetrics.go +++ b/pkg/metrics/vaultmetrics.go @@ -42,7 +42,7 @@ var ( ) // Interceptor function collects sing operation metrics -func Interceptor(opt *signatory.SingInterceptorOptions, sing func() error) error { +func Interceptor(opt *signatory.SignInterceptorOptions, sing func() error) error { timer := prometheus.NewTimer(prometheus.ObserverFunc(func(seconds float64) { vaultSigningHist.WithLabelValues(opt.Vault).Observe(seconds * 1000) })) diff --git a/pkg/signatory/import.go b/pkg/signatory/import.go index 0f2a9c8f..00bd6bbf 100644 --- a/pkg/signatory/import.go +++ b/pkg/signatory/import.go @@ -55,11 +55,11 @@ func (s *Signatory) Import(pubkey string, secretKey string, importer Importer) ( } logfields := log.Fields{ - logPKH: hash, - logVault: importer.Name(), + LogPKH: hash, + LogVault: importer.Name(), } if n, ok := importer.(VaultNamer); ok { - logfields[logVaultName] = n.VaultName() + logfields[LogVaultName] = n.VaultName() } l := s.logger.WithFields(logfields) @@ -70,7 +70,7 @@ func (s *Signatory) Import(pubkey string, secretKey string, importer Importer) ( return nil, err } - l.WithField(logKeyID, keyID).Info("Successfully imported") + l.WithField(LogKeyID, keyID).Info("Successfully imported") importedKey := &ImportedKey{ KeyID: keyID, diff --git a/pkg/signatory/signatory.go b/pkg/signatory/signatory.go index 2b989a60..32d3e65c 100644 --- a/pkg/signatory/signatory.go +++ b/pkg/signatory/signatory.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "math/big" + "sync" log "github.com/sirupsen/logrus" @@ -21,19 +22,19 @@ var ( ) const ( - logPKH = "pkh" - logVault = "vault" - logVaultName = "vault_name" - logOp = "op" - logKind = "kind" - logKeyID = "key_id" + LogPKH = "pkh" + LogVault = "vault" + LogVaultName = "vault_name" + LogOp = "op" + LogKind = "kind" + LogKeyID = "key_id" ) -// SingInterceptor is an observer function for signing request -type SingInterceptor func(opt *SingInterceptorOptions, sing func() error) error +// SignInterceptor is an observer function for signing request +type SignInterceptor func(opt *SignInterceptorOptions, sing func() error) error -// SingInterceptorOptions contains SingInterceptor arguments to avoid confusion -type SingInterceptorOptions struct { +// SignInterceptorOptions contains SignInterceptor arguments to avoid confusion +type SignInterceptorOptions struct { Address string Vault string Op string @@ -67,40 +68,34 @@ type Watermark interface { IsSafeToSign(msgID string, level *big.Int) bool } -type vaultKeyIDPair struct { +type keyVaultPair struct { vault Vault key StoredKey } -// HashVaultStore store the id and the vault of each key -type HashVaultStore = map[string]vaultKeyIDPair - -func (s *Signatory) addKeyMap(hash string, key StoredKey, vault Vault) { - s.hashVaultStore[hash] = vaultKeyIDPair{key: key, vault: vault} -} - // Signatory is a struct coordinate signatory action and select vault according to the key being used type Signatory struct { vaults []Vault config config.TezosConfig - interceptor SingInterceptor + interceptor SignInterceptor watermark Watermark - hashVaultStore HashVaultStore logger log.FieldLogger + hashVaultCache map[string]*keyVaultPair + mtx sync.Mutex } // NewSignatory return a new signatory struct func NewSignatory( vaults []Vault, config config.TezosConfig, - interceptor SingInterceptor, + interceptor SignInterceptor, watermark Watermark, logger log.FieldLogger, ) (s *Signatory) { s = &Signatory{ vaults: vaults, config: config, - hashVaultStore: make(HashVaultStore), + hashVaultCache: make(map[string]*keyVaultPair), interceptor: interceptor, watermark: watermark, logger: logger, @@ -113,7 +108,7 @@ func NewSignatory( return } -func IsSupportedCurve(curve string) bool { +func isSupportedCurve(curve string) bool { return curve == crypto.CurveP256 || curve == crypto.CurveP256K || curve == crypto.CurveED25519 } @@ -137,20 +132,6 @@ func (s *Signatory) FetchPolicyOrDefault(keyHash string) *config.TezosPolicy { } } -func (s *Signatory) getVaultFromKeyHash(keyHash string) Vault { - if pair, ok := s.hashVaultStore[keyHash]; ok { - return pair.vault - } - return nil -} - -func (s *Signatory) getKeyFromKeyHash(keyHash string) StoredKey { - if pair, ok := s.hashVaultStore[keyHash]; ok { - return pair.key - } - return nil -} - func (s *Signatory) validateMessage(msg *tezos.Message, policy *config.TezosPolicy) error { err := msg.Validate() @@ -167,9 +148,51 @@ func (s *Signatory) validateMessage(msg *tezos.Message, policy *config.TezosPoli return nil } +func (s *Signatory) cacheLookup(ctx context.Context, keyHash string, update bool) (*keyVaultPair, error) { + s.mtx.Lock() + if v, ok := s.hashVaultCache[keyHash]; ok { + s.mtx.Unlock() + return v, nil + } + s.mtx.Unlock() + + if !update { + return nil, nil + } + + // Rescan + cache, err := s.listPublicKeyHash(ctx) + if err != nil { + return nil, err + } + + s.mtx.Lock() + defer s.mtx.Unlock() + + s.hashVaultCache = cache + if v, ok := s.hashVaultCache[keyHash]; ok { + return v, nil + } + return nil, nil +} + +// GetCachedPublicKey returns public key object and vault for the given hash from the cache. The latter is not updated. +func (s *Signatory) GetCachedPublicKey(ctx context.Context, keyHash string) (Vault, StoredKey, error) { + cached, err := s.cacheLookup(ctx, keyHash, false) + if err != nil { + return nil, nil, err + } + + if cached == nil { + return nil, nil, ErrVaultNotFound + } + + return cached.vault, cached.key, nil +} + // Sign ask the vault to sign a message with the private key associated to keyHash func (s *Signatory) Sign(ctx context.Context, keyHash string, message []byte) (string, error) { - l := s.logger.WithField(logPKH, keyHash) + l := s.logger.WithField(LogPKH, keyHash) if !s.IsAllowed(keyHash) { err := fmt.Errorf("%s is not listed in config", keyHash) @@ -186,21 +209,26 @@ func (s *Signatory) Sign(ctx context.Context, keyHash string, message []byte) (s } l = l.WithFields(log.Fields{ - logOp: msg.Type(), - logKind: msg.Kind(), + LogOp: msg.Type(), + LogKind: msg.Kind(), }) - vault := s.getVaultFromKeyHash(keyHash) - if vault == nil { + cached, err := s.cacheLookup(ctx, keyHash, true) + if err != nil { + l.Error(err) + return "", err + } + + if cached == nil { l.Error("Vault not found") return "", ErrVaultNotFound } logfields := log.Fields{ - logVault: vault.Name(), + LogVault: cached.vault.Name(), } - if n, ok := vault.(VaultNamer); ok { - logfields[logVaultName] = n.VaultName() + if n, ok := cached.vault.(VaultNamer); ok { + logfields[LogVaultName] = n.VaultName() } l = l.WithFields(logfields) @@ -220,26 +248,22 @@ func (s *Signatory) Sign(ctx context.Context, keyHash string, message []byte) (s } // Not nil if vault found - storedKey := s.getKeyFromKeyHash(keyHash) digest := tezos.DigestFunc(message) - var ( - sig []byte - err error - ) + var sig []byte if s.interceptor != nil { - err = s.interceptor(&SingInterceptorOptions{ + err = s.interceptor(&SignInterceptorOptions{ Address: keyHash, - Vault: vault.Name(), + Vault: cached.vault.Name(), Op: msg.Type(), Kind: msg.Kind(), }, func() (err error) { - sig, err = vault.Sign(ctx, digest[:], storedKey) + sig, err = cached.vault.Sign(ctx, digest[:], cached.key) return err }) } else { - sig, err = vault.Sign(ctx, digest[:], storedKey) + sig, err = cached.vault.Sign(ctx, digest[:], cached.key) } if err != nil { @@ -257,49 +281,68 @@ func (s *Signatory) Sign(ctx context.Context, keyHash string, message []byte) (s return encodedSig, nil } -// ListPublicKeyHash retrieve the list of all public key hash supported by the current configuration -func (s *Signatory) ListPublicKeyHash(ctx context.Context) ([]string, error) { - results := []string{} +func (s *Signatory) listPublicKeyHash(ctx context.Context) (map[string]*keyVaultPair, error) { + cache := make(map[string]*keyVaultPair) + for _, vault := range s.vaults { pubKeys, err := vault.ListPublicKeys(ctx) - if err != nil { return nil, err } for _, key := range pubKeys { - if IsSupportedCurve(key.Curve()) { + if isSupportedCurve(key.Curve()) { encoded := tezos.EncodePubKeyHash(key.PublicKey(), key.Curve()) - results = append(results, encoded) - s.addKeyMap(encoded, key, vault) - + cache[encoded] = &keyVaultPair{key: key, vault: vault} } } } + return cache, nil +} + +// ListPublicKeyHash retrieve the list of all public key hash supported by the current configuration +func (s *Signatory) ListPublicKeyHash(ctx context.Context) ([]string, error) { + cache, err := s.listPublicKeyHash(ctx) + if err != nil { + return nil, err + } + + // Replace the cache + s.mtx.Lock() + s.hashVaultCache = cache + s.mtx.Unlock() + + results := make([]string, 0, len(cache)) + for keyHash := range cache { + results = append(results, keyHash) + } + return results, nil } // GetPublicKey retrieve the public key from a vault func (s *Signatory) GetPublicKey(ctx context.Context, keyHash string) (string, error) { - vault := s.getVaultFromKeyHash(keyHash) - if vault == nil { + cached, err := s.cacheLookup(ctx, keyHash, true) + if err != nil { + return "", err + } + + if cached == nil { return "", ErrVaultNotFound } logfields := log.Fields{ - logPKH: keyHash, - logVault: vault.Name(), + LogPKH: keyHash, + LogVault: cached.vault.Name(), } - if n, ok := vault.(VaultNamer); ok { - logfields[logVaultName] = n.VaultName() + if n, ok := cached.vault.(VaultNamer); ok { + logfields[LogVaultName] = n.VaultName() } l := s.logger.WithFields(logfields) - key := s.getKeyFromKeyHash(keyHash) - l.Debugf("Fetching public key for: %s", keyHash) - pubKey, err := vault.GetPublicKey(ctx, key.ID()) + pubKey, err := cached.vault.GetPublicKey(ctx, cached.key.ID()) if err != nil { return "", err } diff --git a/pkg/signatory/signatory_test.go b/pkg/signatory/signatory_test.go index b3b14e71..eb5a73a5 100644 --- a/pkg/signatory/signatory_test.go +++ b/pkg/signatory/signatory_test.go @@ -38,7 +38,7 @@ func TestGetPublicKeyNoVault(t *testing.T) { nil, ) - _, err := s.GetPublicKey(context.TODO(), "Unkown address") + _, err := s.GetPublicKey(context.TODO(), "Unknown address") if err != signatory.ErrVaultNotFound { fmt.Printf("Unexpected error was thrown: %s\n", err.Error()) diff --git a/pkg/tezos/message.go b/pkg/tezos/message.go index 5056b7c6..5b18c3f8 100644 --- a/pkg/tezos/message.go +++ b/pkg/tezos/message.go @@ -31,7 +31,7 @@ const ( OpEndorsement = "endorsement" // OpGeneric config string for generic operation OpGeneric = "generic" - // OpUnknown config string for unkown operation + // OpUnknown config string for unknown operation OpUnknown = "unknown" ) diff --git a/pkg/tezos/message_test.go b/pkg/tezos/message_test.go index 7b4c9c6c..ef8714ff 100644 --- a/pkg/tezos/message_test.go +++ b/pkg/tezos/message_test.go @@ -96,7 +96,7 @@ func TestFilterMessage(t *testing.T) { Case{Name: "Generic operation not configured", Message: []byte{0x03, 0x02}, Error: &tezos.FilterError{}, Config: genTezosConfig([]string{tezos.OpGeneric}, nil)}, Case{Name: "Generic operation not long enough", Message: []byte{0x03, 0x02}, Error: &tezos.FilterError{}, Config: genTezosConfig([]string{tezos.OpGeneric}, []string{tezos.OpGenBallot})}, - Case{Name: "Generic operation unkown not long enough", Message: []byte{0x03, 0x02}, Error: nil, Config: genTezosConfig([]string{tezos.OpGeneric}, []string{tezos.OpGenUnknown})}, + Case{Name: "Generic operation unknown not long enough", Message: []byte{0x03, 0x02}, Error: nil, Config: genTezosConfig([]string{tezos.OpGeneric}, []string{tezos.OpGenUnknown})}, Case{Name: "Generic operation ballot", Message: createGeneric(0x06), Error: nil, Config: genTezosConfig([]string{tezos.OpGeneric}, []string{tezos.OpGenBallot})}, Case{Name: "Generic operation transaction", Message: createGeneric(0x08), Error: nil, Config: genTezosConfig([]string{tezos.OpGeneric}, []string{tezos.OpGenTransaction})}, Case{Name: "Generic operation proposal", Message: createGeneric(0x05), Error: nil, Config: genTezosConfig([]string{tezos.OpGeneric}, []string{tezos.OpGenProposal})},