fix(rln-relay): modify keystore credentials logic (#1956)

* fix(rln-relay): modify keystore credentials logic

fix: bump version

* Update waku/waku_rln_relay/group_manager/on_chain/group_manager.nim

Co-authored-by: Ivan Folgueira Bande <128452529+Ivansete-status@users.noreply.github.com>

* Update tests/waku_rln_relay/test_waku_rln_relay.nim

Co-authored-by: Ivan Folgueira Bande <128452529+Ivansete-status@users.noreply.github.com>

* Update waku/waku_keystore/protocol_types.nim

Co-authored-by: Ivan Folgueira Bande <128452529+Ivansete-status@users.noreply.github.com>

* fix: greatly improve error handling

* fix: display proc and appropriate assert

---------

Co-authored-by: Ivan Folgueira Bande <128452529+Ivansete-status@users.noreply.github.com>
This commit is contained in:
Aaryamann Challani 2023-08-29 17:46:21 +05:30 committed by GitHub
parent ac25855018
commit e7b2b88f5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 199 additions and 316 deletions

View File

@ -508,11 +508,10 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} =
let rlnConf = WakuRlnConfig( let rlnConf = WakuRlnConfig(
rlnRelayDynamic: conf.rlnRelayDynamic, rlnRelayDynamic: conf.rlnRelayDynamic,
rlnRelayCredIndex: conf.rlnRelayCredIndex, rlnRelayCredIndex: conf.rlnRelayCredIndex,
rlnRelayMembershipGroupIndex: conf.rlnRelayMembershipGroupIndex,
rlnRelayEthContractAddress: conf.rlnRelayEthContractAddress, rlnRelayEthContractAddress: conf.rlnRelayEthContractAddress,
rlnRelayEthClientAddress: conf.rlnRelayEthClientAddress, rlnRelayEthClientAddress: conf.rlnRelayEthClientAddress,
rlnRelayCredPath: conf.rlnRelayCredPath, rlnRelayCredPath: conf.rlnRelayCredPath,
rlnRelayCredentialsPassword: conf.rlnRelayCredentialsPassword rlnRelayCredPassword: conf.rlnRelayCredPassword
) )
waitFor node.mountRlnRelay(rlnConf, waitFor node.mountRlnRelay(rlnConf,

View File

@ -237,11 +237,6 @@ type
defaultValue: 0 defaultValue: 0
name: "rln-relay-cred-index" }: uint name: "rln-relay-cred-index" }: uint
rlnRelayMembershipGroupIndex* {.
desc: "the index of credentials to use, within a specific rln membership set",
defaultValue: 0
name: "rln-relay-membership-group-index" }: uint
rlnRelayDynamic* {. rlnRelayDynamic* {.
desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false", desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false",
defaultValue: false defaultValue: false
@ -267,7 +262,7 @@ type
defaultValue: "" defaultValue: ""
name: "rln-relay-eth-contract-address" }: string name: "rln-relay-eth-contract-address" }: string
rlnRelayCredentialsPassword* {. rlnRelayCredPassword* {.
desc: "Password for encrypting RLN credentials", desc: "Password for encrypting RLN credentials",
defaultValue: "" defaultValue: ""
name: "rln-relay-cred-password" }: string name: "rln-relay-cred-password" }: string

View File

@ -398,11 +398,10 @@ proc setupProtocols(node: WakuNode,
let rlnConf = WakuRlnConfig( let rlnConf = WakuRlnConfig(
rlnRelayDynamic: conf.rlnRelayDynamic, rlnRelayDynamic: conf.rlnRelayDynamic,
rlnRelayCredIndex: conf.rlnRelayCredIndex, rlnRelayCredIndex: conf.rlnRelayCredIndex,
rlnRelayMembershipGroupIndex: conf.rlnRelayMembershipGroupIndex,
rlnRelayEthContractAddress: conf.rlnRelayEthContractAddress, rlnRelayEthContractAddress: conf.rlnRelayEthContractAddress,
rlnRelayEthClientAddress: conf.rlnRelayEthClientAddress, rlnRelayEthClientAddress: conf.rlnRelayEthClientAddress,
rlnRelayCredPath: conf.rlnRelayCredPath, rlnRelayCredPath: conf.rlnRelayCredPath,
rlnRelayCredentialsPassword: conf.rlnRelayCredentialsPassword, rlnRelayCredPassword: conf.rlnRelayCredPassword,
rlnRelayTreePath: conf.rlnRelayTreePath, rlnRelayTreePath: conf.rlnRelayTreePath,
rlnRelayBandwidthThreshold: conf.rlnRelayBandwidthThreshold rlnRelayBandwidthThreshold: conf.rlnRelayBandwidthThreshold
) )

View File

@ -150,11 +150,6 @@ type
defaultValue: 0 defaultValue: 0
name: "rln-relay-membership-index" }: uint name: "rln-relay-membership-index" }: uint
rlnRelayMembershipGroupIndex* {.
desc: "the index of credentials to use, within a specific rln membership set",
defaultValue: 0
name: "rln-relay-membership-group-index" }: uint
rlnRelayDynamic* {. rlnRelayDynamic* {.
desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false", desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false",
defaultValue: false defaultValue: false
@ -180,7 +175,7 @@ type
defaultValue: "" defaultValue: ""
name: "rln-relay-eth-contract-address" }: string name: "rln-relay-eth-contract-address" }: string
rlnRelayCredentialsPassword* {. rlnRelayCredPassword* {.
desc: "Password for encrypting RLN credentials", desc: "Password for encrypting RLN credentials",
defaultValue: "" defaultValue: ""
name: "rln-relay-cred-password" }: string name: "rln-relay-cred-password" }: string

