feat(credentials): store and processing generic app credentials (#1466)

* feat(credentials): store and processing generic app credentials

* feat(credentials): separate module; minimal tests

* more work

* feat(credentials): check presence of idCredential in keystore and add only new membership groups

* feat(credential): refactor, new data structure, dynamic add credential, filter

* feat(credential): add filter, get credentials

* feat(credential): encode/decode utility

* feat(credential): sort groups, test credential retrieval/group merging

* fix(credential): remove unnecessary order in sort

* fix(credentials): fix vendor commits

* fix(credential/rln): embed credential module in rln relay

* feat(credentials/rln): use credentials API in rln-relay to store/read credentials

* refactor(credentials): implement hasKeys for JsonNode

* fix(credentials): restore connectToNodes call

* refactor(credentials): remove unnecessary imports

* refactor(credentials): add Res suffix to results

* refactor(credential): moved save json to separate proc; added comments

* feat(credentials): use appInfo

* refactor(keystore): refactor code in a more structured module; address reviewers

* fix(keystore): fix indentation
This commit is contained in:
G 2023-02-08 16:26:23 +01:00 committed by GitHub
parent 1a9f633311
commit cdc09aeeb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 791 additions and 176 deletions

View File

@ -27,7 +27,6 @@ when defined(waku_exp_store_resume):
# TODO: Review store resume test cases (#1282)
import ./v2/waku_store/test_resume
import
# Waku v2 tests
./v2/test_wakunode,
@ -61,11 +60,12 @@ import
./v2/test_waku_noise,
./v2/test_waku_noise_sessions,
./v2/test_waku_switch,
# Waku Keystore
./v2/test_waku_keystore_keyfile,
./v2/test_waku_keystore,
# Utils
./v2/test_utils_compat,
./v2/test_utils_keyfile
./v2/test_utils_compat
## Experimental
when defined(rln):

View File

@ -0,0 +1,191 @@
{.used.}
import
std/[algorithm, json, options, os],
testutils/unittests, chronos, stint,
../../waku/v2/protocol/waku_keystore,
../test_helpers
from ../../waku/v2/protocol/waku_noise/noise_utils import randomSeqByte
procSuite "Credentials test suite":
# We initialize the RNG in test_helpers
let rng = rng()
let testAppInfo = AppInfo(application: "test", appIdentifier: "1234", version: "0.1")
asyncTest "Create keystore":
let filepath = "./testAppKeystore.txt"
defer: removeFile(filepath)
let keystoreRes = createAppKeystore(path = filepath,
appInfo = testAppInfo)
check:
keystoreRes.isOk()
asyncTest "Load keystore":
let filepath = "./testAppKeystore.txt"
defer: removeFile(filepath)
# If no keystore exists at filepath, a new one is created for appInfo and empty credentials
let keystoreRes = loadAppKeystore(path = filepath,
appInfo = testAppInfo)
check:
keystoreRes.isOk()
let keystore = keystoreRes.get()
check:
keystore.hasKeys(["application", "appIdentifier", "version", "credentials"])
keystore["application"].getStr() == testAppInfo.application
keystore["appIdentifier"].getStr() == testAppInfo.appIdentifier
keystore["version"].getStr() == testAppInfo.version
# We assume the loaded keystore to not have credentials set (previous tests delete the keystore at filepath)
keystore["credentials"].getElems().len() == 0
asyncTest "Add credentials to keystore":
let filepath = "./testAppKeystore.txt"
defer: removeFile(filepath)
# We generate a random identity credential (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen)
var
idTrapdoor = randomSeqByte(rng[], 32)
idNullifier = randomSeqByte(rng[], 32)
idSecretHash = randomSeqByte(rng[], 32)
idCommitment = randomSeqByte(rng[], 32)
var idCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment)
var contract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789")
var index1 = MembershipIndex(1)
var membershipGroup1 = MembershipGroup(membershipContract: contract, treeIndex: index1)
let membershipCredentials1 = MembershipCredentials(identityCredential: idCredential,
membershipGroups: @[membershipGroup1])
# We generate a random identity credential (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen)
idTrapdoor = randomSeqByte(rng[], 32)
idNullifier = randomSeqByte(rng[], 32)
idSecretHash = randomSeqByte(rng[], 32)
idCommitment = randomSeqByte(rng[], 32)
idCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment)
var index2 = MembershipIndex(2)
var membershipGroup2 = MembershipGroup(membershipContract: contract, treeIndex: index2)
let membershipCredentials2 = MembershipCredentials(identityCredential: idCredential,
membershipGroups: @[membershipGroup2])
let password = "%m0um0ucoW%"
let keystoreRes = addMembershipCredentials(path = filepath,
credentials = @[membershipCredentials1, membershipCredentials2],
password = password,
appInfo = testAppInfo)
check:
keystoreRes.isOk()
asyncTest "Add/retrieve credentials in keystore":
let filepath = "./testAppKeystore.txt"
defer: removeFile(filepath)
# We generate two random identity credentials (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen)
var
idTrapdoor1 = randomSeqByte(rng[], 32)
idNullifier1 = randomSeqByte(rng[], 32)
idSecretHash1 = randomSeqByte(rng[], 32)
idCommitment1 = randomSeqByte(rng[], 32)
idCredential1 = IdentityCredential(idTrapdoor: idTrapdoor1, idNullifier: idNullifier1, idSecretHash: idSecretHash1, idCommitment: idCommitment1)
var
idTrapdoor2 = randomSeqByte(rng[], 32)
idNullifier2 = randomSeqByte(rng[], 32)
idSecretHash2 = randomSeqByte(rng[], 32)
idCommitment2 = randomSeqByte(rng[], 32)
idCredential2 = IdentityCredential(idTrapdoor: idTrapdoor2, idNullifier: idNullifier2, idSecretHash: idSecretHash2, idCommitment: idCommitment2)
# We generate two distinct membership groups
var contract1 = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789")
var index1 = MembershipIndex(1)
var membershipGroup1 = MembershipGroup(membershipContract: contract1, treeIndex: index1)
var contract2 = MembershipContract(chainId: "6", address: "0x0000000000000000000000000000000000000000")
var index2 = MembershipIndex(2)
var membershipGroup2 = MembershipGroup(membershipContract: contract2, treeIndex: index2)
# We generate three membership credentials
let membershipCredentials1 = MembershipCredentials(identityCredential: idCredential1,
membershipGroups: @[membershipGroup1])
let membershipCredentials2 = MembershipCredentials(identityCredential: idCredential2,
membershipGroups: @[membershipGroup2])
let membershipCredentials3 = MembershipCredentials(identityCredential: idCredential1,
membershipGroups: @[membershipGroup2])
# This is the same as rlnMembershipCredentials3, should not change the keystore entry of idCredential
let membershipCredentials4 = MembershipCredentials(identityCredential: idCredential1,
membershipGroups: @[membershipGroup2])
let password = "%m0um0ucoW%"
# We add credentials to the keystore. Note that only 3 credentials should be effectively added, since rlnMembershipCredentials3 is equal to membershipCredentials2
let keystoreRes = addMembershipCredentials(path = filepath,
credentials = @[membershipCredentials1, membershipCredentials2, membershipCredentials3, membershipCredentials4],
password = password,
appInfo = testAppInfo)
check:
keystoreRes.isOk()
# We test retrieval of credentials.
var expectedMembershipGroups1 = @[membershipGroup1, membershipGroup2]
expectedMembershipGroups1.sort(sortMembershipGroup)
let expectedCredential1 = MembershipCredentials(identityCredential: idCredential1,
membershipGroups: expectedMembershipGroups1)
var expectedMembershipGroups2 = @[membershipGroup2]
expectedMembershipGroups2.sort(sortMembershipGroup)
let expectedCredential2 = MembershipCredentials(identityCredential: idCredential2,
membershipGroups: expectedMembershipGroups2)
# We retrieve all credentials stored under password (no filter)
var recoveredCredentialsRes = getMembershipCredentials(path = filepath,
password = password,
appInfo = testAppInfo)
check:
recoveredCredentialsRes.isOk()
recoveredCredentialsRes.get() == @[expectedCredential1, expectedCredential2]
# We retrieve credentials by filtering on an IdentityCredential
recoveredCredentialsRes = getMembershipCredentials(path = filepath,
password = password,
filterIdentityCredentials = @[idCredential1],
appInfo = testAppInfo)
check:
recoveredCredentialsRes.isOk()
recoveredCredentialsRes.get() == @[expectedCredential1]
# We retrieve credentials by filtering on multiple IdentityCredentials
recoveredCredentialsRes = getMembershipCredentials(path = filepath,
password = password,
filterIdentityCredentials = @[idCredential1, idCredential2],
appInfo = testAppInfo)
check:
recoveredCredentialsRes.isOk()
recoveredCredentialsRes.get() == @[expectedCredential1, expectedCredential2]

