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(
rlnRelayDynamic: conf.rlnRelayDynamic,
rlnRelayCredIndex: conf.rlnRelayCredIndex,
rlnRelayMembershipGroupIndex: conf.rlnRelayMembershipGroupIndex,
rlnRelayEthContractAddress: conf.rlnRelayEthContractAddress,
rlnRelayEthClientAddress: conf.rlnRelayEthClientAddress,
rlnRelayCredPath: conf.rlnRelayCredPath,
rlnRelayCredentialsPassword: conf.rlnRelayCredentialsPassword
rlnRelayCredPassword: conf.rlnRelayCredPassword
)
waitFor node.mountRlnRelay(rlnConf,

View File

@ -237,11 +237,6 @@ type
defaultValue: 0
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* {.
desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false",
defaultValue: false
@ -267,7 +262,7 @@ type
defaultValue: ""
name: "rln-relay-eth-contract-address" }: string
rlnRelayCredentialsPassword* {.
rlnRelayCredPassword* {.
desc: "Password for encrypting RLN credentials",
defaultValue: ""
name: "rln-relay-cred-password" }: string

View File

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

View File

@ -150,11 +150,6 @@ type
defaultValue: 0
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* {.
desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false",
defaultValue: false
@ -180,7 +175,7 @@ type
defaultValue: ""
name: "rln-relay-eth-contract-address" }: string
rlnRelayCredentialsPassword* {.
rlnRelayCredPassword* {.
desc: "Password for encrypting RLN credentials",
defaultValue: ""
name: "rln-relay-cred-password" }: string

View File

@ -1,8 +1,9 @@
{.used.}
import
std/[algorithm, json, options, os],
testutils/unittests, chronos, stint
std/[os, json],
chronos,
testutils/unittests
import
../../waku/waku_keystore,
./testlib/common
@ -44,7 +45,7 @@ procSuite "Credentials test suite":
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
keystore["credentials"].len() == 0
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 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])
var index = MembershipIndex(1)
let membershipCredential = KeystoreMembership(membershipContract: contract,
treeIndex: index,
identityCredential: idCredential)
let password = "%m0um0ucoW%"
let keystoreRes = addMembershipCredentials(path = filepath,
credentials = @[membershipCredentials1, membershipCredentials2],
membership = membershipCredential,
password = password,
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)
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)
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)
# 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 contract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789")
var index = MembershipIndex(1)
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%"
# 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],
membership = membershipCredential,
password = password,
appInfo = testAppInfo)
@ -146,45 +110,16 @@ procSuite "Credentials test suite":
keystoreRes.isOk()
# We test retrieval of credentials.
var expectedMembershipGroups1 = @[membershipGroup1, membershipGroup2]
expectedMembershipGroups1.sort(sortMembershipGroup)
let expectedCredential1 = MembershipCredentials(identityCredential: idCredential1,
membershipGroups: expectedMembershipGroups1)
var expectedMembership = membershipCredential
let membershipQuery = KeystoreMembership(membershipContract: contract,
treeIndex: index)
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,
query = membershipQuery,
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]
recoveredCredentialsRes.get() == expectedMembership

View File

@ -815,10 +815,14 @@ suite "Waku rln relay":
let index = MembershipIndex(1)
let rlnMembershipContract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789")
let rlnMembershipGroup = MembershipGroup(membershipContract: rlnMembershipContract, treeIndex: index)
let rlnMembershipCredentials = MembershipCredentials(identityCredential: idCredential, membershipGroups: @[rlnMembershipGroup])
let keystoreMembership = KeystoreMembership(
membershipContract: MembershipContract(
chainId: "5",
address: "0x0123456789012345678901234567890123456789"
),
treeIndex: index,
identityCredential: idCredential,
)
let password = "%m0um0ucoW%"
let filepath = "./testRLNCredentials.txt"
@ -827,30 +831,29 @@ suite "Waku rln relay":
# Write RLN credentials
require:
addMembershipCredentials(path = filepath,
credentials = @[rlnMembershipCredentials],
membership = keystoreMembership,
password = password,
appInfo = RLNAppInfo).isOk()
let readCredentialsResult = getMembershipCredentials(path = filepath,
let readKeystoreRes = getMembershipCredentials(path = filepath,
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)
assert readKeystoreRes.isOk(), $readKeystoreRes.error
require:
readCredentialsResult.isOk()
# 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()
# getMembershipCredentials returns the credential in the keystore which matches
# the query, in this case the query is =
# chainId = "5" and
# address = "0x0123456789012345678901234567890123456789" and
# treeIndex = 1
let readKeystoreMembership = readKeystoreRes.get()
check:
credentials.get().identityCredential == idCredential
credentials.get().membershipGroups == @[rlnMembershipGroup]
readKeystoreMembership == keystoreMembership
test "histogram static bucket generation":
let buckets = generateBucketsForHistogram(10)

View File

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

View File

@ -8,22 +8,24 @@ import
stew/[results, byteutils],
./protocol_types
# Encodes a Membership credential to a byte sequence
proc encode*(credential: MembershipCredentials): seq[byte] =
# Encodes a KeystoreMembership credential to a byte sequence
proc encode*(credential: KeystoreMembership): 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] =
# Decodes a byte sequence to a KeystoreMembership credential
proc decode*(encodedCredential: seq[byte]): KeystoreResult[KeystoreMembership] =
# 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))
return ok(to(jsonObject, KeystoreMembership))
except JsonParsingError:
return err(KeystoreJsonError)
return err(AppKeystoreError(kind: KeystoreJsonError,
msg: getCurrentExceptionMsg()))
except Exception: #parseJson raises Exception
return err(KeystoreOsError)
return err(AppKeystoreError(kind: KeystoreOsError,
msg: getCurrentExceptionMsg()))