View File

@ -1,8 +1,9 @@
{.used.} {.used.}
import import
std/[algorithm, json, options, os], std/[os, json],
testutils/unittests, chronos, stint chronos,
testutils/unittests
import import
../../waku/waku_keystore, ../../waku/waku_keystore,
./testlib/common ./testlib/common
@ -44,7 +45,7 @@ procSuite "Credentials test suite":
keystore["appIdentifier"].getStr() == testAppInfo.appIdentifier keystore["appIdentifier"].getStr() == testAppInfo.appIdentifier
keystore["version"].getStr() == testAppInfo.version keystore["version"].getStr() == testAppInfo.version
# We assume the loaded keystore to not have credentials set (previous tests delete the keystore at filepath) # We assume the loaded keystore to not have credentials set (previous tests delete the keystore at filepath)
keystore["credentials"].getElems().len() == 0 keystore["credentials"].len() == 0
test "Add credentials to keystore": test "Add credentials to keystore":
@ -61,30 +62,15 @@ procSuite "Credentials test suite":
var idCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment) var idCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment)
var contract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789") var contract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789")
var index1 = MembershipIndex(1) var index = 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 membershipCredential = KeystoreMembership(membershipContract: contract,
treeIndex: index,
identityCredential: idCredential)
let password = "%m0um0ucoW%" let password = "%m0um0ucoW%"
let keystoreRes = addMembershipCredentials(path = filepath, let keystoreRes = addMembershipCredentials(path = filepath,
credentials = @[membershipCredentials1, membershipCredentials2], membership = membershipCredential,
password = password, password = password,
appInfo = testAppInfo) appInfo = testAppInfo)
@ -98,47 +84,25 @@ procSuite "Credentials test suite":
# We generate two random identity credentials (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen) # We generate two random identity credentials (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen)
var var
idTrapdoor1 = randomSeqByte(rng[], 32) idTrapdoor = randomSeqByte(rng[], 32)
idNullifier1 = randomSeqByte(rng[], 32) idNullifier = randomSeqByte(rng[], 32)
idSecretHash1 = randomSeqByte(rng[], 32) idSecretHash = randomSeqByte(rng[], 32)
idCommitment1 = randomSeqByte(rng[], 32) idCommitment = randomSeqByte(rng[], 32)
idCredential1 = IdentityCredential(idTrapdoor: idTrapdoor1, idNullifier: idNullifier1, idSecretHash: idSecretHash1, idCommitment: idCommitment1) idCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment)
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 # We generate two distinct membership groups
var contract1 = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789") var contract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789")
var index1 = MembershipIndex(1) var index = MembershipIndex(1)
var membershipGroup1 = MembershipGroup(membershipContract: contract1, treeIndex: index1) var membershipCredential = KeystoreMembership(membershipContract: contract,
treeIndex: index,
identityCredential: idCredential)
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%" let password = "%m0um0ucoW%"
# We add credentials to the keystore. Note that only 3 credentials should be effectively added, since rlnMembershipCredentials3 is equal to membershipCredentials2 # 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, let keystoreRes = addMembershipCredentials(path = filepath,
credentials = @[membershipCredentials1, membershipCredentials2, membershipCredentials3, membershipCredentials4], membership = membershipCredential,
password = password, password = password,
appInfo = testAppInfo) appInfo = testAppInfo)
@ -146,45 +110,16 @@ procSuite "Credentials test suite":
keystoreRes.isOk() keystoreRes.isOk()
# We test retrieval of credentials. # We test retrieval of credentials.
var expectedMembershipGroups1 = @[membershipGroup1, membershipGroup2] var expectedMembership = membershipCredential
expectedMembershipGroups1.sort(sortMembershipGroup) let membershipQuery = KeystoreMembership(membershipContract: contract,
let expectedCredential1 = MembershipCredentials(identityCredential: idCredential1, treeIndex: index)
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, var recoveredCredentialsRes = getMembershipCredentials(path = filepath,
password = password, password = password,
query = membershipQuery,
appInfo = testAppInfo) appInfo = testAppInfo)
check: check:
recoveredCredentialsRes.isOk() recoveredCredentialsRes.isOk()
recoveredCredentialsRes.get() == @[expectedCredential1, expectedCredential2] recoveredCredentialsRes.get() == expectedMembership
# 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