View File

@ -6,7 +6,7 @@ import
testutils/unittests, chronos,
eth/keys
import
../../waku/v2/utils/keyfile
../../waku/v2/protocol/waku_keystore
from ../../waku/v2/protocol/waku_noise/noise_utils import randomSeqByte
@ -312,7 +312,7 @@ suite "KeyFile test suite (adapted from nim-eth keyfile tests)":
check:
secret.isErr()
secret.error == KeyFileError.IncorrectMac
secret.error == KeyFileError.KeyfileIncorrectMac
test "Wrong mac in keyfile":
@ -350,7 +350,7 @@ suite "KeyFile test suite (adapted from nim-eth keyfile tests)":
keyfileWrongMac.getOrDefault("password").getStr())
check:
secret.isErr()
secret.error == KeyFileError.IncorrectMac
secret.error == KeyFileError.KeyFileIncorrectMac
test "Scrypt keyfiles":
let

View File

@ -14,12 +14,14 @@ import
../../waku/v2/node/waku_node,
../../waku/v2/protocol/waku_message,
../../waku/v2/protocol/waku_rln_relay,
../../waku/v2/protocol/waku_keystore,
../test_helpers
const RlnRelayPubsubTopic = "waku/2/rlnrelay/proto"
const RlnRelayContentTopic = "waku/2/rlnrelay/proto"
procSuite "Waku rln relay":
asyncTest "mount waku-rln-relay in the off-chain mode":
let
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[]
@ -1041,10 +1043,11 @@ suite "Waku rln relay":
debug "the generated identity credential: ", idCredential
let index = MembershipIndex(1)
let index = MembershipIndex(1)
let rlnMembershipCredentials = RlnMembershipCredentials(identityCredential: idCredential,
rlnIndex: index)
let rlnMembershipContract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789")
let rlnMembershipGroup = MembershipGroup(membershipContract: rlnMembershipContract, treeIndex: index)
let rlnMembershipCredentials = MembershipCredentials(identityCredential: idCredential, membershipGroups: @[rlnMembershipGroup])
let password = "%m0um0ucoW%"
@ -1053,19 +1056,31 @@ suite "Waku rln relay":
# Write RLN credentials
require:
writeRlnCredentials(filepath, rlnMembershipCredentials, password).isOk()
addMembershipCredentials(path = filepath,
credentials = @[rlnMembershipCredentials],
password = password,
appInfo = RLNAppInfo).isOk()
let readCredentialsResult = getMembershipCredentials(path = filepath,
password = password,
filterMembershipContracts = @[rlnMembershipContract],
appInfo = RLNAppInfo)
let readCredentialsResult = readRlnCredentials(filepath, password)
require:
readCredentialsResult.isOk()
let credentials = readCredentialsResult.get()
# getMembershipCredentials returns all credentials in keystore as sequence matching the filter
let allMatchingCredentials = readCredentialsResult.get()
# if any is found, we return the first credential, otherwise credentials is none
var credentials = none(MembershipCredentials)
if allMatchingCredentials.len() > 0:
credentials = some(allMatchingCredentials[0])
require:
credentials.isSome()
check:
credentials.get().identityCredential == idCredential
credentials.get().rlnIndex == index
credentials.get().membershipGroups == @[rlnMembershipGroup]
test "histogram static bucket generation":
let buckets = generateBucketsForHistogram(10)

View File

@ -8,6 +8,7 @@ import
stew/byteutils, stew/shims/net as stewNet,
libp2p/crypto/crypto,
eth/keys,
../../waku/v2/protocol/waku_keystore,
../../waku/v2/protocol/waku_rln_relay,
../../waku/v2/node/waku_node,
../test_helpers,

View File

@ -17,6 +17,7 @@ import
../../waku/v2/node/waku_node,
../../waku/v2/protocol/waku_message,
../../waku/v2/protocol/waku_rln_relay,
../../waku/v2/protocol/waku_keystore,
../../waku/v2/utils/peers
from std/times import epochTime

View File

@ -390,7 +390,7 @@ proc info*(node: WakuNode): WakuInfo =
proc connectToNodes*(node: WakuNode, nodes: seq[RemotePeerInfo] | seq[string], source = "api") {.async.} =
## `source` indicates source of node addrs (static config, api call, discovery, etc)
# NOTE This is dialing on WakuRelay protocol specifically
await connectToNodes(node.peerManager, nodes, WakuRelayCodec, source=source)
await peer_manager.connectToNodes(node.peerManager, nodes, WakuRelayCodec, source=source)
## Waku relay

View File

@ -0,0 +1,19 @@
# The keyfile submodule (implementation adapted from nim-eth keyfile module https://github.com/status-im/nim-eth/blob/master/eth/keyfile)
import
./waku_keystore/keyfile
export
keyfile
# The Waku Keystore implementation
import
./waku_keystore/keystore,
./waku_keystore/conversion_utils,
./waku_keystore/protocol_types,
./waku_keystore/utils
export
keystore,
conversion_utils,
protocol_types,
utils

View File