View File

@ -5,7 +5,7 @@ else:
import
options, json, strutils,
std/[algorithm, os, sequtils, sets]
std/[tables, os]
import
./keyfile,
@ -20,15 +20,16 @@ proc createAppKeystore*(path: string,
let keystore = AppKeystore(application: appInfo.application,
appIdentifier: appInfo.appIdentifier,
credentials: @[],
version: appInfo.version)
version: appInfo.version,
credentials: initTable[string, KeystoreMembership]())
var jsonKeystore: string
jsonKeystore.toUgly(%keystore)
var f: File
if not f.open(path, fmWrite):
return err(KeystoreOsError)
return err(AppKeystoreError(kind: KeystoreOsError,
msg: "Cannot open file for writing"))
try:
# 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)
ok()
except CatchableError:
err(KeystoreOsError)
err(AppKeystoreError(kind: KeystoreOsError,
msg: getCurrentExceptionMsg()))
finally:
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 fileExists(path) == false:
let newKeystore = createAppKeystore(path, appInfo, separator)
if newKeystore.isErr():
return err(KeystoreCreateKeystoreError)
let newKeystoreRes = createAppKeystore(path, appInfo, separator)
if newKeystoreRes.isErr():
return err(newKeystoreRes.error)
try:
# We read all the file contents
var f: File
if not f.open(path, fmRead):
return err(KeystoreOsError)
return err(AppKeystoreError(kind: KeystoreOsError,
msg: "Cannot open file for reading"))
let fileContents = readAll(f)
# 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
# TODO: we might continue rather than return for some of these errors
except JsonParsingError:
return err(KeystoreJsonError)
return err(AppKeystoreError(kind: KeystoreJsonError,
msg: getCurrentExceptionMsg()))
except ValueError:
return err(KeystoreJsonError)
return err(AppKeystoreError(kind: KeystoreJsonError,
msg: getCurrentExceptionMsg()))
except OSError:
return err(KeystoreOsError)
return err(AppKeystoreError(kind: KeystoreOsError,
msg: getCurrentExceptionMsg()))
except Exception: #parseJson raises Exception
return err(KeystoreOsError)
return err(AppKeystoreError(kind: KeystoreOsError,
msg: getCurrentExceptionMsg()))
except IOError:
return err(KeystoreIoError)
return err(AppKeystoreError(kind: KeystoreIoError,
msg: getCurrentExceptionMsg()))
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,
credentials: seq[MembershipCredentials],
membership: KeystoreMembership,
password: string,
appInfo: AppInfo,
separator: string = "\n"): KeystoreResult[void] =
@ -118,77 +126,38 @@ proc addMembershipCredentials*(path: string,
let jsonKeystoreRes = loadAppKeystore(path, appInfo, separator)
if jsonKeystoreRes.isErr():
return err(KeystoreLoadKeystoreError)
return err(jsonKeystoreRes.error)
# 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
let keystoreCredentials = jsonKeystore["credentials"]
let key = membership.hash()
if keystoreCredentials.hasKey(key):
# noop
return ok()
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 encodedMembershipCredential = membership.encode()
let keyfileRes = createKeyFileJson(encodedMembershipCredential, password)
if keyfileRes.isErr():
return err(KeystoreCreateKeyfileError)
return err(AppKeystoreError(kind: KeystoreCreateKeyfileError,
msg: $keyfileRes.error))
# We add it to the credentials field of the keystore
jsonKeystore["credentials"].add(keyfileRes.get())
jsonKeystore["credentials"][key] = keyfileRes.get()
except CatchableError:
return err(KeystoreJsonError)
return err(AppKeystoreError(kind: KeystoreJsonError,
msg: getCurrentExceptionMsg()))
# We save to disk the (updated) keystore.
if save(jsonKeystore, path, separator).isErr():
return err(KeystoreOsError)
let saveRes = save(jsonKeystore, path, separator)
if saveRes.isErr():
return err(saveRes.error)
return ok()
@ -196,18 +165,15 @@ proc addMembershipCredentials*(path: string,
# 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] = @[]
query: KeystoreMembership,
appInfo: AppInfo): KeystoreResult[KeystoreMembership] =
# 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)
return err(jsonKeystoreRes.error)
# We load the JSON node corresponding to the app keystore
var jsonKeystore = jsonKeystoreRes.get()
@ -215,27 +181,24 @@ proc getMembershipCredentials*(path: string,
try:
if jsonKeystore.hasKey("credentials"):
# We get all credentials in keystore
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():
# keystoreCredential is encrypted. We decrypt it
let keystoreCredential = keystoreCredentials[key]
let decodedKeyfileRes = decodeKeyFileJson(keystoreCredential, password)
if decodedKeyfileRes.isOk():
if decodedKeyfileRes.isErr():
return err(AppKeystoreError(kind: KeystoreReadKeyfileError,
msg: $decodedKeyfileRes.error))
# 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())
return ok(keyfileMembershipCredential)
except CatchableError:
return err(KeystoreJsonError)
return ok(outputMembershipCredentials)
return err(AppKeystoreError(kind: KeystoreJsonError,
msg: getCurrentExceptionMsg()))

View File

@ -4,8 +4,9 @@ else:
{.push raises: [].}
import
std/sequtils,
std/[sequtils, tables],
stew/[results, endians2],
nimcrypto,
stint
# 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
address*: string
type MembershipGroup* = object
type KeystoreMembership* = ref object of RootObj
membershipContract*: MembershipContract
treeIndex*: MembershipIndex
type MembershipCredentials* = object
identityCredential*: IdentityCredential
membershipGroups*: seq[MembershipGroup]
proc `$`*(m: KeystoreMembership): string =
return "KeystoreMembership(chainId: " & m.membershipContract.chainId & ", contractAddress: " & m.membershipContract.address & ", treeIndex: " & $m.treeIndex & ", identityCredential: " & $m.identityCredential & ")"
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
application*: string
@ -104,11 +120,11 @@ type AppInfo* = object
type AppKeystore* = object
application*: string
appIdentifier*: string
credentials*: seq[MembershipCredentials]
credentials*: MembershipTable
version*: string
type
AppKeystoreError* = enum
AppKeystoreErrorKind* = enum
KeystoreOsError = "keystore error: OS specific error"
KeystoreIoError = "keystore error: IO specific error"
KeystoreJsonKeyError = "keystore error: fields not present in JSON"
@ -119,5 +135,14 @@ type
KeystoreCreateKeyfileError = "Error while creating keyfile for credentials"
KeystoreSaveKeyfileError = "Error while saving 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]

View File

@ -5,7 +5,9 @@ else:
import
json,
std/[options, os, sequtils],
std/[os, sequtils]
import
./keyfile,
./protocol_types
@ -13,25 +15,22 @@ import
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: # TODO: Fix "BareExcept" warning
return err(KeystoreOsError)
return err(AppKeystoreError(kind: KeystoreOsError,
msg: "could not backup keystore: " & getCurrentExceptionMsg()))
# We save the updated json
var f: File
if not f.open(path, fmAppend):
return err(KeystoreOsError)
return err(AppKeystoreError(kind: KeystoreOsError,
msg: getCurrentExceptionMsg()))
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})
@ -47,8 +46,10 @@ proc save*(json: JsonNode, path: string, separator: string): KeystoreResult[void
moveFile(path & ".bkp", path)
except: # TODO: Fix "BareExcept" warning
# Unlucky, we just fail
return err(KeystoreOsError)
return err(KeystoreOsError)
return err(AppKeystoreError(kind: KeystoreOsError,
msg: "could not restore keystore backup: " & getCurrentExceptionMsg()))
return err(AppKeystoreError(kind: KeystoreOsError,
msg: "could not write keystore: " & getCurrentExceptionMsg()))
finally:
f.close()
@ -57,39 +58,7 @@ proc save*(json: JsonNode, path: string, separator: string): KeystoreResult[void
try:
removeFile(path & ".bkp")
except CatchableError:
return err(KeystoreOsError)
return err(AppKeystoreError(kind: KeystoreOsError,
msg: "could not remove keystore backup: " & getCurrentExceptionMsg()))
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
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]
chainId*: Option[Quantity]
keystorePath*: Option[string]
keystoreIndex*: uint
membershipGroupIndex*: uint
keystorePassword*: Option[string]
registrationHandler*: Option[RegistrationHandler]
# 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)
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:
let parsedCredsRes = getMembershipCredentials(path = g.keystorePath.get(),
let keystoreCredRes = getMembershipCredentials(path = g.keystorePath.get(),
password = g.keystorePassword.get(),
filterMembershipContracts = @[MembershipContract(chainId: $chainId,
address: g.ethContractAddress)],
query = keystoreQuery,
appInfo = RLNAppInfo)
if parsedCredsRes.isErr():
raise newException(ValueError, "could not parse the keystore: " & $parsedCredsRes.error())
let parsedCreds = parsedCredsRes.get()
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)
if keystoreCredRes.isErr():
raise newException(ValueError, "could not parse the keystore: " & $keystoreCredRes.error)
let keystoreCred = keystoreCredRes.get()
g.idCredentials = some(keystoreCred.identityCredential)
let metadataGetRes = g.rlnInstance.getMetadata()
if metadataGetRes.isErr():

View File

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