Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 23 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cc64ac8
feat(credentials): store and processing generic app credentials
s1fr0 Dec 15, 2022
49faf2b
feat(credentials): separate module; minimal tests
s1fr0 Dec 23, 2022
bb9d869
more work
s1fr0 Jan 10, 2023
cc75811
feat(credentials): check presence of idCredential in keystore and add…
s1fr0 Jan 11, 2023
d1afdeb
feat(credential): refactor, new data structure, dynamic add credentia…
s1fr0 Jan 24, 2023
7566cad
feat(credential): add filter, get credentials
s1fr0 Jan 24, 2023
48fc72c
feat(credential): encode/decode utility
s1fr0 Jan 24, 2023
58724d7
feat(credential): sort groups, test credential retrieval/group merging
s1fr0 Jan 24, 2023
03cae54
fix(credential): remove unnecessary order in sort
s1fr0 Jan 27, 2023
32ac620
Merge branch 'master' into app-credentials
s1fr0 Jan 27, 2023
d133e5d
fix(credentials): fix vendor commits
s1fr0 Jan 27, 2023
1acb693
fix(credential/rln): embed credential module in rln relay
s1fr0 Jan 27, 2023
dda0591
Merge branch 'master' into app-credentials
s1fr0 Jan 31, 2023
1fcf016
feat(credentials/rln): use credentials API in rln-relay to store/read…
s1fr0 Jan 31, 2023
caa4a33
refactor(credentials): implement hasKeys for JsonNode
s1fr0 Feb 5, 2023
a135568
fix(credentials): restore connectToNodes call
s1fr0 Feb 5, 2023
8e96bce
refactor(credentials): remove unnecessary imports
s1fr0 Feb 5, 2023
1581e70
refactor(credentials): add Res suffix to results
s1fr0 Feb 5, 2023
b57c6ba
refactor(credential): moved save json to separate proc; added comments
s1fr0 Feb 5, 2023
af90ba5
feat(credentials): use appInfo
s1fr0 Feb 6, 2023
082043c
refactor(keystore): refactor code in a more structured module; addres…
s1fr0 Feb 7, 2023
2a44956
Merge branch 'master' into app-credentials
s1fr0 Feb 7, 2023
ae0a117
fix(keystore): fix indentation
s1fr0 Feb 7, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions tests/all_tests_v2.nim
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ when defined(waku_exp_store_resume):
# TODO: Review store resume test cases (#1282)
import ./v2/waku_store/test_resume


import
# Waku v2 tests
./v2/test_wakunode,
Expand Down Expand Up @@ -62,11 +61,12 @@ import
./v2/test_waku_noise,
./v2/test_waku_noise_sessions,
./v2/test_waku_switch,
# Waku Keystore
./v2/test_waku_keystore_keyfile,
./v2/test_waku_keystore,
# Utils
./v2/test_utils_compat,
./v2/test_utils_keyfile


./v2/test_utils_compat

## Experimental

when defined(rln):
Expand Down
191 changes: 191 additions & 0 deletions tests/v2/test_waku_keystore.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
{.used.}

import
std/[algorithm, json, options, os],
testutils/unittests, chronos, stint,
../../waku/v2/protocol/waku_keystore,
../test_helpers

from ../../waku/v2/protocol/waku_noise/noise_utils import randomSeqByte

procSuite "Credentials test suite":

# We initialize the RNG in test_helpers
let rng = rng()
let testAppInfo = AppInfo(application: "test", appIdentifier: "1234", version: "0.1")

asyncTest "Create keystore":

let filepath = "./testAppKeystore.txt"
defer: removeFile(filepath)

let keystoreRes = createAppKeystore(path = filepath,
appInfo = testAppInfo)

check:
keystoreRes.isOk()

asyncTest "Load keystore":

let filepath = "./testAppKeystore.txt"
defer: removeFile(filepath)

# If no keystore exists at filepath, a new one is created for appInfo and empty credentials
let keystoreRes = loadAppKeystore(path = filepath,
appInfo = testAppInfo)

check:
keystoreRes.isOk()

let keystore = keystoreRes.get()

check:
keystore.hasKeys(["application", "appIdentifier", "version", "credentials"])
keystore["application"].getStr() == testAppInfo.application
keystore["appIdentifier"].getStr() == testAppInfo.appIdentifier
keystore["version"].getStr() == testAppInfo.version
# We assume the loaded keystore to not have credentials set (previous tests delete the keystore at filepath)
keystore["credentials"].getElems().len() == 0

asyncTest "Add credentials to keystore":

let filepath = "./testAppKeystore.txt"
defer: removeFile(filepath)

# We generate a random identity credential (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen)
var
idTrapdoor = randomSeqByte(rng[], 32)
idNullifier = randomSeqByte(rng[], 32)
idSecretHash = randomSeqByte(rng[], 32)
idCommitment = randomSeqByte(rng[], 32)

var idCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment)

var contract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789")
var index1 = MembershipIndex(1)
var membershipGroup1 = MembershipGroup(membershipContract: contract, treeIndex: index1)

let membershipCredentials1 = MembershipCredentials(identityCredential: idCredential,
membershipGroups: @[membershipGroup1])

# We generate a random identity credential (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen)
idTrapdoor = randomSeqByte(rng[], 32)
idNullifier = randomSeqByte(rng[], 32)
idSecretHash = randomSeqByte(rng[], 32)
idCommitment = randomSeqByte(rng[], 32)

idCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment)

var index2 = MembershipIndex(2)
var membershipGroup2 = MembershipGroup(membershipContract: contract, treeIndex: index2)

let membershipCredentials2 = MembershipCredentials(identityCredential: idCredential,
membershipGroups: @[membershipGroup2])

let password = "%m0um0ucoW%"

let keystoreRes = addMembershipCredentials(path = filepath,
credentials = @[membershipCredentials1, membershipCredentials2],
password = password,
appInfo = testAppInfo)

check:
keystoreRes.isOk()

asyncTest "Add/retrieve credentials in keystore":

let filepath = "./testAppKeystore.txt"
defer: removeFile(filepath)

# We generate two random identity credentials (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen)
var
idTrapdoor1 = randomSeqByte(rng[], 32)
idNullifier1 = randomSeqByte(rng[], 32)
idSecretHash1 = randomSeqByte(rng[], 32)
idCommitment1 = randomSeqByte(rng[], 32)
idCredential1 = IdentityCredential(idTrapdoor: idTrapdoor1, idNullifier: idNullifier1, idSecretHash: idSecretHash1, idCommitment: idCommitment1)

var
idTrapdoor2 = randomSeqByte(rng[], 32)
idNullifier2 = randomSeqByte(rng[], 32)
idSecretHash2 = randomSeqByte(rng[], 32)
idCommitment2 = randomSeqByte(rng[], 32)
idCredential2 = IdentityCredential(idTrapdoor: idTrapdoor2, idNullifier: idNullifier2, idSecretHash: idSecretHash2, idCommitment: idCommitment2)

# We generate two distinct membership groups
var contract1 = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789")
var index1 = MembershipIndex(1)
var membershipGroup1 = MembershipGroup(membershipContract: contract1, treeIndex: index1)

var contract2 = MembershipContract(chainId: "6", address: "0x0000000000000000000000000000000000000000")
var index2 = MembershipIndex(2)
var membershipGroup2 = MembershipGroup(membershipContract: contract2, treeIndex: index2)

# We generate three membership credentials
let membershipCredentials1 = MembershipCredentials(identityCredential: idCredential1,
membershipGroups: @[membershipGroup1])

let membershipCredentials2 = MembershipCredentials(identityCredential: idCredential2,
membershipGroups: @[membershipGroup2])

let membershipCredentials3 = MembershipCredentials(identityCredential: idCredential1,
membershipGroups: @[membershipGroup2])

# This is the same as rlnMembershipCredentials3, should not change the keystore entry of idCredential
let membershipCredentials4 = MembershipCredentials(identityCredential: idCredential1,
membershipGroups: @[membershipGroup2])

let password = "%m0um0ucoW%"