@ -0,0 +1,29 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import
json,
stew/[results, byteutils],
./protocol_types
# Encodes a Membership credential to a byte sequence
proc encode*(credential: MembershipCredentials): seq[byte] =
# TODO: use custom encoding, avoid wordy json
var stringCredential: string
# NOTE: toUgly appends to the string, doesn't replace its contents
stringCredential.toUgly(%credential)
return toBytes(stringCredential)
# Decodes a byte sequence to a Membership credential
proc decode*(encodedCredential: seq[byte]): KeystoreResult[MembershipCredentials] =
# TODO: use custom decoding, avoid wordy json
try:
# we parse the json decrypted keystoreCredential
let jsonObject = parseJson(string.fromBytes(encodedCredential))
return ok(to(jsonObject, MembershipCredentials))
except JsonParsingError:
return err(KeystoreJsonError)
except Exception: #parseJson raises Exception
return err(KeystoreOsError)

View File

@ -29,26 +29,26 @@ const
type
KeyFileError* = enum
RandomError = "keyfile error: Random generator error"
UuidError = "keyfile error: UUID generator error"
BufferOverrun = "keyfile error: Supplied buffer is too small"
IncorrectDKLen = "keyfile error: `dklen` parameter is 0 or more then MaxDKLen"
MalformedError = "keyfile error: JSON has incorrect structure"
NotImplemented = "keyfile error: Feature is not implemented"
NotSupported = "keyfile error: Feature is not supported"
EmptyMac = "keyfile error: `mac` parameter is zero length or not in hexadecimal form"
EmptyCiphertext = "keyfile error: `ciphertext` parameter is zero length or not in hexadecimal format"
EmptySalt = "keyfile error: `salt` parameter is zero length or not in hexadecimal format"
EmptyIV = "keyfile error: `cipherparams.iv` parameter is zero length or not in hexadecimal format"
IncorrectIV = "keyfile error: Size of IV vector is not equal to cipher block size"
PrfNotSupported = "keyfile error: PRF algorithm for PBKDF2 is not supported"
KdfNotSupported = "keyfile error: KDF algorithm is not supported"
CipherNotSupported = "keyfile error: `cipher` parameter is not supported"
IncorrectMac = "keyfile error: `mac` verification failed"
ScryptBadParam = "keyfile error: bad scrypt's parameters"
OsError = "keyfile error: OS specific error"
IoError = "keyfile error: IO specific error"
JsonError = "keyfile error: JSON encoder/decoder error"
KeyfileRandomError = "keyfile error: Random generator error"
KeyfileUuidError = "keyfile error: UUID generator error"
KeyfileBufferOverrun = "keyfile error: Supplied buffer is too small"
KeyfileIncorrectDKLen = "keyfile error: `dklen` parameter is 0 or more then MaxDKLen"
KeyfileMalformedError = "keyfile error: JSON has incorrect structure"
KeyfileNotImplemented = "keyfile error: Feature is not implemented"
KeyfileNotSupported = "keyfile error: Feature is not supported"
KeyfileEmptyMac = "keyfile error: `mac` parameter is zero length or not in hexadecimal form"
KeyfileEmptyCiphertext = "keyfile error: `ciphertext` parameter is zero length or not in hexadecimal format"
KeyfileEmptySalt = "keyfile error: `salt` parameter is zero length or not in hexadecimal format"
KeyfileEmptyIV = "keyfile error: `cipherparams.iv` parameter is zero length or not in hexadecimal format"
KeyfileIncorrectIV = "keyfile error: Size of IV vector is not equal to cipher block size"
KeyfilePrfNotSupported = "keyfile error: PRF algorithm for PBKDF2 is not supported"
KeyfileKdfNotSupported = "keyfile error: KDF algorithm is not supported"
KeyfileCipherNotSupported = "keyfile error: `cipher` parameter is not supported"
KeyfileIncorrectMac = "keyfile error: `mac` verification failed"
KeyfileScryptBadParam = "keyfile error: bad scrypt's parameters"
KeyfileOsError = "keyfile error: OS specific error"
KeyfileIoError = "keyfile error: IO specific error"
KeyfileJsonError = "keyfile error: JSON encoder/decoder error"
KeyfileDoesNotExist = "keyfile error: file does not exist"
KdfKind* = enum
@ -215,9 +215,9 @@ proc deriveKey(password: string,
ctx.clear()
ok(output)
else:
err(PrfNotSupported)
err(KeyfilePrfNotSupported)
else:
err(NotImplemented)
err(KeyfileNotImplemented)
# Scrypt wrapper
func scrypt[T, M](password: openArray[T], salt: openArray[M],
@ -234,7 +234,7 @@ proc deriveKey(password: string, salt: string,
let wf = if workFactor == 0: ScryptWorkFactor else: workFactor
var output: DKey
if scrypt(password, salt, wf, r, p, output) == 0:
return err(ScryptBadParam)
return err(KeyfileScryptBadParam)
return ok(output)
@ -251,7 +251,7 @@ proc encryptData(plaintext: openArray[byte],
ctx.clear()
ok(ciphertext)
else:
err(NotImplemented)
err(KeyfileNotImplemented)
# Decryption routine
proc decryptData(ciphertext: openArray[byte],
@ -260,7 +260,7 @@ proc decryptData(ciphertext: openArray[byte],
iv: openArray[byte]): KfResult[seq[byte]] =
if cryptkind == AES128CTR:
if len(iv) != aes128.sizeBlock:
return err(IncorrectIV)
return err(KeyfileIncorrectIV)
var plaintext = newSeqWith(ciphertext.len, 0.byte)
var ctx: CTR[aes128]
ctx.init(toOpenArray(key, 0, 15), iv)
@ -268,7 +268,7 @@ proc decryptData(ciphertext: openArray[byte],
ctx.clear()
ok(plaintext)
else:
err(NotImplemented)
err(KeyfileNotImplemented)
# Encodes KDF parameters in JSON
proc kdfParams(kdfkind: KdfKind, salt: string, workfactor: int): KfResult[JsonNode] =
@ -294,7 +294,7 @@ proc kdfParams(kdfkind: KdfKind, salt: string, workfactor: int): KfResult[JsonNo
}
)
else:
err(NotImplemented)
err(KeyfileNotImplemented)
# Decodes hex strings to byte sequences
proc decodeHex*(m: string): seq[byte] =
@ -350,12 +350,12 @@ proc createKeyFileJson*(secret: openArray[byte],
var salt: array[SaltSize, byte]
var saltstr = newString(SaltSize)
if randomBytes(iv) != aes128.sizeBlock:
return err(RandomError)
return err(KeyfileRandomError)
if randomBytes(salt) != SaltSize:
return err(RandomError)
return err(KeyfileRandomError)
copyMem(addr saltstr[0], addr salt[0], SaltSize)
let u = ? uuidGenerate().mapErrTo(UuidError)
let u = ? uuidGenerate().mapErrTo(KeyfileUuidError)
let
dkey = case kdfkind
@ -398,21 +398,21 @@ proc createKeyFileJson*(secret: openArray[byte],
proc decodeCrypto(n: JsonNode): KfResult[Crypto] =
var crypto = n.getOrDefault("crypto")
if isNil(crypto):
return err(MalformedError)
return err(KeyfileMalformedError)
var kdf = crypto.getOrDefault("kdf")
if isNil(kdf):
return err(MalformedError)
return err(KeyfileMalformedError)
var c: Crypto
case kdf.getStr()
of "pbkdf2": c.kind = PBKDF2
of "scrypt": c.kind = SCRYPT
else: return err(KdfNotSupported)
else: return err(KeyfileKdfNotSupported)
var cipherparams = crypto.getOrDefault("cipherparams")
if isNil(cipherparams):
return err(MalformedError)
return err(KeyfileMalformedError)
c.cipher.kind = getCipher(crypto.getOrDefault("cipher").getStr())
c.cipher.params.iv = decodeHex(cipherparams.getOrDefault("iv").getStr())
@ -421,13 +421,13 @@ proc decodeCrypto(n: JsonNode): KfResult[Crypto] =
c.kdfParams = crypto.getOrDefault("kdfparams")
if c.cipher.kind == CipherNoSupport:
return err(CipherNotSupported)
return err(KeyfileCipherNotSupported)
if len(c.cipher.text) == 0:
return err(EmptyCiphertext)
return err(KeyfileEmptyCiphertext)
if len(c.mac) == 0:
return err(EmptyMac)
return err(KeyfileEmptyMac)
if isNil(c.kdfParams):
return err(MalformedError)
return err(KeyfileMalformedError)
return ok(c)
@ -436,16 +436,16 @@ proc decodePbkdf2Params(params: JsonNode): KfResult[Pbkdf2Params] =
var p: Pbkdf2Params
p.salt = decodeSalt(params.getOrDefault("salt").getStr())
if len(p.salt) == 0:
return err(EmptySalt)
return err(KeyfileEmptySalt)
p.dklen = params.getOrDefault("dklen").getInt()
p.c = params.getOrDefault("c").getInt()
p.prf = getPrfHash(params.getOrDefault("prf").getStr())
if p.prf == HashNoSupport:
return err(PrfNotSupported)
return err(KeyfilePrfNotSupported)
if p.dklen == 0 or p.dklen > MaxDKLen:
return err(IncorrectDKLen)
return err(KeyfileIncorrectDKLen)
return ok(p)
@ -454,7 +454,7 @@ proc decodeScryptParams(params: JsonNode): KfResult[ScryptParams] =
var p: ScryptParams
p.salt = decodeSalt(params.getOrDefault("salt").getStr())
if len(p.salt) == 0:
return err(EmptySalt)
return err(KeyfileEmptySalt)
p.dklen = params.getOrDefault("dklen").getInt()
p.n = params.getOrDefault("n").getInt()
@ -462,7 +462,7 @@ proc decodeScryptParams(params: JsonNode): KfResult[ScryptParams] =
p.r = params.getOrDefault("r").getInt()
if p.dklen == 0 or p.dklen > MaxDKLen:
return err(IncorrectDKLen)
return err(KeyfileIncorrectDKLen)
return ok(p)
@ -475,7 +475,7 @@ func decryptSecret(crypto: Crypto, dkey: DKey): KfResult[seq[byte]] =
var mac = ctx.finish()
ctx.clear()
if not compareMac(mac.data, crypto.mac):
return err(IncorrectMac)
return err(KeyfileIncorrectMac)
let plaintext = ? decryptData(crypto.cipher.text, crypto.cipher.kind, dkey, crypto.cipher.params.iv)
@ -537,20 +537,20 @@ proc loadKeyFiles*(pathname: string,
try:
data = json.parseJson(keyfile)
except JsonParsingError:
return err(JsonError)
return err(KeyfileJsonError)
except ValueError:
return err(JsonError)
return err(KeyfileJsonError)
except OSError:
return err(OsError)
return err(KeyfileOsError)
except Exception: #parseJson raises Exception
return err(OsError)
return err(KeyfileOsError)
decodedKeyfile = decodeKeyFileJson(data, password)
if decodedKeyfile.isOk():
successfullyDecodedKeyfiles.add decodedKeyfile
except IOError:
return err(IoError)
return err(KeyfileIoError)
return ok(successfullyDecodedKeyfiles)
@ -561,7 +561,7 @@ proc saveKeyFile*(pathname: string,
var
f: File
if not f.open(pathname, fmAppend):
return err(OsError)
return err(KeyfileOsError)
try:
# To avoid other users/attackers to be able to read keyfiles, we make the file readable/writable only by the running user
setFilePermissions(pathname, {fpUserWrite, fpUserRead})
@ -570,7 +570,7 @@ proc saveKeyFile*(pathname: string,
f.write("\n")
ok()
except CatchableError:
err(OsError)
err(KeyfileOsError)
finally:
f.close()

View File

@ -0,0 +1,241 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import
options, json, strutils,
std/[algorithm, os, sequtils, sets]
import
./keyfile,
./conversion_utils,
./protocol_types,
./utils
# This proc creates an empty keystore (i.e. with no credentials)
proc createAppKeystore*(path: string,
appInfo: AppInfo,
separator: string = "\n"): KeystoreResult[void] =
let keystore = AppKeystore(application: appInfo.application,
appIdentifier: appInfo.appIdentifier,
credentials: @[],
version: appInfo.version)
var jsonKeystore: string
jsonKeystore.toUgly(%keystore)
var f: File
if not f.open(path, fmWrite):
return err(KeystoreOsError)
try:
# To avoid other users/attackers to be able to read keyfiles, we make the file readable/writable only by the running user
setFilePermissions(path, {fpUserWrite, fpUserRead})
f.write(jsonKeystore)
# We separate keystores with separator
f.write(separator)
ok()
except CatchableError:
err(KeystoreOsError)
finally:
f.close()
# This proc load a keystore based on the application, appIdentifier and version filters.
# If none is found, it automatically creates an empty keystore for the passed parameters
proc loadAppKeystore*(path: string,
appInfo: AppInfo,
separator: string = "\n"): KeystoreResult[JsonNode] =
## Load and decode JSON keystore from pathname
var data: JsonNode
var matchingAppKeystore: JsonNode
# If no keystore exists at path we create a new empty one with passed keystore parameters
if fileExists(path) == false:
let newKeystore = createAppKeystore(path, appInfo, separator)
if newKeystore.isErr():
return err(KeystoreCreateKeystoreError)
try:
# We read all the file contents
var f: File
if not f.open(path, fmRead):
return err(KeystoreOsError)
let fileContents = readAll(f)
# We iterate over each substring split by separator (which we expect to correspond to a single keystore json)
for keystore in fileContents.split(separator):
# We skip if read line is empty
if keystore.len == 0:
continue
# We skip all lines that don't seem to define a json
if not keystore.startsWith("{") or not keystore.endsWith("}"):
continue
try:
# We parse the json
data = json.parseJson(keystore)
# We check if parsed json contains the relevant keystore credentials fields and if these are set to the passed parameters
# (note that "if" is lazy, so if one of the .contains() fails, the json fields contents will not be checked and no ResultDefect will be raised due to accessing unavailable fields)
if data.hasKeys(["application", "appIdentifier", "credentials", "version"]) and
data["application"].getStr() == appInfo.application and
data["appIdentifier"].getStr() == appInfo.appIdentifier and
data["version"].getStr() == appInfo.version:
# We return the first json keystore that matches the passed app parameters
# We assume a unique kesytore with such parameters is present in the file
matchingAppKeystore = data
break
# TODO: we might continue rather than return for some of these errors
except JsonParsingError:
return err(KeystoreJsonError)
except ValueError:
return err(KeystoreJsonError)
except OSError:
return err(KeystoreOsError)
except Exception: #parseJson raises Exception
return err(KeystoreOsError)
except IOError:
return err(KeystoreIoError)
return ok(matchingAppKeystore)
# Adds a sequence of membership credential to the keystore matching the application, appIdentifier and version filters.
proc addMembershipCredentials*(path: string,
credentials: seq[MembershipCredentials],
password: string,
appInfo: AppInfo,
separator: string = "\n"): KeystoreResult[void] =
# We load the keystore corresponding to the desired parameters
# This call ensures that JSON has all required fields
let jsonKeystoreRes = loadAppKeystore(path, appInfo, separator)
if jsonKeystoreRes.isErr():
return err(KeystoreLoadKeystoreError)
# We load the JSON node corresponding to the app keystore
var jsonKeystore = jsonKeystoreRes.get()
try:
if jsonKeystore.hasKey("credentials"):
# We get all credentials in keystore
var keystoreCredentials = jsonKeystore["credentials"]
var found: bool
for membershipCredential in credentials:
# A flag to tell us if the keystore contains a credential associated to the input identity credential, i.e. membershipCredential
found = false
for keystoreCredential in keystoreCredentials.mitems():
# keystoreCredential is encrypted. We decrypt it
let decodedKeyfileRes = decodeKeyFileJson(keystoreCredential, password)
if decodedKeyfileRes.isOk():
# we parse the json decrypted keystoreCredential
let decodedCredentialRes = decode(decodedKeyfileRes.get())
if decodedCredentialRes.isOk():
let keyfileMembershipCredential = decodedCredentialRes.get()
# We check if the decrypted credential has its identityCredential field equal to the input credential
if keyfileMembershipCredential.identityCredential == membershipCredential.identityCredential:
# idCredential is present in keystore. We add the input credential membership group to the one contained in the decrypted keystore credential (we deduplicate groups using sets)
var allMemberships = toSeq(toHashSet(keyfileMembershipCredential.membershipGroups) + toHashSet(membershipCredential.membershipGroups))
# We sort membership groups, otherwise we will not have deterministic results in tests
allMemberships.sort(sortMembershipGroup)
# we define the updated credential with the updated membership sets
let updatedCredential = MembershipCredentials(identityCredential: keyfileMembershipCredential.identityCredential, membershipGroups: allMemberships)
# we re-encrypt creating a new keyfile
let encodedUpdatedCredential = updatedCredential.encode()
let updatedCredentialKeyfileRes = createKeyFileJson(encodedUpdatedCredential, password)
if updatedCredentialKeyfileRes.isErr():
return err(KeystoreCreateKeyfileError)
# we update the original credential field in keystoreCredentials
keystoreCredential = updatedCredentialKeyfileRes.get()
found = true
# We stop decrypting other credentials in the keystore
break
# If no credential in keystore with same input identityCredential value is found, we add it
if found == false:
let encodedMembershipCredential = membershipCredential.encode()
let keyfileRes = createKeyFileJson(encodedMembershipCredential, password)
if keyfileRes.isErr():
return err(KeystoreCreateKeyfileError)
# We add it to the credentials field of the keystore
jsonKeystore["credentials"].add(keyfileRes.get())
except:
return err(KeystoreJsonError)
# We save to disk the (updated) keystore.
if save(jsonKeystore, path, separator).isErr():
return err(KeystoreOsError)
return ok()
# Returns the membership credentials in the keystore matching the application, appIdentifier and version filters, further filtered by the input
# identity credentials and membership contracts
proc getMembershipCredentials*(path: string,
password: string,
filterIdentityCredentials: seq[IdentityCredential] = @[],
filterMembershipContracts: seq[MembershipContract] = @[],
appInfo: AppInfo): KeystoreResult[seq[MembershipCredentials]] =
var outputMembershipCredentials: seq[MembershipCredentials] = @[]
# We load the keystore corresponding to the desired parameters
# This call ensures that JSON has all required fields
let jsonKeystoreRes = loadAppKeystore(path, appInfo)
if jsonKeystoreRes.isErr():
return err(KeystoreLoadKeystoreError)
# We load the JSON node corresponding to the app keystore
var jsonKeystore = jsonKeystoreRes.get()
try:
if jsonKeystore.hasKey("credentials"):
# We get all credentials in keystore
var keystoreCredentials = jsonKeystore["credentials"]
for keystoreCredential in keystoreCredentials.mitems():
# keystoreCredential is encrypted. We decrypt it
let decodedKeyfileRes = decodeKeyFileJson(keystoreCredential, password)
if decodedKeyfileRes.isOk():
# we parse the json decrypted keystoreCredential
let decodedCredentialRes = decode(decodedKeyfileRes.get())
if decodedCredentialRes.isOk():
let keyfileMembershipCredential = decodedCredentialRes.get()
let filteredCredentialOpt = filterCredential(keyfileMembershipCredential, filterIdentityCredentials, filterMembershipContracts)
if filteredCredentialOpt.isSome():
outputMembershipCredentials.add(filteredCredentialOpt.get())
except:
return err(KeystoreJsonError)
return ok(outputMembershipCredentials)

View File

@ -0,0 +1,66 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import
stew/results
type
IdentityTrapdoor* = seq[byte] #array[32, byte]
IdentityNullifier* = seq[byte] #array[32, byte]
# identity key as defined in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
IdentitySecretHash* = seq[byte] #array[32, byte]
# hash of identity key as defined ed in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
IDCommitment* = seq[byte] #array[32, byte]
type IdentityCredential* = object
idTrapdoor*: IdentityTrapdoor
idNullifier*: IdentityNullifier
## user's identity key (a secret key) which is selected randomly
## see details in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
idSecretHash*: IdentitySecretHash
# hash of user's identity key generated by
# Poseidon hash function implemented in rln lib
# more details in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
idCommitment*: IDCommitment
type MembershipIndex* = uint
type MembershipContract* = object
chainId*: string
address*: string
type MembershipGroup* = object
membershipContract*: MembershipContract
treeIndex*: MembershipIndex
type MembershipCredentials* = object
identityCredential*: IdentityCredential
membershipGroups*: seq[MembershipGroup]
type AppInfo* = object
application*: string
appIdentifier*: string
version*: string
type AppKeystore* = object
application*: string
appIdentifier*: string
credentials*: seq[MembershipCredentials]
version*: string
type
AppKeystoreError* = enum
KeystoreOsError = "keystore error: OS specific error"
KeystoreIoError = "keystore error: IO specific error"
KeystoreJsonKeyError = "keystore error: fields not present in JSON"
KeystoreJsonError = "keystore error: JSON encoder/decoder error"
KeystoreKeystoreDoesNotExist = "keystore error: file does not exist"
KeystoreCreateKeystoreError = "Error while creating application keystore"
KeystoreLoadKeystoreError = "Error while loading application keystore"
KeystoreCreateKeyfileError = "Error while creating keyfile for credentials"
KeystoreSaveKeyfileError = "Error while saving keyfile for credentials"
KeystoreReadKeyfileError = "Error while reading keyfile for credentials"
type KeystoreResult*[T] = Result[T, AppKeystoreError]

View File

@ -0,0 +1,95 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import
json,
std/[options, os, sequtils],
./keyfile,
./protocol_types
# Checks if a JsonNode has all keys contained in "keys"
proc hasKeys*(data: JsonNode, keys: openArray[string]): bool =
return all(keys, proc (key: string): bool = return data.hasKey(key))
# Defines how to sort membership groups
proc sortMembershipGroup*(a,b: MembershipGroup): int =
return cmp(a.membershipContract.address, b.membershipContract.address)
# 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
proc save*(json: JsonNode, path: string, separator: string): KeystoreResult[void] =
# We first backup the current keystore
if fileExists(path):
try:
moveFile(path, path & ".bkp")
except:
return err(KeystoreOsError)
# We save the updated json
var f: File
if not f.open(path, fmAppend):
return err(KeystoreOsError)
try:
# To avoid other users/attackers to be able to read keyfiles, we make the file readable/writable only by the running user
setFilePermissions(path, {fpUserWrite, fpUserRead})
f.write($json)
# We store a keyfile per line
f.write(separator)
except CatchableError:
# We got some KeystoreOsError writing to disk. We attempt to restore the previous keystore backup
if fileExists(path & ".bkp"):
try:
f.close()
removeFile(path)
moveFile(path & ".bkp", path)
except:
# Unlucky, we just fail
return err(KeystoreOsError)
return err(KeystoreOsError)
finally:
f.close()
# The write went fine, so we can remove the backup keystore
if fileExists(path & ".bkp"):
try:
removeFile(path & ".bkp")
except:
return err(KeystoreOsError)
return ok()
# Filters a membership credential based on either input identity credential's value, membership contracts or both
proc filterCredential*(credential: MembershipCredentials,
filterIdentityCredentials: seq[IdentityCredential],
filterMembershipContracts: seq[MembershipContract]): Option[MembershipCredentials] =
# We filter by identity credentials
if filterIdentityCredentials.len() != 0:
if (credential.identityCredential in filterIdentityCredentials) == false:
return none(MembershipCredentials)
# We filter by membership groups credentials
if filterMembershipContracts.len() != 0:
# Here we keep only groups that match a contract in the filter
var membershipGroupsIntersection: seq[MembershipGroup] = @[]
# We check if we have a group in the input credential matching any contract in the filter
for membershipGroup in credential.membershipGroups:
if membershipGroup.membershipContract in filterMembershipContracts:
membershipGroupsIntersection.add(membershipGroup)
if membershipGroupsIntersection.len() != 0:
# If we have a match on some groups, we return the credential with filtered groups
return some(MembershipCredentials(identityCredential: credential.identityCredential,
membershipGroups: membershipGroupsIntersection))
else:
return none(MembershipCredentials)
# 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 some(credential)

View File

@ -1,6 +1,9 @@
import
stint
import
../waku_keystore
# Acceptable roots for merkle root validation of incoming messages
const AcceptableRootWindowSize* = 5
@ -48,3 +51,9 @@ const MaxClockGapSeconds* = 20.0 # the maximum clock difference between peers in
# maximum allowed gap between the epochs of messages' RateLimitProofs
const MaxEpochGap* = uint64(MaxClockGapSeconds/EpochUnitSeconds)
# RLN Keystore defaults
const
RLNAppInfo* = AppInfo(application: "nwaku-rln-relay", appIdentifier: "01234567890abcdef", version: "0.1")
# NOTE: 256-bytes long credentials are due to the use of BN254 in RLN. Other implementations/curves might have a different byte size
CredentialByteSize* = 256

View File

@ -10,9 +10,10 @@ import
stew/[arrayops, results, endians2],
stint
import
./constants,
./protocol_types
import
../../utils/keyfile
../waku_keystore
export
web3,
@ -27,16 +28,9 @@ proc toUInt256*(idCommitment: IDCommitment): UInt256 =
return pk
proc toIDCommitment*(idCommitmentUint: UInt256): IDCommitment =
let pk = IDCommitment(idCommitmentUint.toBytesLE())
let pk = IDCommitment(@(idCommitmentUint.toBytesLE()))
return pk
proc inHex*(value: array[32, byte]): string =
var valueHex = (UInt256.fromBytesLE(value)).toHex()
# We pad leading zeroes
while valueHex.len < value.len * 2:
valueHex = "0" & valueHex
return valueHex
proc toMembershipIndex*(v: UInt256): MembershipIndex =
let membershipIndex: MembershipIndex = cast[MembershipIndex](v)
return membershipIndex
@ -115,10 +109,10 @@ proc toIdentityCredentials*(groupKeys: seq[(string, string, string, string)]): R
for i in 0..groupKeys.len-1:
try:
let
idTrapdoor = hexToUint[IdentityTrapdoor.len*8](groupKeys[i][0]).toBytesLE()
idNullifier = hexToUint[IdentityNullifier.len*8](groupKeys[i][1]).toBytesLE()
idSecretHash = hexToUint[IdentitySecretHash.len*8](groupKeys[i][2]).toBytesLE()
idCommitment = hexToUint[IDCommitment.len*8](groupKeys[i][3]).toBytesLE()
idTrapdoor = IdentityTrapdoor(@(hexToUint[CredentialByteSize](groupKeys[i][0]).toBytesLE()))
idNullifier = IdentityNullifier(@(hexToUint[CredentialByteSize](groupKeys[i][1]).toBytesLE()))
idSecretHash = IdentitySecretHash(@(hexToUint[CredentialByteSize](groupKeys[i][2]).toBytesLE()))
idCommitment = IDCommitment(@(hexToUint[CredentialByteSize](groupKeys[i][3]).toBytesLE()))
groupIdCredentials.add(IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash,
idCommitment: idCommitment))
except ValueError as err:
@ -138,8 +132,8 @@ proc toIdentityCredentials*(groupKeys: seq[(string, string)]): RlnRelayResult[se
for i in 0..groupKeys.len-1:
try:
let
idSecretHash = hexToUint[IdentitySecretHash.len*8](groupKeys[i][0]).toBytesLE()
idCommitment = hexToUint[IDCommitment.len*8](groupKeys[i][1]).toBytesLE()
idSecretHash = IdentitySecretHash(@(hexToUint[CredentialByteSize](groupKeys[i][0]).toBytesLE()))
idCommitment = IDCommitment(@(hexToUint[CredentialByteSize](groupKeys[i][1]).toBytesLE()))
groupIdCredentials.add(IdentityCredential(idSecretHash: idSecretHash,
idCommitment: idCommitment))
except ValueError as err:

View File

@ -10,6 +10,7 @@ import
web3,
eth/keys
import
../waku_keystore,
../../../common/protobuf
type RlnRelayResult*[T] = Result[T, string]
@ -19,12 +20,6 @@ type RLN* {.incompleteStruct.} = object
type RLNResult* = RlnRelayResult[ptr RLN]
type
IdentityTrapdoor* = array[32, byte]
IdentityNullifier* = array[32, byte]
# identity key as defined in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
IdentitySecretHash* = array[32, byte]
# hash of identity key as defined ed in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
IDCommitment* = array[32, byte]
MerkleNode* = array[32, byte] # Each node of the Merkle tee is a Poseidon hash which is a 32 byte value
Nullifier* = array[32, byte]
Epoch* = array[32, byte]
@ -32,17 +27,6 @@ type
ZKSNARK* = array[128, byte]
# Custom data types defined for waku rln relay -------------------------
type IdentityCredential* = object
idTrapdoor*: IdentityTrapdoor
idNullifier*: IdentityNullifier
## user's identity key (a secret key) which is selected randomly
## see details in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
idSecretHash*: IdentitySecretHash
# hash of user's identity key generated by
# Poseidon hash function implemented in rln lib
# more details in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
idCommitment*: IDCommitment
type RateLimitProof* = object
## RateLimitProof holds the public inputs to rln circuit as
## defined in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Public-Inputs
@ -63,12 +47,6 @@ type RateLimitProof* = object
## Application specific RLN Identifier
rlnIdentifier*: RlnIdentifier
type MembershipIndex* = uint
type RlnMembershipCredentials* = object
identityCredential*: IdentityCredential
rlnIndex*: MembershipIndex
type ProofMetadata* = object
nullifier*: Nullifier
shareX*: MerkleNode

View File

@ -11,6 +11,7 @@ import
../protocol_metrics,
../constants
import
../../waku_keystore,
../../../utils/time
logScope:
@ -47,7 +48,7 @@ proc membershipKeyGen*(ctxPtr: ptr RLN): RlnRelayResult[IdentityCredential] =
for (i, x) in idCommitment.mpairs: x = generatedKeys[i+3*32]
var
identityCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment)
identityCredential = IdentityCredential(idTrapdoor: @idTrapdoor, idNullifier: @idNullifier, idSecretHash: @idSecretHash, idCommitment: @idCommitment)
return ok(identityCredential)

View File

@ -4,7 +4,7 @@ else:
{.push raises: [].}
import
std/[sequtils, tables, times, os, deques],
std/[algorithm, sequtils, strutils, tables, times, os, deques],
chronicles, options, chronos, stint,
confutils,
strutils,
@ -23,7 +23,7 @@ import
./protocol_metrics
import
../../utils/time,
../../utils/keyfile,
../waku_keystore,
../waku_message,
../waku_relay
@ -50,7 +50,7 @@ type
MembershipTuple* = tuple[index: MembershipIndex, idComm: IDCommitment]
# membership contract interface
contract(MembershipContract):
contract(MembershipContractInterface):
proc register(pubkey: Uint256) # external payable
proc MemberRegistered(pubkey: Uint256, index: Uint256) {.event.}
# TODO the followings are to be supported
@ -58,22 +58,21 @@ contract(MembershipContract):
# proc withdraw(secret: Uint256, pubkeyIndex: Uint256, receiver: Address)
# proc withdrawBatch( secrets: seq[Uint256], pubkeyIndex: seq[Uint256], receiver: seq[Address])
proc inHex*(value:
IdentityTrapdoor or
proc inHex*(value: IdentityTrapdoor or
IdentityNullifier or
IdentitySecretHash or
IDCommitment or
MerkleNode or
Nullifier or
Epoch or
RlnIdentifier
): string =
var valueHex = UInt256.fromBytesLE(value)
valueHex = valueHex.toHex()
RlnIdentifier): string =
var valueHex = "" #UInt256.fromBytesLE(value)
for b in value.reversed():
valueHex = valueHex & b.toHex()
# We pad leading zeroes
while valueHex.len < value.len * 2:
valueHex = "0" & valueHex
return valueHex
return toLowerAscii(valueHex)
proc register*(idComm: IDCommitment, ethAccountAddress: Option[Address], ethAccountPrivKey: keys.PrivateKey, ethClientAddress: string, membershipContractAddress: Address, registrationHandler: Option[RegistrationHandler] = none(RegistrationHandler)): Future[Result[MembershipIndex, string]] {.async.} =
# TODO may need to also get eth Account Private Key as PrivateKey
@ -95,7 +94,7 @@ proc register*(idComm: IDCommitment, ethAccountAddress: Option[Address], ethAcco
# when the private key is set in a web3 instance, the send proc (sender.register(pk).send(MembershipFee))
# does the signing using the provided key
# web3.privateKey = some(ethAccountPrivateKey)
var sender = web3.contractSender(MembershipContract, membershipContractAddress) # creates a Sender object with a web3 field and contract address of type Address
var sender = web3.contractSender(MembershipContractInterface, membershipContractAddress) # creates a Sender object with a web3 field and contract address of type Address
debug "registering an id commitment", idComm=idComm.inHex()
let pk = idComm.toUInt256()
@ -563,7 +562,7 @@ proc getHistoricalEvents*(ethClientUri: string,
## returns a table that maps block numbers to the list of members registered in that block
## returns an error if it cannot retrieve the historical events
let web3 = await newWeb3(ethClientUri)
let contract = web3.contractSender(MembershipContract, contractAddress)
let contract = web3.contractSender(MembershipContractInterface, contractAddress)
# Get the historical events, and insert memberships into the tree
let historicalEvents = await contract.getJsonLogs(MemberRegistered,
fromBlock=some(fromBlock.blockId()),
@ -592,11 +591,11 @@ proc subscribeToGroupEvents*(ethClientUri: string,
blockNumber: string = "0x0",
handler: GroupUpdateHandler) {.async, gcsafe.} =
## connects to the eth client whose URI is supplied as `ethClientUri`
## subscribes to the `MemberRegistered` event emitted from the `MembershipContract` which is available on the supplied `contractAddress`
## subscribes to the `MemberRegistered` event emitted from the `MembershipContractInterface` which is available on the supplied `contractAddress`
## it collects all the events starting from the given `blockNumber`
## for every received block, it calls the `handler`
let web3 = await newWeb3(ethClientUri)
let contract = web3.contractSender(MembershipContract, contractAddress)
let contract = web3.contractSender(MembershipContractInterface, contractAddress)
let blockTableRes = await getHistoricalEvents(ethClientUri,
contractAddress,
@ -730,7 +729,7 @@ proc mountRlnRelayStatic*(wakuRelay: WakuRelay,
debug "mounting rln-relay in off-chain/static mode"
# check the peer's index and the inclusion of user's identity commitment in the group
if not memIdCredential.idCommitment == group[int(memIndex)]:
if memIdCredential.idCommitment != group[int(memIndex)]:
return err("The peer's index is not consistent with the group")
# create an RLN instance
@ -832,51 +831,6 @@ proc mountRlnRelayDynamic*(wakuRelay: WakuRelay,
return ok(rlnPeer)
proc writeRlnCredentials*(path: string,
credentials: RlnMembershipCredentials,
password: string): RlnRelayResult[void] =
# Returns RlnRelayResult[void], which indicates the success of the call
info "Storing RLN credentials"
var jsonString: string
jsonString.toUgly(%credentials)
let keyfile = createKeyFileJson(toBytes(jsonString), password)
if keyfile.isErr():
return err("Error while creating keyfile for RLN credentials")
if saveKeyFile(path, keyfile.get()).isErr():
return err("Error while saving keyfile for RLN credentials")
return ok()
# Attempts decryptions of all keyfiles with the provided password.
# If one or more credentials are successfully decrypted, the max(min(index,number_decrypted),0)-th is returned.
proc readRlnCredentials*(path: string,
password: string,
index: int = 0): RlnRelayResult[Option[RlnMembershipCredentials]] =
# Returns RlnRelayResult[Option[RlnMembershipCredentials]], which indicates the success of the call
info "Reading RLN credentials"
# With regards to printing the keys, it is purely for debugging purposes so that the user becomes explicitly aware of the current keys in use when nwaku is started.
# Note that this is only until the RLN contract being used is the one deployed on Goerli testnet.
# These prints need to omitted once RLN contract is deployed on Ethereum mainnet and using valuable funds for staking.
waku_rln_membership_credentials_import_duration_seconds.nanosecondTime:
try:
var decodedKeyfiles = loadKeyFiles(path, password)
if decodedKeyfiles.isOk():
var decodedRlnCredentials = decodedKeyfiles.get()
debug "Successfully decrypted keyfiles for the provided password", numberKeyfilesDecrypted=decodedRlnCredentials.len
# We should return the index-th decrypted credential, but we ensure to not overflow
let credentialIndex = max(min(index, decodedRlnCredentials.len - 1), 0)
debug "Picking credential with (adjusted) index", inputIndex=index, adjustedIndex=credentialIndex
let jsonObject = parseJson(string.fromBytes(decodedRlnCredentials[credentialIndex].get()))
let deserializedRlnCredentials = to(jsonObject, RlnMembershipCredentials)
debug "Deserialized RLN credentials", rlnCredentials=deserializedRlnCredentials
return ok(some(deserializedRlnCredentials))
else:
debug "Unable to decrypt RLN credentials with provided password. ", error=decodedKeyfiles.error
return ok(none(RlnMembershipCredentials))
except:
return err("Error while loading keyfile for RLN credentials at " & path)
proc mount(wakuRelay: WakuRelay,
conf: WakuRlnConfig,
spamHandler: Option[SpamHandler] = none(SpamHandler),
@ -943,10 +897,14 @@ proc mount(wakuRelay: WakuRelay,
return err("invalid eth contract address: " & err.msg)
var ethAccountPrivKeyOpt = none(keys.PrivateKey)
var ethAccountAddressOpt = none(Address)
var credentials = none(RlnMembershipCredentials)
var credentials = none(MembershipCredentials)
var rlnRelayRes: RlnRelayResult[WakuRlnRelay]
var rlnRelayCredPath: string
var persistCredentials: bool = false
# The RLN membership contract
let rlnMembershipContract = MembershipContract(chainId: "5", # This is Goerli ChainID. TODO: pass chainId to web3 as config option
address: conf.rlnRelayEthContractAddress)
if conf.rlnRelayEthAccountPrivateKey != "":
ethAccountPrivKeyOpt = some(keys.PrivateKey(SkSecretKey.fromHex(conf.rlnRelayEthAccountPrivateKey).value))
@ -973,12 +931,23 @@ proc mount(wakuRelay: WakuRelay,
info "A RLN credential file exists in provided path", path=rlnRelayCredPath
# retrieve rln-relay credential
let readCredentialsRes = readRlnCredentials(rlnRelayCredPath, conf.rlnRelayCredentialsPassword)
waku_rln_membership_credentials_import_duration_seconds.nanosecondTime:
let readCredentialsRes = getMembershipCredentials(path = rlnRelayCredPath,
password = conf.rlnRelayCredentialsPassword,
filterMembershipContracts = @[rlnMembershipContract],
# TODO: the following can be embedded in conf
appInfo = RLNAppInfo)
if readCredentialsRes.isErr():
return err("RLN credentials cannot be read: " & readCredentialsRes.error())
if readCredentialsRes.isErr():
return err("RLN credentials cannot be read")
credentials = readCredentialsRes.get()
# getMembershipCredentials returns all credentials in keystore as sequence matching the filter
let allMatchingCredentials = readCredentialsRes.get()
# if any is found, we return the first credential, otherwise credentials is none
if allMatchingCredentials.len() > 0:
credentials = some(allMatchingCredentials[0])
else:
credentials = none(MembershipCredentials)
else: # there is no credential file available in the supplied path
# mount the rln-relay protocol leaving rln-relay credentials arguments unassigned
@ -997,7 +966,7 @@ proc mount(wakuRelay: WakuRelay,
spamHandler = spamHandler,
registrationHandler = registrationHandler,
memIdCredential = some(credentials.get().identityCredential),
memIndex = some(credentials.get().rlnIndex))
memIndex = some(credentials.get().membershipGroups[0].treeIndex)) # TODO: use a proper proc to get a certain membership index
else:
# mount rln-relay in on-chain mode, with the provided private key
rlnRelayRes = await mountRlnRelayDynamic(wakuRelay,
@ -1030,10 +999,16 @@ proc mount(wakuRelay: WakuRelay,
return err("dynamic rln-relay could not be mounted: " & rlnRelayRes.error())
let wakuRlnRelay = rlnRelayRes.get()
if persistCredentials:
# persist rln credential
credentials = some(RlnMembershipCredentials(rlnIndex: wakuRlnRelay.membershipIndex,
identityCredential: wakuRlnRelay.identityCredential))
if writeRlnCredentials(rlnRelayCredPath, credentials.get(), conf.rlnRelayCredentialsPassword).isErr():
credentials = some(MembershipCredentials(identityCredential: wakuRlnRelay.identityCredential,
membershipGroups: @[MembershipGroup(membershipContract: rlnMembershipContract, treeIndex: wakuRlnRelay.membershipIndex)]
))
if addMembershipCredentials(path = rlnRelayCredPath,
credentials = @[credentials.get()],
password = conf.rlnRelayCredentialsPassword,
# TODO: the following can be embedded in conf
appInfo = RLNAppInfo).isErr():
return err("error in storing rln credentials")
return ok(wakuRlnRelay)