mirror of https://github.com/waku-org/nwaku.git
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:
parent
ac25855018
commit
e7b2b88f5b
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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],
|
||||
password = password,
|
||||
appInfo = RLNAppInfo).isOk()
|
||||
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)
|
||||
|
|
|
@ -79,19 +79,17 @@ when isMainModule:
|
|||
debug "Transaction hash", txHash = groupManager.registrationTxHash.get()
|
||||
|
||||
# 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,
|
||||
membershipGroups: @[MembershipGroup(
|
||||
membershipContract: MembershipContract(
|
||||
chainId: $groupManager.chainId.get(),
|
||||
address: conf.rlnRelayEthContractAddress,
|
||||
),
|
||||
treeIndex: groupManager.membershipIndex.get(),
|
||||
)]
|
||||
)
|
||||
|
||||
let persistRes = addMembershipCredentials(conf.rlnRelayCredPath,
|
||||
@[keystoreCred],
|
||||
keystoreCred,
|
||||
conf.rlnRelayCredPassword,
|
||||
RLNAppInfo)
|
||||
if persistRes.isErr():
|
||||
|
|
|
@ -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()))
|
||||
|
|
|
@ -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:
|
||||
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
|
||||
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())
|
||||
# We add it to the credentials field of the keystore
|
||||
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 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())
|
||||
let keystoreCredential = keystoreCredentials[key]
|
||||
let decodedKeyfileRes = decodeKeyFileJson(keystoreCredential, password)
|
||||
if decodedKeyfileRes.isErr():
|
||||
return err(AppKeystoreError(kind: KeystoreReadKeyfileError,
|
||||
msg: $decodedKeyfileRes.error))
|
||||
# we parse the json decrypted keystoreCredential
|
||||
let decodedCredentialRes = decode(decodedKeyfileRes.get())
|
||||
let keyfileMembershipCredential = decodedCredentialRes.get()
|
||||
return ok(keyfileMembershipCredential)
|
||||
|
||||
except CatchableError:
|
||||
return err(KeystoreJsonError)
|
||||
|
||||
return ok(outputMembershipCredentials)
|
||||
return err(AppKeystoreError(kind: KeystoreJsonError,
|
||||
msg: getCurrentExceptionMsg()))
|
||||
|
|
|
@ -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
|
||||
identityCredential*: IdentityCredential
|
||||
|
||||
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]
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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(),
|
||||
password = g.keystorePassword.get(),
|
||||
filterMembershipContracts = @[MembershipContract(chainId: $chainId,
|
||||
address: g.ethContractAddress)],
|
||||
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)
|
||||
let keystoreCredRes = getMembershipCredentials(path = g.keystorePath.get(),
|
||||
password = g.keystorePassword.get(),
|
||||
query = keystoreQuery,
|
||||
appInfo = RLNAppInfo)
|
||||
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():
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue