Skip to content

Commit

Permalink
feat: add signature verification for all client actions via relay ser…
Browse files Browse the repository at this point in the history
…vice (#167)

* feat: add signature verification for all client actions via relay service

* feat: per-message signatures and integration with network layer

* feat: add message recovery in webworker

* Update packages/services/cmd/ecs-relay/main.go

Co-authored-by: ludens <ludens@lattice.xyz>

Co-authored-by: alvrs <alvarius@lattice.xyz>
Co-authored-by: alvarius <89248902+alvrs@users.noreply.github.com>
Co-authored-by: ludens <ludens@lattice.xyz>
  • Loading branch information
4 people authored Sep 30, 2022
1 parent 44a8676 commit 7920d6e
Show file tree
Hide file tree
Showing 17 changed files with 930 additions and 316 deletions.
9 changes: 6 additions & 3 deletions packages/network/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
"ts-node": "^10.7.0",
"tslib": "^2.3.1",
"typedoc": "^0.23.10",
"typescript": "^4.6.2"
"typescript": "^4.6.2",
"threads": "^1.7.0"
},
"peerDependencies": {
"@ethersproject/providers": "^5.6.1",
Expand All @@ -59,7 +60,9 @@
"ethers": "^5.7.1",
"mobx": "^6.5.0",
"observable-webworker": "^4.0.1",
"rxjs": "^7.5.5"
"rxjs": "^7.5.5",
"threads": "^1.7.0"
},
"gitHead": "218f56893d268b0c5157a3e4c603b859e287a343"
"gitHead": "218f56893d268b0c5157a3e4c603b859e287a343",
"dependencies": {}
}
45 changes: 33 additions & 12 deletions packages/network/src/createRelayerStream.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Message } from "@latticexyz/services/protobuf/ts/ecs-relay/ecs-relay";
import { ECSRelayServiceClient } from "@latticexyz/services/protobuf/ts/ecs-relay/ecs-relay.client";
import { awaitPromise } from "@latticexyz/utils";
import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
import { from } from "rxjs";
import { Signer } from "ethers";
import { from, map } from "rxjs";
import { spawn } from "threads";
import { messagePayload } from "./utils";

/**
* Create a ECSRelayServiceClient
Expand All @@ -18,37 +23,53 @@ export function createRelayerClient(url: string): ECSRelayServiceClient {
* @param id User id (eg address)
* @returns RelayService connection
*/
export async function createRelayerStream(url: string, id: string) {
export async function createRelayerStream(signer: Signer, url: string, id: string) {
const relayerClient = createRelayerClient(url);
const identity = { name: id };
await relayerClient.authenticate(identity);
const recoverWorker = await spawn(
new Worker(new URL("./workers/Recover.worker.ts", import.meta.url), { type: "module" })
);

// Signature that should be used to prove identity
const signature = {
signature: await signer.signMessage("ecs-relay-service"),
};

await relayerClient.authenticate(signature);

// Subscribe to the stream of relayed events
const stream = relayerClient.openStream(identity);
const event$ = from(stream.responses);
const stream = relayerClient.openStream(signature);
const event$ = from(stream.responses).pipe(
map(async (message) => ({
message,
address: await recoverWorker.recoverAddress(message),
})),
awaitPromise()
);

// Ping every 15s to stay alive
const keepAlive = setInterval(() => relayerClient.ping(identity), 15000);
const keepAlive = setInterval(() => relayerClient.ping(signature), 15000);
function dispose() {
clearInterval(keepAlive);
}

// Subscribe to new labels
function subscribe(label: string) {
relayerClient.subscribe({ identity, subscription: { label } });
relayerClient.subscribe({ signature, subscription: { label } });
}

// Unsubscribe from labels
function unsubscribe(label: string) {
relayerClient.unsubscribe({ identity, subscription: { label } });
relayerClient.unsubscribe({ signature, subscription: { label } });
}

// Push data to subscribers
function push(label: string, data: Uint8Array) {
async function push(label: string, data: Uint8Array) {
const message: Message = { version: 1, id: Date.now() + id, timestamp: BigInt(Date.now()), data, signature: "" };
message.signature = await signer.signMessage(messagePayload(message));

relayerClient.push({
identity,
label,
messages: [{ version: 1, data, timestamp: BigInt(Date.now()), id: id + Date.now() }],
message,
});
}

Expand Down
7 changes: 7 additions & 0 deletions packages/network/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Message } from "@latticexyz/services/protobuf/ts/ecs-relay/ecs-relay";
import { keccak256 } from "ethers/lib/utils";

// Message payload to sign and use to recover signer
export function messagePayload(msg: Message) {
return `(${msg.version},${msg.id},${keccak256(msg.data)},${msg.timestamp})`;
}
10 changes: 10 additions & 0 deletions packages/network/src/workers/Recover.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Message } from "@latticexyz/services/protobuf/ts/ecs-relay/ecs-relay";
import { expose } from "threads";
import { verifyMessage } from "ethers/lib/utils";
import { messagePayload } from "../utils";

function recoverAddress(msg: Message) {
return verifyMessage(messagePayload(msg), msg.signature);
}

expose({ recoverAddress });
3 changes: 2 additions & 1 deletion packages/services/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
i.DS_Store
/bin
/snapshots
docs
docs
FaucetStore
2 changes: 2 additions & 0 deletions packages/services/cmd/ecs-relay/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var (
port = flag.Int("port", 50071, "gRPC Server Port")
idleTimeoutTime = flag.Int("idle-timeout-time", 30, "Time in seconds after which a client connection times out. Defaults to 30s")
idleDisconnectIterval = flag.Int("idle-disconnect-interval", 60, "Time in seconds for how oftern to disconnect idle clients. Defaults to 60s")
messsageDriftTime = flag.Int("message-drift-time", 5, "Time in seconds that is acceptable as drift before message is not relayed. Defaults to 5s")
)

func main() {
Expand All @@ -27,6 +28,7 @@ func main() {
config := &relay.RelayServerConfig{
IdleTimeoutTime: *idleTimeoutTime,
IdleDisconnectIterval: *idleDisconnectIterval,
MessageDriftTime: *messsageDriftTime,
}

// Start gRPC server and the relayer.
Expand Down
27 changes: 5 additions & 22 deletions packages/services/pkg/faucet/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,12 @@ import (
"fmt"
"io/ioutil"
"latticexyz/mud/packages/services/pkg/logger"
"latticexyz/mud/packages/services/pkg/utils"
"os"
"strings"
"time"

"github.com/dghubble/go-twitter/twitter"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"

Expand All @@ -28,24 +26,6 @@ func TwitterUsernameQuery(username string) string {
return fmt.Sprintf("from:%s AND %s", username, MatchingHashtag)
}

func VerifySig(from, sigHex string, msg []byte) (bool, string) {
sig := hexutil.MustDecode(sigHex)

msg = accounts.TextHash(msg)
if sig[crypto.RecoveryIDOffset] == 27 || sig[crypto.RecoveryIDOffset] == 28 {
sig[crypto.RecoveryIDOffset] -= 27 // Transform yellow paper V from 27/28 to 0/1
}

recovered, err := crypto.SigToPub(msg, sig)
if err != nil {
return false, ""
}

recoveredAddr := crypto.PubkeyToAddress(*recovered)

return from == recoveredAddr.Hex(), recoveredAddr.Hex()
}

func ExtractSignatureFromTweet(tweet twitter.Tweet) string {
tokens := strings.Split(tweet.FullText, "faucet")
return strings.TrimSpace(tokens[1])[:132]
Expand All @@ -54,11 +34,14 @@ func ExtractSignatureFromTweet(tweet twitter.Tweet) string {
func VerifyDripRequestTweet(tweet twitter.Tweet, username string, address string) error {
tweetSignature := ExtractSignatureFromTweet(tweet)

isVerified, recoveredAddress := VerifySig(
isVerified, recoveredAddress, err := utils.VerifySig(
address,
tweetSignature,
[]byte(fmt.Sprintf("%s tweetooor requesting drip to %s address", username, address)),
)
if err != nil {
return fmt.Errorf("error verifying signature: %s", err.Error())
}
if !isVerified {
return fmt.Errorf("recovered address %s != provided address %s", recoveredAddress, address)
}
Expand Down
Loading

0 comments on commit 7920d6e

Please sign in to comment.