3

I will refer to this post How to verify MetaMask account holder is the real owner of the address? .

So what devs are suggesting is to create a nonce on server side, get it via public API, sign wallet address with it and return to server for validation. And its assumed to be secure authentication.

Lets demystify this starting with:

The frontend should do a GET request to retrieve the current nonce for the public address that is trying to sign in. If the account doesn't exist yet then create it and return the nonce anyway.

What it means is that there is a public API that takes walletAdress as parameter and creates a entry in database (not questions asked). This is like leaving a huge backdoor for hackers to bring down your database. Nothing is preventing them to create fake but valid wallet addresses and bombard the server creating millions of fake records until it dies off.

Secondly lets take a a look at this code for signature validation:

nonce = "\x19Ethereum Signed Message:\n" + nonce.length + nonce
nonce = util.keccak(Buffer.from(nonce, "utf-8"))
const { v, r, s } = util.fromRpcSig(signature)
const pubKey = util.ecrecover(util.toBuffer(nonce), v, r, s)
const addrBuf = util.pubToAddress(pubKey)
const addr = util.bufferToHex(addrBuf)

This works completely offline. So who is the stopping the hacker to reverse engineer it? Create a function that takes nonce and some other user wallet address and create a signature? The crypt code is public. And the nonce retrieval is public.

What I tried so far is to replace the database part with user cookie short lived session and a redirect to retrieve the nonce. It would prevent spamming the database, as well generate a private nonce for every nonce request.

Still the whole flow doesn't make sense in terms of security. Any fresh ideas on this?

John T
  • 133
  • 4

1 Answers1

2

The flow presented by Metamask deals with authentication and does not deal with DoS issues or other security matters.

It is considered a secure way of authentication, because the wallet uses the private key (SK) to sign the message, which can be later verified by the backend. Verification is done by computing (recovering) the public key from the signature.

Let's take a look at ethers.js signing implementation -

    async signMessage(message: Bytes | string): Promise<string> {
        return joinSignature(this._signingKey().signDigest(hashMessage(message)));
    }
signDigest(digest: BytesLike): Signature {
    const keyPair = getCurve().keyFromPrivate(arrayify(this.privateKey));
    const digestBytes = arrayify(digest);
    if (digestBytes.length !== 32) {
        logger.throwArgumentError(&quot;bad digest length&quot;, &quot;digest&quot;, digest);
    }
    const signature = keyPair.sign(digestBytes, { canonical: true });
    return splitSignature({
        recoveryParam: signature.recoveryParam,
        r: hexZeroPad(&quot;0x&quot; + signature.r.toString(16), 32),
        s: hexZeroPad(&quot;0x&quot; + signature.s.toString(16), 32),
    })
}

and verification of a message authenticity is done by computing the public key from its given signature -

export function verifyMessage(message: Bytes | string, signature: SignatureLike): string {
    return recoverAddress(hashMessage(message), signature);
}

export function recoverAddress(digest: BytesLike, signature: SignatureLike): string { return computeAddress(recoverPublicKey(arrayify(digest), signature)); }

export function recoverPublicKey(digest: BytesLike, signature: SignatureLike): string { const sig = splitSignature(signature); const rs = { r: arrayify(sig.r), s: arrayify(sig.s) }; return "0x" + getCurve().recoverPubKey(arrayify(digest), rs, sig.recoveryParam).encode("hex", false); }

In this answer, the author explains the process of digital signature and why you need to know the private key in order to produce the signature, and in his more detailed answer he explains the formulas that uses the SK d to sign, which is then verified against the PK (,).

Auth flow presented above is such -

  1. The backend decides upon a nonce and sends to frontend;
  2. Frontend signs the nonce using its private key;
  3. Backend recovers the public key from signature, compares signed payload to given nonce (see recoverPublicKey input args);
  4. If recovered public key equals the expected user wallet, authentication is approved.