# We add credentials to the keystore. Note that only 3 credentials should be effectively added, since rlnMembershipCredentials3 is equal to membershipCredentials2
let keystoreRes = addMembershipCredentials(path = filepath,
credentials = @[membershipCredentials1, membershipCredentials2, membershipCredentials3, membershipCredentials4],
password = password,
appInfo = testAppInfo)

check:
keystoreRes.isOk()

# We test retrieval of credentials.
var expectedMembershipGroups1 = @[membershipGroup1, membershipGroup2]
expectedMembershipGroups1.sort(sortMembershipGroup)
let expectedCredential1 = MembershipCredentials(identityCredential: idCredential1,
membershipGroups: expectedMembershipGroups1)


var expectedMembershipGroups2 = @[membershipGroup2]
expectedMembershipGroups2.sort(sortMembershipGroup)
let expectedCredential2 = MembershipCredentials(identityCredential: idCredential2,
membershipGroups: expectedMembershipGroups2)


# We retrieve all credentials stored under password (no filter)
var recoveredCredentialsRes = getMembershipCredentials(path = filepath,
password = password,
appInfo = testAppInfo)

check:
recoveredCredentialsRes.isOk()
recoveredCredentialsRes.get() == @[expectedCredential1, expectedCredential2]


# We retrieve credentials by filtering on an IdentityCredential
recoveredCredentialsRes = getMembershipCredentials(path = filepath,
password = password,
filterIdentityCredentials = @[idCredential1],
appInfo = testAppInfo)

check:
recoveredCredentialsRes.isOk()
recoveredCredentialsRes.get() == @[expectedCredential1]

# We retrieve credentials by filtering on multiple IdentityCredentials
recoveredCredentialsRes = getMembershipCredentials(path = filepath,
password = password,
filterIdentityCredentials = @[idCredential1, idCredential2],
appInfo = testAppInfo)

check:
recoveredCredentialsRes.isOk()
recoveredCredentialsRes.get() == @[expectedCredential1, expectedCredential2]

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import
testutils/unittests, chronos,
eth/keys
import
../../waku/v2/utils/keyfile
../../waku/v2/protocol/waku_keystore

from ../../waku/v2/protocol/waku_noise/noise_utils import randomSeqByte

Expand Down Expand Up @@ -312,7 +312,7 @@ suite "KeyFile test suite (adapted from nim-eth keyfile tests)":

check:
secret.isErr()
secret.error == KeyFileError.IncorrectMac
secret.error == KeyFileError.KeyfileIncorrectMac

test "Wrong mac in keyfile":

Expand Down Expand Up @@ -350,7 +350,7 @@ suite "KeyFile test suite (adapted from nim-eth keyfile tests)":
keyfileWrongMac.getOrDefault("password").getStr())
check:
secret.isErr()
secret.error == KeyFileError.IncorrectMac
secret.error == KeyFileError.KeyFileIncorrectMac

test "Scrypt keyfiles":
let
Expand Down
29 changes: 22 additions & 7 deletions tests/v2/test_waku_rln_relay.nim
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import
../../waku/v2/node/waku_node,
../../waku/v2/protocol/waku_message,
../../waku/v2/protocol/waku_rln_relay,
../../waku/v2/protocol/waku_keystore,
../test_helpers

const RlnRelayPubsubTopic = "waku/2/rlnrelay/proto"
const RlnRelayContentTopic = "waku/2/rlnrelay/proto"

procSuite "Waku rln relay":

asyncTest "mount waku-rln-relay in the off-chain mode":
let
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[]
Expand Down Expand Up @@ -1041,10 +1043,11 @@ suite "Waku rln relay":

debug "the generated identity credential: ", idCredential

let index = MembershipIndex(1)
let index = MembershipIndex(1)

let rlnMembershipCredentials = RlnMembershipCredentials(identityCredential: idCredential,
rlnIndex: index)
let rlnMembershipContract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789")
let rlnMembershipGroup = MembershipGroup(membershipContract: rlnMembershipContract, treeIndex: index)
let rlnMembershipCredentials = MembershipCredentials(identityCredential: idCredential, membershipGroups: @[rlnMembershipGroup])

let password = "%m0um0ucoW%"

Expand All @@ -1053,19 +1056,31 @@ suite "Waku rln relay":

