refactor: keystore as a data type

This commit is contained in:
Richard Ramos 2023-08-23 16:50:25 -04:00 committed by richΛrd
parent 229fb7a970
commit 0854edaf3d
6 changed files with 248 additions and 248 deletions

View File

@ -28,13 +28,13 @@ var Command = cli.Command{
Name: "generate-rln-credentials",
Usage: "Generate credentials for usage with RLN",
Action: func(cCtx *cli.Context) error {
err := verifyFlags()
if err != nil {
if options.ETHPrivateKey == nil {
err := errors.New("a private key must be specified")
logger.Error("validating option flags", zap.Error(err))
return cli.Exit(err, 1)
}
err = execute(context.Background())
err := execute(context.Background())
if err != nil {
logger.Error("registering RLN credentials", zap.Error(err))
return cli.Exit(err, 1)
@ -45,24 +45,6 @@ var Command = cli.Command{
Flags: flags,
}
func verifyFlags() error {
if options.CredentialsPath == "" {
logger.Warn("keystore: no credentials path set, using default path", zap.String("path", keystore.RLN_CREDENTIALS_FILENAME))
options.CredentialsPath = keystore.RLN_CREDENTIALS_FILENAME
}
if options.CredentialsPassword == "" {
logger.Warn("keystore: no credentials password set, using default password", zap.String("password", keystore.RLN_CREDENTIALS_PASSWORD))
options.CredentialsPassword = keystore.RLN_CREDENTIALS_PASSWORD
}
if options.ETHPrivateKey == nil {
return errors.New("a private key must be specified")
}
return nil
}
func execute(ctx context.Context) error {
ethClient, err := ethclient.Dial(options.ETHClientAddress)
if err != nil {
@ -122,15 +104,20 @@ func execute(ctx context.Context) error {
}
func persistCredentials(identityCredential *rln.IdentityCredential, membershipIndex rln.MembershipIndex, chainID *big.Int) error {
appKeystore, err := keystore.New(options.CredentialsPath, dynamic.RLNAppInfo, logger)
if err != nil {
return err
}
membershipGroup := keystore.MembershipGroup{
TreeIndex: membershipIndex,
MembershipContract: keystore.MembershipContract{
ChainId: fmt.Sprintf("0x%X", chainID.Int64()),
ChainID: fmt.Sprintf("0x%X", chainID.Int64()),
Address: options.MembershipContractAddress.String(),
},
}
membershipGroupIndex, err := keystore.AddMembershipCredentials(options.CredentialsPath, identityCredential, membershipGroup, options.CredentialsPassword, dynamic.RLNAppInfo, keystore.DefaultSeparator)
membershipGroupIndex, err := appKeystore.AddMembershipCredentials(identityCredential, membershipGroup, options.CredentialsPassword)
if err != nil {
return fmt.Errorf("failed to persist credentials: %w", err)
}

View File

@ -13,12 +13,12 @@ var flags = []cli.Flag{
&cli.PathFlag{
Name: "cred-path",
Usage: "RLN relay membership credentials file",
Value: keystore.RLN_CREDENTIALS_FILENAME,
Value: keystore.DefaultCredentialsFilename,
Destination: &options.CredentialsPath,
},
&cli.StringFlag{
Name: "cred-password",
Value: keystore.RLN_CREDENTIALS_PASSWORD,
Value: keystore.DefaultCredentialsPassword,
Usage: "Password for encrypting RLN credentials",
Destination: &options.CredentialsPassword,
},

View File

@ -12,6 +12,7 @@ import (
"github.com/waku-org/go-waku/waku/v2/protocol/rln"
"github.com/waku-org/go-waku/waku/v2/protocol/rln/group_manager/dynamic"
"github.com/waku-org/go-waku/waku/v2/protocol/rln/group_manager/static"
"github.com/waku-org/go-waku/waku/v2/protocol/rln/keystore"
r "github.com/waku-org/go-zerokit-rln/rln"
)
@ -44,14 +45,18 @@ func (w *WakuNode) setupRLNRelay() error {
} else {
w.log.Info("setting up waku-rln-relay in on-chain mode")
appKeystore, err := keystore.New(w.opts.keystorePath, dynamic.RLNAppInfo, w.log)
if err != nil {
return err
}
groupManager, err = dynamic.NewDynamicGroupManager(
w.opts.rlnETHClientAddress,
w.opts.rlnMembershipContractAddress,
w.opts.rlnRelayMemIndex,
w.opts.keystorePath,
appKeystore,
w.opts.keystorePassword,
w.opts.keystoreIndex,
true,
w.opts.prometheusReg,
w.log,
)

View File

@ -51,8 +51,7 @@ type DynamicGroupManager struct {
chainId *big.Int
rlnContract *contracts.RLN
saveKeystore bool
keystorePath string
appKeystore *keystore.AppKeystore
keystorePassword string
keystoreIndex uint
@ -122,35 +121,21 @@ func NewDynamicGroupManager(
ethClientAddr string,
memContractAddr common.Address,
membershipGroupIndex uint,
keystorePath string,
appKeystore *keystore.AppKeystore,
keystorePassword string,
keystoreIndex uint,
saveKeystore bool,
reg prometheus.Registerer,
log *zap.Logger,
) (*DynamicGroupManager, error) {
log = log.Named("rln-dynamic")
path := keystorePath
if path == "" {
log.Warn("keystore: no credentials path set, using default path", zap.String("path", keystore.RLN_CREDENTIALS_FILENAME))
path = keystore.RLN_CREDENTIALS_FILENAME
}
password := keystorePassword
if password == "" {
log.Warn("keystore: no credentials password set, using default password", zap.String("password", keystore.RLN_CREDENTIALS_PASSWORD))
password = keystore.RLN_CREDENTIALS_PASSWORD
}
return &DynamicGroupManager{
membershipGroupIndex: membershipGroupIndex,
membershipContractAddress: memContractAddr,
ethClientAddress: ethClientAddr,
eventHandler: handler,
saveKeystore: saveKeystore,
keystorePath: path,
keystorePassword: password,
appKeystore: appKeystore,
keystorePassword: keystorePassword,
keystoreIndex: keystoreIndex,
log: log,
metrics: newMetrics(reg),
@ -196,39 +181,9 @@ func (gm *DynamicGroupManager) Start(ctx context.Context, rlnInstance *rln.RLN,
return err
}
if gm.identityCredential == nil && gm.keystorePassword != "" && gm.keystorePath != "" {
start := time.Now()
credentials, err := keystore.GetMembershipCredentials(gm.log,
gm.keystorePath,
gm.keystorePassword,
RLNAppInfo,
nil,
[]keystore.MembershipContract{{
ChainId: fmt.Sprintf("0x%X", gm.chainId),
Address: gm.membershipContractAddress.Hex(),
}})
if err != nil {
return err
}
gm.metrics.RecordMembershipCredentialsImportDuration(time.Since(start))
if len(credentials) != 0 {
if int(gm.keystoreIndex) <= len(credentials)-1 {
credential := credentials[gm.keystoreIndex]
gm.identityCredential = credential.IdentityCredential
if int(gm.membershipGroupIndex) <= len(credential.MembershipGroups)-1 {
gm.membershipIndex = &credential.MembershipGroups[gm.membershipGroupIndex].TreeIndex
} else {
return errors.New("invalid membership group index")
}
} else {
return errors.New("invalid keystore index")
}
}
}
if gm.identityCredential == nil || gm.membershipIndex == nil {
return errors.New("no credentials available")
err = gm.loadCredential()
if err != nil {
return err
}
if err = gm.HandleGroupUpdates(ctx, gm.eventHandler); err != nil {
@ -238,6 +193,39 @@ func (gm *DynamicGroupManager) Start(ctx context.Context, rlnInstance *rln.RLN,
return nil
}
func (gm *DynamicGroupManager) loadCredential() error {
start := time.Now()
credentials, err := gm.appKeystore.GetMembershipCredentials(
gm.keystorePassword,
nil,
[]keystore.MembershipContract{{
ChainID: fmt.Sprintf("0x%X", gm.chainId),
Address: gm.membershipContractAddress.Hex(),
}})
if err != nil {
return err
}
gm.metrics.RecordMembershipCredentialsImportDuration(time.Since(start))
if len(credentials) == 0 {
return errors.New("no credentials available")
}
if int(gm.keystoreIndex) > len(credentials)-1 {
return errors.New("invalid keystore index")
}
if int(gm.membershipGroupIndex) > len(credentials[gm.keystoreIndex].MembershipGroups)-1 {
return errors.New("invalid membership group index")
}
gm.identityCredential = credentials[gm.keystoreIndex].IdentityCredential
gm.membershipIndex = &credentials[gm.keystoreIndex].MembershipGroups[gm.membershipGroupIndex].TreeIndex
return nil
}
func (gm *DynamicGroupManager) InsertMembers(toInsert *om.OrderedMap) error {
for pair := toInsert.Oldest(); pair != nil; pair = pair.Next() {
events := pair.Value.([]*contracts.RLNMemberRegistered) // TODO: should these be sortered by index? we assume all members arrive in order

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"os"
"sort"
@ -14,130 +13,49 @@ import (
"go.uber.org/zap"
)
const RLN_CREDENTIALS_FILENAME = "rlnKeystore.json"
const RLN_CREDENTIALS_PASSWORD = "password"
// DefaultCredentialsFilename is the default filename for the rln credentials keystore
const DefaultCredentialsFilename = "rlnKeystore.json"
type MembershipContract struct {
ChainId string `json:"chainId"`
Address string `json:"address"`
}
// DefaultCredentialsPassword contains the default password used when no password is specified
const DefaultCredentialsPassword = "password"
type MembershipGroup struct {
MembershipContract MembershipContract `json:"membershipContract"`
TreeIndex rln.MembershipIndex `json:"treeIndex"`
}
// New creates a new instance of a rln credentials keystore
func New(keystorePath string, appInfo AppInfo, logger *zap.Logger) (*AppKeystore, error) {
logger = logger.Named("rln-keystore")
type MembershipCredentials struct {
IdentityCredential *rln.IdentityCredential `json:"identityCredential"`
MembershipGroups []MembershipGroup `json:"membershipGroups"`
}
type AppInfo struct {
Application string `json:"application"`
AppIdentifier string `json:"appIdentifier"`
Version string `json:"version"`
}
type AppKeystore struct {
Application string `json:"application"`
AppIdentifier string `json:"appIdentifier"`
Credentials []AppKeystoreCredential `json:"credentials"`
Version string `json:"version"`
}
type AppKeystoreCredential struct {
Crypto keystore.CryptoJSON `json:"crypto"`
}
const DefaultSeparator = "\n"
func (m MembershipCredentials) Equals(other MembershipCredentials) bool {
if !rln.IdentityCredentialEquals(*m.IdentityCredential, *other.IdentityCredential) {
return false
}
for _, x := range m.MembershipGroups {
found := false
for _, y := range other.MembershipGroups {
if x.Equals(y) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
func (m MembershipGroup) Equals(other MembershipGroup) bool {
return m.MembershipContract.Equals(other.MembershipContract) && m.TreeIndex == other.TreeIndex
}
func (m MembershipContract) Equals(other MembershipContract) bool {
return m.Address == other.Address && m.ChainId == other.ChainId
}
func CreateAppKeystore(path string, appInfo AppInfo, separator string) error {
if separator == "" {
separator = DefaultSeparator
}
keystore := AppKeystore{
Application: appInfo.Application,
AppIdentifier: appInfo.AppIdentifier,
Version: appInfo.Version,
}
b, err := json.Marshal(keystore)
if err != nil {
return err
}
b = append(b, []byte(separator)...)
buffer := new(bytes.Buffer)
err = json.Compact(buffer, b)
if err != nil {
return err
}
return ioutil.WriteFile(path, buffer.Bytes(), 0600)
}
func LoadAppKeystore(path string, appInfo AppInfo, separator string) (AppKeystore, error) {
if separator == "" {
separator = DefaultSeparator
path := keystorePath
if path == "" {
logger.Warn("keystore: no credentials path set, using default path", zap.String("path", DefaultCredentialsFilename))
path = DefaultCredentialsFilename
}
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
// If no keystore exists at path we create a new empty one with passed keystore parameters
err = CreateAppKeystore(path, appInfo, separator)
err = createAppKeystore(path, appInfo, defaultSeparator)
if err != nil {
return AppKeystore{}, err
return nil, err
}
} else {
return AppKeystore{}, err
return nil, err
}
}
src, err := os.ReadFile(path)
if err != nil {
return AppKeystore{}, err
return nil, err
}
for _, keystoreBytes := range bytes.Split(src, []byte(separator)) {
for _, keystoreBytes := range bytes.Split(src, []byte(defaultSeparator)) {
if len(keystoreBytes) == 0 {
continue
}
keystore := AppKeystore{}
err := json.Unmarshal(keystoreBytes, &keystore)
keystore := new(AppKeystore)
keystore.logger = logger
keystore.path = path
err := json.Unmarshal(keystoreBytes, keystore)
if err != nil {
continue
}
@ -147,53 +65,15 @@ func LoadAppKeystore(path string, appInfo AppInfo, separator string) (AppKeystor
}
}
return AppKeystore{}, errors.New("no keystore found")
return nil, errors.New("no keystore found")
}
func filterCredential(credential MembershipCredentials, filterIdentityCredentials []MembershipCredentials, filterMembershipContracts []MembershipContract) *MembershipCredentials {
if len(filterIdentityCredentials) != 0 {
found := false
for _, filterCreds := range filterIdentityCredentials {
if filterCreds.Equals(credential) {
found = true
}
}
if !found {
return nil
}
}
if len(filterMembershipContracts) != 0 {
var membershipGroupsIntersection []MembershipGroup
for _, filterContract := range filterMembershipContracts {
for _, credentialGroups := range credential.MembershipGroups {
if filterContract.Equals(credentialGroups.MembershipContract) {
membershipGroupsIntersection = append(membershipGroupsIntersection, credentialGroups)
}
}
}
if len(membershipGroupsIntersection) != 0 {
// If we have a match on some groups, we return the credential with filtered groups
return &MembershipCredentials{
IdentityCredential: credential.IdentityCredential,
MembershipGroups: membershipGroupsIntersection,
}
} else {
return nil
}
}
// We hit this return only if
// - filterIdentityCredentials.len() == 0 and filterMembershipContracts.len() == 0 (no filter)
// - filterIdentityCredentials.len() != 0 and filterMembershipContracts.len() == 0 (filter only on identity credential)
// Indeed, filterMembershipContracts.len() != 0 will have its exclusive return based on all values of membershipGroupsIntersection.len()
return &credential
}
func GetMembershipCredentials(logger *zap.Logger, credentialsPath string, password string, appInfo AppInfo, filterIdentityCredentials []MembershipCredentials, filterMembershipContracts []MembershipContract) ([]MembershipCredentials, error) {
k, err := LoadAppKeystore(credentialsPath, appInfo, DefaultSeparator)
if err != nil {
return nil, err
// GetMembershipCredentials decrypts and retrieves membership credentials from the keystore applying filters
func (k *AppKeystore) GetMembershipCredentials(keystorePassword string, filterIdentityCredentials []MembershipCredentials, filterMembershipContracts []MembershipContract) ([]MembershipCredentials, error) {
password := keystorePassword
if password == "" {
k.logger.Warn("keystore: no credentials password set, using default password", zap.String("password", DefaultCredentialsPassword))
password = DefaultCredentialsPassword
}
var result []MembershipCredentials
@ -220,12 +100,7 @@ func GetMembershipCredentials(logger *zap.Logger, credentialsPath string, passwo
}
// AddMembershipCredentials inserts a membership credential to the keystore matching the application, appIdentifier and version filters.
func AddMembershipCredentials(path string, newIdentityCredential *rln.IdentityCredential, newMembershipGroup MembershipGroup, password string, appInfo AppInfo, separator string) (membershipGroupIndex uint, err error) {
k, err := LoadAppKeystore(path, appInfo, DefaultSeparator)
if err != nil {
return 0, err
}
func (k *AppKeystore) AddMembershipCredentials(newIdentityCredential *rln.IdentityCredential, newMembershipGroup MembershipGroup, password string) (membershipGroupIndex uint, err error) {
// A flag to tell us if the keystore contains a credential associated to the input identity credential, i.e. membershipCredential
found := false
for i, existingCredentials := range k.Credentials {
@ -275,7 +150,7 @@ func AddMembershipCredentials(path string, newIdentityCredential *rln.IdentityCr
}
// we update the original credential field in keystoreCredentials
k.Credentials[i] = AppKeystoreCredential{Crypto: encryptedCredentials}
k.Credentials[i] = appKeystoreCredential{Crypto: encryptedCredentials}
found = true
@ -309,28 +184,23 @@ func AddMembershipCredentials(path string, newIdentityCredential *rln.IdentityCr
return 0, err
}
k.Credentials = append(k.Credentials, AppKeystoreCredential{Crypto: encryptedCredentials})
k.Credentials = append(k.Credentials, appKeystoreCredential{Crypto: encryptedCredentials})
membershipGroupIndex = uint(len(newCredential.MembershipGroups) - 1)
}
return membershipGroupIndex, save(k, path, separator)
return membershipGroupIndex, save(k, k.path)
}
// Safely saves a Keystore's JsonNode to disk.
// If exists, the destination file is renamed with extension .bkp; the file is written at its destination and the .bkp file is removed if write is successful, otherwise is restored
func save(keystore AppKeystore, path string, separator string) error {
// We first backup the current keystore
_, err := os.Stat(path)
if err == nil {
err := os.Rename(path, path+".bkp")
if err != nil {
return err
}
func createAppKeystore(path string, appInfo AppInfo, separator string) error {
if separator == "" {
separator = defaultSeparator
}
if separator == "" {
separator = DefaultSeparator
keystore := AppKeystore{
Application: appInfo.Application,
AppIdentifier: appInfo.AppIdentifier,
Version: appInfo.Version,
}
b, err := json.Marshal(keystore)
@ -342,6 +212,75 @@ func save(keystore AppKeystore, path string, separator string) error {
buffer := new(bytes.Buffer)
err = json.Compact(buffer, b)
if err != nil {
return err
}
return os.WriteFile(path, buffer.Bytes(), 0600)
}
func filterCredential(credential MembershipCredentials, filterIdentityCredentials []MembershipCredentials, filterMembershipContracts []MembershipContract) *MembershipCredentials {
if len(filterIdentityCredentials) != 0 {
found := false
for _, filterCreds := range filterIdentityCredentials {
if filterCreds.Equals(credential) {
found = true
}
}
if !found {
return nil
}
}
if len(filterMembershipContracts) != 0 {
var membershipGroupsIntersection []MembershipGroup
for _, filterContract := range filterMembershipContracts {
for _, credentialGroups := range credential.MembershipGroups {
if filterContract.Equals(credentialGroups.MembershipContract) {
membershipGroupsIntersection = append(membershipGroupsIntersection, credentialGroups)
}
}
}
if len(membershipGroupsIntersection) != 0 {
// If we have a match on some groups, we return the credential with filtered groups
return &MembershipCredentials{
IdentityCredential: credential.IdentityCredential,
MembershipGroups: membershipGroupsIntersection,
}
} else {
return nil
}
}
// We hit this return only if
// - filterIdentityCredentials.len() == 0 and filterMembershipContracts.len() == 0 (no filter)
// - filterIdentityCredentials.len() != 0 and filterMembershipContracts.len() == 0 (filter only on identity credential)
// Indeed, filterMembershipContracts.len() != 0 will have its exclusive return based on all values of membershipGroupsIntersection.len()
return &credential
}
// Safely saves a Keystore's JsonNode to disk.
// If exists, the destination file is renamed with extension .bkp; the file is written at its destination and the .bkp file is removed if write is successful, otherwise is restored
func save(keystore *AppKeystore, path string) error {
// We first backup the current keystore
_, err := os.Stat(path)
if err == nil {
err := os.Rename(path, path+".bkp")
if err != nil {
return err
}
}
b, err := json.Marshal(keystore)
if err != nil {
return err
}
b = append(b, []byte(defaultSeparator)...)
buffer := new(bytes.Buffer)
err = json.Compact(buffer, b)
if err != nil {
restoreErr := os.Rename(path, path+".bkp")
@ -351,7 +290,7 @@ func save(keystore AppKeystore, path string, separator string) error {
return err
}
err = ioutil.WriteFile(path, buffer.Bytes(), 0600)
err = os.WriteFile(path, buffer.Bytes(), 0600)
if err != nil {
restoreErr := os.Rename(path, path+".bkp")
if restoreErr != nil {

View File

@ -0,0 +1,81 @@
package keystore
import (
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/waku-org/go-zerokit-rln/rln"
"go.uber.org/zap"
)
// MembershipContract contains information about a membership smart contract address and the chain in which it is deployed
type MembershipContract struct {
ChainID string `json:"chainId"`
Address string `json:"address"`
}
// Equals is used to compare MembershipContract
func (m MembershipContract) Equals(other MembershipContract) bool {
return m.Address == other.Address && m.ChainID == other.ChainID
}
// MembershipGroup contains information about the index in which a credential is stored in the merkle tree and the contract associated to this credential
type MembershipGroup struct {
MembershipContract MembershipContract `json:"membershipContract"`
TreeIndex rln.MembershipIndex `json:"treeIndex"`
}
// Equals is used to compare MembershipGroup
func (m MembershipGroup) Equals(other MembershipGroup) bool {
return m.MembershipContract.Equals(other.MembershipContract) && m.TreeIndex == other.TreeIndex
}
// MembershipCredentials contains all the information about an RLN Identity Credential and membership group it belongs to
type MembershipCredentials struct {
IdentityCredential *rln.IdentityCredential `json:"identityCredential"`
MembershipGroups []MembershipGroup `json:"membershipGroups"`
}
// Equals is used to compare MembershipCredentials
func (m MembershipCredentials) Equals(other MembershipCredentials) bool {
if !rln.IdentityCredentialEquals(*m.IdentityCredential, *other.IdentityCredential) {
return false
}
for _, x := range m.MembershipGroups {
found := false
for _, y := range other.MembershipGroups {
if x.Equals(y) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// AppInfo is a helper structure that contains information about the application that uses these credentials
type AppInfo struct {
Application string `json:"application"`
AppIdentifier string `json:"appIdentifier"`
Version string `json:"version"`
}
// AppKeystore represents the membership credentials to be used in RLN
type AppKeystore struct {
Application string `json:"application"`
AppIdentifier string `json:"appIdentifier"`
Credentials []appKeystoreCredential `json:"credentials"`
Version string `json:"version"`
path string
logger *zap.Logger
}
type appKeystoreCredential struct {
Crypto keystore.CryptoJSON `json:"crypto"`
}
const defaultSeparator = "\n"