Kof
  • 2,954
  • 6
  • 23
  • 1
    The 12345 you wrote we already know. It was in link I referenced, as well in my text. Still the security flaws I am pointing out stand. Nothing is preventing hacker to reverse engineer the decoding process and create signatures for other user wallets with a custom script. Because what can be decoded can also be encoded. Its 2 way process. And everybody can get nonce for free as you do not prove it belonging to you. Its a dummy endpoint. Spamming is not a problem with session strategy. – John T May 04 '22 at 13:49
  • 1
    Also from here https://en.wikipedia.org/wiki/Public-key_cryptography. Private keys to do not encrypt (public keys do). They can sign. And public key can verify message validity. – John T May 04 '22 at 14:04
  • Without the private key, which is hidden in MetaMask, you can't produce an encrypted payload that can be decoded using the public key of that wallet, so as long as the private key is protected, you can't fake the signed payload. – Kof May 04 '22 at 15:09
  • 2
    Fixed the 12345 according to correct public key encryption process. – Kof May 04 '22 at 15:29
  • What payload are you talking about? And your comment 'decoded using the public key', again public keys do no decrypt, they encrypt or validate signatures. – John T May 04 '22 at 15:47
  • 1
    Payload can be a string or a number. In your case, the nonce is a string payload. – Kof May 04 '22 at 15:58
  • I am very grateful for your input, but I have the feeling you are just tossing ideas around, like brainstorming. I am hoping on a answer from a person who actually has implemented secure metmask authorization. For example, I don't think there is such thing as encrypting on client with private key with metmask. All you can do is sign. – John T May 04 '22 at 16:45
  • *I don't think there is such thing as decrypting on client with private key with metmask. (typo) – John T May 04 '22 at 17:02
  • 1
    Yes, I wasn't referring to actual implementation, more to a general idea for a process how to do this. I've read the answers in your link, it does seem strange to do this they way they have, anyone can encrypt the nonce string using the public key. I'll research and let you know if I find anything concrete. – Kof May 04 '22 at 17:54
  • It seems we were both right. Public keys decrypt what was encrypted with the private key and private key decrypt what was encrypted with public key. – Kof May 04 '22 at 19:19
  • So the process flow is actually my original 12345 – Kof May 04 '22 at 19:33
  • Hey @JohnT, does this answer your question? Since private key can be used to encrypt and public key to decrypt, the authentication process should now be clear. – Kof May 15 '22 at 09:39
  • I can not accept this as an answer as it holds bad information. 1) Public keys encrypt and private key decrypt and never the way around. Signing process is different. Private key creates signature and public key verifies all of it, however the message is not encrypted, it comes along plain with the signature. 2) Answer contains link to toptal tutorial that flaws in security. a) Rate limit for spam is not really a good solution. Ip rate limiter will fail when attacked behind proxies. No Ip limiter will mess up real users. b) No expire for nonce, can reuse same for 4ever until verify. – John T May 18 '22 at 13:59
  • I said "some sort of rate limit" of course along with "all standard security guidelines", the authentication flow does not attempt to protect against DoS attacks, that's a different subject. – Kof May 18 '22 at 18:01
  • Re your claim that "private keys do not encrypt", I'm pretty sure that it's false, will look for a proof and let you know. – Kof May 18 '22 at 18:06
  • OK according to the following (and other sources), it's possible to encrypt using the SK and decrypt with PK, but it's highly unadvisable since the PK is using a small public exponent that can be brute forced. https://crypto.stackexchange.com/questions/29030/rsa-is-it-secure-to-use-the-private-key-to-encrypt-and-the-public-key-to-decryp – Kof May 18 '22 at 18:19
  • However, encryption isn't used for signing, that indeed is wrong on my part, it's using a form of hashing to produce a signature using the SK (https://github.com/ethers-io/ethers.js/blob/8b62aeff9cce44cbd16ff41f8fc01ebb101f8265/packages/wallet/src.ts/index.ts#L124), that can be verified by computing the public key from the signature, which proves ownership. (https://github.com/ethers-io/ethers.js/blob/8b62aeff9cce44cbd16ff41f8fc01ebb101f8265/packages/wallet/src.ts/index.ts#L197) – Kof May 18 '22 at 18:29