@ -815,10 +815,14 @@ suite "Waku rln relay":
let index = MembershipIndex(1) let index = MembershipIndex(1)
let rlnMembershipContract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789") let keystoreMembership = KeystoreMembership(
let rlnMembershipGroup = MembershipGroup(membershipContract: rlnMembershipContract, treeIndex: index) membershipContract: MembershipContract(
let rlnMembershipCredentials = MembershipCredentials(identityCredential: idCredential, membershipGroups: @[rlnMembershipGroup]) chainId: "5",
address: "0x0123456789012345678901234567890123456789"
),
treeIndex: index,
identityCredential: idCredential,
)
let password = "%m0um0ucoW%" let password = "%m0um0ucoW%"
let filepath = "./testRLNCredentials.txt" let filepath = "./testRLNCredentials.txt"
@ -827,30 +831,29 @@ suite "Waku rln relay":
# Write RLN credentials # Write RLN credentials
require: require:
addMembershipCredentials(path = filepath, addMembershipCredentials(path = filepath,
credentials = @[rlnMembershipCredentials], membership = keystoreMembership,
password = password, password = password,
appInfo = RLNAppInfo).isOk() appInfo = RLNAppInfo).isOk()
let readCredentialsResult = getMembershipCredentials(path = filepath, let readKeystoreRes = getMembershipCredentials(path = filepath,
password = password, password = password,
filterMembershipContracts = @[rlnMembershipContract], # here the query would not include
# the identityCredential,
# since it is not part of the query
# but have used the same value
# to avoid re-declaration
query = keystoreMembership,
appInfo = RLNAppInfo) appInfo = RLNAppInfo)
assert readKeystoreRes.isOk(), $readKeystoreRes.error
require: # getMembershipCredentials returns the credential in the keystore which matches
readCredentialsResult.isOk() # the query, in this case the query is =
# chainId = "5" and
# getMembershipCredentials returns all credentials in keystore as sequence matching the filter # address = "0x0123456789012345678901234567890123456789" and
let allMatchingCredentials = readCredentialsResult.get() # treeIndex = 1
# if any is found, we return the first credential, otherwise credentials is none let readKeystoreMembership = readKeystoreRes.get()
var credentials = none(MembershipCredentials)
if allMatchingCredentials.len() > 0:
credentials = some(allMatchingCredentials[0])
require:
credentials.isSome()
check: check:
credentials.get().identityCredential == idCredential readKeystoreMembership == keystoreMembership
credentials.get().membershipGroups == @[rlnMembershipGroup]
test "histogram static bucket generation": test "histogram static bucket generation":
let buckets = generateBucketsForHistogram(10) let buckets = generateBucketsForHistogram(10)

View File

@ -79,19 +79,17 @@ when isMainModule:
debug "Transaction hash", txHash = groupManager.registrationTxHash.get() debug "Transaction hash", txHash = groupManager.registrationTxHash.get()
# 6. write to keystore # 6. write to keystore
let keystoreCred = MembershipCredentials( let keystoreCred = KeystoreMembership(
membershipContract: MembershipContract(
chainId: $groupManager.chainId.get(),
address: conf.rlnRelayEthContractAddress,
),
treeIndex: groupManager.membershipIndex.get(),
identityCredential: credential, identityCredential: credential,
membershipGroups: @[MembershipGroup(
membershipContract: MembershipContract(
chainId: $groupManager.chainId.get(),
address: conf.rlnRelayEthContractAddress,
),
treeIndex: groupManager.membershipIndex.get(),
)]
) )
let persistRes = addMembershipCredentials(conf.rlnRelayCredPath, let persistRes = addMembershipCredentials(conf.rlnRelayCredPath,
@[keystoreCred], keystoreCred,
conf.rlnRelayCredPassword, conf.rlnRelayCredPassword,
RLNAppInfo) RLNAppInfo)
if persistRes.isErr(): if persistRes.isErr():

View File

