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?

1
  • 1
    I think I figured it out. Will provide my answer soon.
    – John T
    Commented May 4, 2022 at 17:09

1 Answer 1

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("bad digest length", "digest", digest);
        }
        const signature = keyPair.sign(digestBytes, { canonical: true });
        return splitSignature({
            recoveryParam: signature.recoveryParam,
            r: hexZeroPad("0x" + signature.r.toString(16), 32),
            s: hexZeroPad("0x" + 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.
17
  • 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
    Commented May 4, 2022 at 13:49
  • 1
    Also from here 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
    Commented May 4, 2022 at 14:04
  • 2
    Fixed the 12345 according to correct public key encryption process.
    – Kof
    Commented May 4, 2022 at 15:29
  • 1
    Payload can be a string or a number. In your case, the nonce is a string payload.
    – Kof
    Commented May 4, 2022 at 15:58
  • 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
    Commented May 4, 2022 at 17:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.