# Write RLN credentials
require:
writeRlnCredentials(filepath, rlnMembershipCredentials, password).isOk()
addMembershipCredentials(path = filepath,
credentials = @[rlnMembershipCredentials],
password = password,
appInfo = RLNAppInfo).isOk()

let readCredentialsResult = getMembershipCredentials(path = filepath,
password = password,
filterMembershipContracts = @[rlnMembershipContract],
appInfo = RLNAppInfo)

let readCredentialsResult = readRlnCredentials(filepath, password)
require:
readCredentialsResult.isOk()

let credentials = readCredentialsResult.get()
# getMembershipCredentials returns all credentials in keystore as sequence matching the filter
let allMatchingCredentials = readCredentialsResult.get()
# if any is found, we return the first credential, otherwise credentials is none
var credentials = none(MembershipCredentials)
if allMatchingCredentials.len() > 0:
credentials = some(allMatchingCredentials[0])

require:
credentials.isSome()
check:
credentials.get().identityCredential == idCredential
credentials.get().rlnIndex == index
credentials.get().membershipGroups == @[rlnMembershipGroup]

test "histogram static bucket generation":
let buckets = generateBucketsForHistogram(10)
Expand Down
1 change: 1 addition & 0 deletions tests/v2/test_waku_rln_relay_onchain.nim
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import
stew/byteutils, stew/shims/net as stewNet,
libp2p/crypto/crypto,
eth/keys,
../../waku/v2/protocol/waku_keystore,
../../waku/v2/protocol/waku_rln_relay,
../../waku/v2/node/waku_node,
../test_helpers,
Expand Down
1 change: 1 addition & 0 deletions tests/v2/test_wakunode_rln_relay.nim
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import
../../waku/v2/node/waku_node,
../../waku/v2/protocol/waku_message,
../../waku/v2/protocol/waku_rln_relay,
../../waku/v2/protocol/waku_keystore,
../../waku/v2/utils/peers

from std/times import epochTime
Expand Down
2 changes: 1 addition & 1 deletion waku/v2/node/waku_node.nim
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ proc info*(node: WakuNode): WakuInfo =
proc connectToNodes*(node: WakuNode, nodes: seq[RemotePeerInfo] | seq[string], source = "api") {.async.} =
## `source` indicates source of node addrs (static config, api call, discovery, etc)
# NOTE This is dialing on WakuRelay protocol specifically
await connectToNodes(node.peerManager, nodes, WakuRelayCodec, source=source)
await peer_manager.connectToNodes(node.peerManager, nodes, WakuRelayCodec, source=source)
rymnc marked this conversation as resolved.
Show resolved Hide resolved


## Waku relay
Expand Down
19 changes: 19 additions & 0 deletions waku/v2/protocol/waku_keystore.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# The keyfile submodule (implementation adapted from nim-eth keyfile module https://github.com/status-im/nim-eth/blob/master/eth/keyfile)
import
./waku_keystore/keyfile

export
keyfile

# The Waku Keystore implementation
import
./waku_keystore/keystore,
./waku_keystore/conversion_utils,
./waku_keystore/protocol_types,
./waku_keystore/utils

export
keystore,
conversion_utils,
protocol_types,
utils
29 changes: 29 additions & 0 deletions waku/v2/protocol/waku_keystore/conversion_utils.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}

import
json,
stew/[results, byteutils],
./protocol_types

# Encodes a Membership credential to a byte sequence
proc encode*(credential: MembershipCredentials): seq[byte] =
# TODO: use custom encoding, avoid wordy json
var stringCredential: string
# NOTE: toUgly appends to the string, doesn't replace its contents
stringCredential.toUgly(%credential)
return toBytes(stringCredential)

# Decodes a byte sequence to a Membership credential
proc decode*(encodedCredential: seq[byte]): KeystoreResult[MembershipCredentials] =
# TODO: use custom decoding, avoid wordy json
try:
# we parse the json decrypted keystoreCredential
let jsonObject = parseJson(string.fromBytes(encodedCredential))
return ok(to(jsonObject, MembershipCredentials))
except JsonParsingError:
return err(KeystoreJsonError)
except Exception: #parseJson raises Exception
return err(KeystoreOsError)
Loading