@ -8,22 +8,24 @@ import
stew/[results, byteutils], stew/[results, byteutils],
./protocol_types ./protocol_types
# Encodes a Membership credential to a byte sequence # Encodes a KeystoreMembership credential to a byte sequence
proc encode*(credential: MembershipCredentials): seq[byte] = proc encode*(credential: KeystoreMembership): seq[byte] =
# TODO: use custom encoding, avoid wordy json # TODO: use custom encoding, avoid wordy json
var stringCredential: string var stringCredential: string
# NOTE: toUgly appends to the string, doesn't replace its contents # NOTE: toUgly appends to the string, doesn't replace its contents
stringCredential.toUgly(%credential) stringCredential.toUgly(%credential)
return toBytes(stringCredential) return toBytes(stringCredential)
# Decodes a byte sequence to a Membership credential # Decodes a byte sequence to a KeystoreMembership credential
proc decode*(encodedCredential: seq[byte]): KeystoreResult[MembershipCredentials] = proc decode*(encodedCredential: seq[byte]): KeystoreResult[KeystoreMembership] =
# TODO: use custom decoding, avoid wordy json # TODO: use custom decoding, avoid wordy json
try: try:
# we parse the json decrypted keystoreCredential # we parse the json decrypted keystoreCredential
let jsonObject = parseJson(string.fromBytes(encodedCredential)) let jsonObject = parseJson(string.fromBytes(encodedCredential))
return ok(to(jsonObject, MembershipCredentials)) return ok(to(jsonObject, KeystoreMembership))
except JsonParsingError: except JsonParsingError:
return err(KeystoreJsonError) return err(AppKeystoreError(kind: KeystoreJsonError,
msg: getCurrentExceptionMsg()))
except Exception: #parseJson raises Exception except Exception: #parseJson raises Exception
return err(KeystoreOsError) return err(AppKeystoreError(kind: KeystoreOsError,
msg: getCurrentExceptionMsg()))

View File

@ -5,7 +5,7 @@ else:
import import
options, json, strutils, options, json, strutils,
std/[algorithm, os, sequtils, sets] std/[tables, os]
import import
./keyfile, ./keyfile,
@ -20,15 +20,16 @@ proc createAppKeystore*(path: string,
let keystore = AppKeystore(application: appInfo.application, let keystore = AppKeystore(application: appInfo.application,
appIdentifier: appInfo.appIdentifier, appIdentifier: appInfo.appIdentifier,
credentials: @[], version: appInfo.version,
version: appInfo.version) credentials: initTable[string, KeystoreMembership]())
var jsonKeystore: string var jsonKeystore: string
jsonKeystore.toUgly(%keystore) jsonKeystore.toUgly(%keystore)
var f: File var f: File
if not f.open(path, fmWrite): if not f.open(path, fmWrite):
return err(KeystoreOsError) return err(AppKeystoreError(kind: KeystoreOsError,
msg: "Cannot open file for writing"))
try: try:
# To avoid other users/attackers to be able to read keyfiles, we make the file readable/writable only by the running user # To avoid other users/attackers to be able to read keyfiles, we make the file readable/writable only by the running user
@ -38,7 +39,8 @@ proc createAppKeystore*(path: string,
f.write(separator) f.write(separator)
ok() ok()
except CatchableError: except CatchableError:
err(KeystoreOsError) err(AppKeystoreError(kind: KeystoreOsError,
msg: getCurrentExceptionMsg()))
finally: finally:
f.close() f.close()
@ -54,16 +56,17 @@ proc loadAppKeystore*(path: string,
# If no keystore exists at path we create a new empty one with passed keystore parameters # If no keystore exists at path we create a new empty one with passed keystore parameters
if fileExists(path) == false: if fileExists(path) == false:
let newKeystore = createAppKeystore(path, appInfo, separator) let newKeystoreRes = createAppKeystore(path, appInfo, separator)
if newKeystore.isErr(): if newKeystoreRes.isErr():
return err(KeystoreCreateKeystoreError) return err(newKeystoreRes.error)
try: try:
# We read all the file contents # We read all the file contents
var f: File var f: File
if not f.open(path, fmRead): if not f.open(path, fmRead):
return err(KeystoreOsError) return err(AppKeystoreError(kind: KeystoreOsError,
msg: "Cannot open file for reading"))
let fileContents = readAll(f) let fileContents = readAll(f)
# We iterate over each substring split by separator (which we expect to correspond to a single keystore json) # We iterate over each substring split by separator (which we expect to correspond to a single keystore json)
@ -92,23 +95,28 @@ proc loadAppKeystore*(path: string,
break break
# TODO: we might continue rather than return for some of these errors # TODO: we might continue rather than return for some of these errors
except JsonParsingError: except JsonParsingError:
return err(KeystoreJsonError) return err(AppKeystoreError(kind: KeystoreJsonError,
msg: getCurrentExceptionMsg()))
except ValueError: except ValueError:
return err(KeystoreJsonError) return err(AppKeystoreError(kind: KeystoreJsonError,
msg: getCurrentExceptionMsg()))
except OSError: except OSError:
return err(KeystoreOsError) return err(AppKeystoreError(kind: KeystoreOsError,
msg: getCurrentExceptionMsg()))
except Exception: #parseJson raises Exception except Exception: #parseJson raises Exception
return err(KeystoreOsError) return err(AppKeystoreError(kind: KeystoreOsError,
msg: getCurrentExceptionMsg()))
except IOError: except IOError:
return err(KeystoreIoError) return err(AppKeystoreError(kind: KeystoreIoError,
msg: getCurrentExceptionMsg()))
return ok(matchingAppKeystore) return ok(matchingAppKeystore)
# Adds a sequence of membership credential to the keystore matching the application, appIdentifier and version filters. # Adds a membership credential to the keystore matching the application, appIdentifier and version filters.
proc addMembershipCredentials*(path: string, proc addMembershipCredentials*(path: string,
credentials: seq[MembershipCredentials], membership: KeystoreMembership,
password: string, password: string,
appInfo: AppInfo, appInfo: AppInfo,
separator: string = "\n"): KeystoreResult[void] = separator: string = "\n"): KeystoreResult[void] =
@ -118,77 +126,38 @@ proc addMembershipCredentials*(path: string,
let jsonKeystoreRes = loadAppKeystore(path, appInfo, separator) let jsonKeystoreRes = loadAppKeystore(path, appInfo, separator)
if jsonKeystoreRes.isErr(): if jsonKeystoreRes.isErr():
return err(KeystoreLoadKeystoreError) return err(jsonKeystoreRes.error)
# We load the JSON node corresponding to the app keystore # We load the JSON node corresponding to the app keystore
var jsonKeystore = jsonKeystoreRes.get() var jsonKeystore = jsonKeystoreRes.get()
try: try:
if jsonKeystore.hasKey("credentials"): if jsonKeystore.hasKey("credentials"):
# We get all credentials in keystore # We get all credentials in keystore
var keystoreCredentials = jsonKeystore["credentials"] let keystoreCredentials = jsonKeystore["credentials"]
var found: bool let key = membership.hash()
if keystoreCredentials.hasKey(key):
# noop
return ok()
for membershipCredential in credentials: let encodedMembershipCredential = membership.encode()
let keyfileRes = createKeyFileJson(encodedMembershipCredential, password)
if keyfileRes.isErr():
return err(AppKeystoreError(kind: KeystoreCreateKeyfileError,
msg: $keyfileRes.error))
# A flag to tell us if the keystore contains a credential associated to the input identity credential, i.e. membershipCredential # We add it to the credentials field of the keystore
found = false jsonKeystore["credentials"][key] = keyfileRes.get()
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 CatchableError: except CatchableError:
return err(KeystoreJsonError) return err(AppKeystoreError(kind: KeystoreJsonError,
msg: getCurrentExceptionMsg()))
# We save to disk the (updated) keystore. # We save to disk the (updated) keystore.
if save(jsonKeystore, path, separator).isErr(): let saveRes = save(jsonKeystore, path, separator)
return err(KeystoreOsError) if saveRes.isErr():
return err(saveRes.error)
return ok() return ok()
@ -196,18 +165,15 @@ proc addMembershipCredentials*(path: string,
# identity credentials and membership contracts # identity credentials and membership contracts
proc getMembershipCredentials*(path: string, proc getMembershipCredentials*(path: string,
password: string, password: string,
filterIdentityCredentials: seq[IdentityCredential] = @[], query: KeystoreMembership,
filterMembershipContracts: seq[MembershipContract] = @[], appInfo: AppInfo): KeystoreResult[KeystoreMembership] =
appInfo: AppInfo): KeystoreResult[seq[MembershipCredentials]] =
var outputMembershipCredentials: seq[MembershipCredentials] = @[]
# We load the keystore corresponding to the desired parameters # We load the keystore corresponding to the desired parameters
# This call ensures that JSON has all required fields # This call ensures that JSON has all required fields
let jsonKeystoreRes = loadAppKeystore(path, appInfo) let jsonKeystoreRes = loadAppKeystore(path, appInfo)
if jsonKeystoreRes.isErr(): if jsonKeystoreRes.isErr():
return err(KeystoreLoadKeystoreError) return err(jsonKeystoreRes.error)
# We load the JSON node corresponding to the app keystore # We load the JSON node corresponding to the app keystore
var jsonKeystore = jsonKeystoreRes.get() var jsonKeystore = jsonKeystoreRes.get()
@ -215,27 +181,24 @@ proc getMembershipCredentials*(path: string,
try: try:
if jsonKeystore.hasKey("credentials"): if jsonKeystore.hasKey("credentials"):
# We get all credentials in keystore # We get all credentials in keystore
var keystoreCredentials = jsonKeystore["credentials"] var keystoreCredentials = jsonKeystore["credentials"]
let key = query.hash()
if not keystoreCredentials.hasKey(key):
# error
return err(AppKeystoreError(kind: KeystoreCredentialNotFoundError,
msg: "Credential not found in keystore"))
for keystoreCredential in keystoreCredentials.mitems(): let keystoreCredential = keystoreCredentials[key]
let decodedKeyfileRes = decodeKeyFileJson(keystoreCredential, password)
# keystoreCredential is encrypted. We decrypt it if decodedKeyfileRes.isErr():
let decodedKeyfileRes = decodeKeyFileJson(keystoreCredential, password) return err(AppKeystoreError(kind: KeystoreReadKeyfileError,
if decodedKeyfileRes.isOk(): msg: $decodedKeyfileRes.error))
# we parse the json decrypted keystoreCredential # we parse the json decrypted keystoreCredential
let decodedCredentialRes = decode(decodedKeyfileRes.get()) let decodedCredentialRes = decode(decodedKeyfileRes.get())
let keyfileMembershipCredential = decodedCredentialRes.get()
if decodedCredentialRes.isOk(): return ok(keyfileMembershipCredential)
let keyfileMembershipCredential = decodedCredentialRes.get()
let filteredCredentialOpt = filterCredential(keyfileMembershipCredential, filterIdentityCredentials, filterMembershipContracts)
if filteredCredentialOpt.isSome():
outputMembershipCredentials.add(filteredCredentialOpt.get())
except CatchableError: except CatchableError:
return err(KeystoreJsonError) return err(AppKeystoreError(kind: KeystoreJsonError,
msg: getCurrentExceptionMsg()))
return ok(outputMembershipCredentials)

View File

@ -4,8 +4,9 @@ else:
{.push raises: [].} {.push raises: [].}
import import
std/sequtils, std/[sequtils, tables],
stew/[results, endians2], stew/[results, endians2],
nimcrypto,
stint stint
# NOTE: 256-bytes long credentials are due to the use of BN254 in RLN. Other implementations/curves might have a different byte size # NOTE: 256-bytes long credentials are due to the use of BN254 in RLN. Other implementations/curves might have a different byte size
@ -88,13 +89,28 @@ type MembershipContract* = object
chainId*: string chainId*: string
address*: string address*: string
type MembershipGroup* = object type KeystoreMembership* = ref object of RootObj
membershipContract*: MembershipContract membershipContract*: MembershipContract
treeIndex*: MembershipIndex treeIndex*: MembershipIndex
identityCredential*: IdentityCredential
type MembershipCredentials* = object proc `$`*(m: KeystoreMembership): string =
identityCredential*: IdentityCredential return "KeystoreMembership(chainId: " & m.membershipContract.chainId & ", contractAddress: " & m.membershipContract.address & ", treeIndex: " & $m.treeIndex & ", identityCredential: " & $m.identityCredential & ")"
membershipGroups*: seq[MembershipGroup]
proc `==`*(x, y: KeystoreMembership): bool =
return x.membershipContract.chainId == y.membershipContract.chainId and
x.membershipContract.address == y.membershipContract.address and
x.treeIndex == y.treeIndex and
x.identityCredential.idTrapdoor == y.identityCredential.idTrapdoor and
x.identityCredential.idNullifier == y.identityCredential.idNullifier and
x.identityCredential.idSecretHash == y.identityCredential.idSecretHash and
x.identityCredential.idCommitment == y.identityCredential.idCommitment
proc hash*(m: KeystoreMembership): string =
# hash together the chainId, address and treeIndex
return $sha256.digest(m.membershipContract.chainId & m.membershipContract.address & $m.treeIndex)
type MembershipTable* = Table[string, KeystoreMembership]
type AppInfo* = object type AppInfo* = object
application*: string application*: string
@ -104,11 +120,11 @@ type AppInfo* = object
type AppKeystore* = object type AppKeystore* = object
application*: string application*: string
appIdentifier*: string appIdentifier*: string
credentials*: seq[MembershipCredentials] credentials*: MembershipTable
version*: string version*: string
type type
AppKeystoreError* = enum AppKeystoreErrorKind* = enum
KeystoreOsError = "keystore error: OS specific error" KeystoreOsError = "keystore error: OS specific error"
KeystoreIoError = "keystore error: IO specific error" KeystoreIoError = "keystore error: IO specific error"
KeystoreJsonKeyError = "keystore error: fields not present in JSON" KeystoreJsonKeyError = "keystore error: fields not present in JSON"
@ -119,5 +135,14 @@ type
KeystoreCreateKeyfileError = "Error while creating keyfile for credentials" KeystoreCreateKeyfileError = "Error while creating keyfile for credentials"
KeystoreSaveKeyfileError = "Error while saving keyfile for credentials" KeystoreSaveKeyfileError = "Error while saving keyfile for credentials"
KeystoreReadKeyfileError = "Error while reading keyfile for credentials" KeystoreReadKeyfileError = "Error while reading keyfile for credentials"
KeystoreCredentialAlreadyPresentError = "Error while adding credentials to keystore: credential already present"
KeystoreCredentialNotFoundError = "Error while searching credentials in keystore: credential not found"
AppKeystoreError* = object
kind*: AppKeystoreErrorKind
msg*: string
proc `$`*(e: AppKeystoreError) : string =
return $e.kind & ": " & e.msg
type KeystoreResult*[T] = Result[T, AppKeystoreError] type KeystoreResult*[T] = Result[T, AppKeystoreError]

View File

@ -5,7 +5,9 @@ else:
import import
json, json,
std/[options, os, sequtils], std/[os, sequtils]
import
./keyfile, ./keyfile,
./protocol_types ./protocol_types
@ -13,25 +15,22 @@ import
proc hasKeys*(data: JsonNode, keys: openArray[string]): bool = proc hasKeys*(data: JsonNode, keys: openArray[string]): bool =
return all(keys, proc (key: string): bool = return data.hasKey(key)) 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. # 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 # 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] = proc save*(json: JsonNode, path: string, separator: string): KeystoreResult[void] =
# We first backup the current keystore # We first backup the current keystore
if fileExists(path): if fileExists(path):
try: try:
moveFile(path, path & ".bkp") moveFile(path, path & ".bkp")
except: # TODO: Fix "BareExcept" warning except: # TODO: Fix "BareExcept" warning
return err(KeystoreOsError) return err(AppKeystoreError(kind: KeystoreOsError,
msg: "could not backup keystore: " & getCurrentExceptionMsg()))
# We save the updated json # We save the updated json
var f: File var f: File
if not f.open(path, fmAppend): if not f.open(path, fmAppend):
return err(KeystoreOsError) return err(AppKeystoreError(kind: KeystoreOsError,
msg: getCurrentExceptionMsg()))
try: try:
# To avoid other users/attackers to be able to read keyfiles, we make the file readable/writable only by the running user # 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}) setFilePermissions(path, {fpUserWrite, fpUserRead})
@ -47,8 +46,10 @@ proc save*(json: JsonNode, path: string, separator: string): KeystoreResult[void
moveFile(path & ".bkp", path) moveFile(path & ".bkp", path)
except: # TODO: Fix "BareExcept" warning except: # TODO: Fix "BareExcept" warning
# Unlucky, we just fail # Unlucky, we just fail
return err(KeystoreOsError) return err(AppKeystoreError(kind: KeystoreOsError,
return err(KeystoreOsError) msg: "could not restore keystore backup: " & getCurrentExceptionMsg()))
return err(AppKeystoreError(kind: KeystoreOsError,
msg: "could not write keystore: " & getCurrentExceptionMsg()))
finally: finally:
f.close() f.close()
@ -57,39 +58,7 @@ proc save*(json: JsonNode, path: string, separator: string): KeystoreResult[void
try: try:
removeFile(path & ".bkp") removeFile(path & ".bkp")
except CatchableError: except CatchableError:
return err(KeystoreOsError) return err(AppKeystoreError(kind: KeystoreOsError,
msg: "could not remove keystore backup: " & getCurrentExceptionMsg()))
return ok() 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

@ -53,4 +53,4 @@ const MaxEpochGap* = uint64(MaxClockGapSeconds/EpochUnitSeconds)
# RLN Keystore defaults # RLN Keystore defaults
const const
RLNAppInfo* = AppInfo(application: "waku-rln-relay", appIdentifier: "01234567890abcdef", version: "0.1") RLNAppInfo* = AppInfo(application: "waku-rln-relay", appIdentifier: "01234567890abcdef", version: "0.2")

View File

@ -58,8 +58,6 @@ type
registrationTxHash*: Option[TxHash] registrationTxHash*: Option[TxHash]
chainId*: Option[Quantity] chainId*: Option[Quantity]
keystorePath*: Option[string] keystorePath*: Option[string]
keystoreIndex*: uint
membershipGroupIndex*: uint
keystorePassword*: Option[string] keystorePassword*: Option[string]
registrationHandler*: Option[RegistrationHandler] registrationHandler*: Option[RegistrationHandler]
# this buffer exists to backfill appropriate roots for the merkle tree, # this buffer exists to backfill appropriate roots for the merkle tree,
@ -433,19 +431,24 @@ method init*(g: OnchainGroupManager): Future[void] {.async.} =
g.registryContract = some(registryContract) g.registryContract = some(registryContract)
if g.keystorePath.isSome() and g.keystorePassword.isSome(): if g.keystorePath.isSome() and g.keystorePassword.isSome():
if g.membershipIndex.isNone():
raise newException(CatchableError, "membership index is not set when keystore is provided")
let keystoreQuery = KeystoreMembership(
membershipContract: MembershipContract(
chainId: $g.chainId.get(),
address: g.ethContractAddress
),
treeIndex: MembershipIndex(g.membershipIndex.get()),
)
waku_rln_membership_credentials_import_duration_seconds.nanosecondTime: waku_rln_membership_credentials_import_duration_seconds.nanosecondTime:
let parsedCredsRes = getMembershipCredentials(path = g.keystorePath.get(), let keystoreCredRes = getMembershipCredentials(path = g.keystorePath.get(),
password = g.keystorePassword.get(), password = g.keystorePassword.get(),
filterMembershipContracts = @[MembershipContract(chainId: $chainId, query = keystoreQuery,
address: g.ethContractAddress)], appInfo = RLNAppInfo)
appInfo = RLNAppInfo) if keystoreCredRes.isErr():
if parsedCredsRes.isErr(): raise newException(ValueError, "could not parse the keystore: " & $keystoreCredRes.error)
raise newException(ValueError, "could not parse the keystore: " & $parsedCredsRes.error()) let keystoreCred = keystoreCredRes.get()
let parsedCreds = parsedCredsRes.get() g.idCredentials = some(keystoreCred.identityCredential)
if parsedCreds.len == 0:
raise newException(ValueError, "keystore is empty")
g.idCredentials = some(parsedCreds[g.keystoreIndex].identityCredential)
g.membershipIndex = some(parsedCreds[g.keystoreIndex].membershipGroups[g.membershipGroupIndex].treeIndex)
let metadataGetRes = g.rlnInstance.getMetadata() let metadataGetRes = g.rlnInstance.getMetadata()
if metadataGetRes.isErr(): if metadataGetRes.isErr():

View File

@ -32,11 +32,10 @@ logScope:
type WakuRlnConfig* = object type WakuRlnConfig* = object
rlnRelayDynamic*: bool rlnRelayDynamic*: bool
rlnRelayCredIndex*: uint rlnRelayCredIndex*: uint
rlnRelayMembershipGroupIndex*: uint
rlnRelayEthContractAddress*: string rlnRelayEthContractAddress*: string
rlnRelayEthClientAddress*: string rlnRelayEthClientAddress*: string
rlnRelayCredPath*: string rlnRelayCredPath*: string
rlnRelayCredentialsPassword*: string rlnRelayCredPassword*: string
rlnRelayTreePath*: string rlnRelayTreePath*: string
rlnRelayBandwidthThreshold*: int rlnRelayBandwidthThreshold*: int
@ -343,7 +342,6 @@ proc mount(conf: WakuRlnConfig,
): Future[WakuRlnRelay] {.async.} = ): Future[WakuRlnRelay] {.async.} =
var var
groupManager: GroupManager groupManager: GroupManager
credentials: MembershipCredentials
# create an RLN instance # create an RLN instance
let rlnInstanceRes = createRLNInstance(tree_path = conf.rlnRelayTreePath) let rlnInstanceRes = createRLNInstance(tree_path = conf.rlnRelayTreePath)
if rlnInstanceRes.isErr(): if rlnInstanceRes.isErr():
@ -365,15 +363,14 @@ proc mount(conf: WakuRlnConfig,
if s == "": none(string) else: some(s) if s == "": none(string) else: some(s)
let let
rlnRelayCredPath = useValueOrNone(conf.rlnRelayCredPath) rlnRelayCredPath = useValueOrNone(conf.rlnRelayCredPath)
rlnRelayCredentialsPassword = useValueOrNone(conf.rlnRelayCredentialsPassword) rlnRelayCredPassword = useValueOrNone(conf.rlnRelayCredPassword)
groupManager = OnchainGroupManager(ethClientUrl: conf.rlnRelayEthClientAddress, groupManager = OnchainGroupManager(ethClientUrl: conf.rlnRelayEthClientAddress,
ethContractAddress: $conf.rlnRelayEthContractAddress, ethContractAddress: $conf.rlnRelayEthContractAddress,
rlnInstance: rlnInstance, rlnInstance: rlnInstance,
registrationHandler: registrationHandler, registrationHandler: registrationHandler,
keystorePath: rlnRelayCredPath, keystorePath: rlnRelayCredPath,
keystorePassword: rlnRelayCredentialsPassword, keystorePassword: rlnRelayCredPassword,
keystoreIndex: conf.rlnRelayCredIndex, membershipIndex: some(conf.rlnRelayCredIndex))
membershipGroupIndex: conf.rlnRelayMembershipGroupIndex)
# Initialize the groupManager # Initialize the groupManager
await groupManager.init() await groupManager.init()
# Start the group sync # Start the group sync