Wie kann ich überprüfen, ob der Inhaber des MetaMask-Kontos der wahre Eigentümer der Adresse ist?

Ich mache eine Dapp, die einen Node.js-Server anruft. Ich erwarte, dass der Benutzer MetaMask installiert hat, und ich möchte überprüfen, ob er der wirkliche Besitzer der aktuellen Adresse auf MetaMask ist (dh accounts[0]).

Dies ist der Benutzerfluss, den ich zu implementieren versuche:

  1. Der Benutzer lädt mein DAPP-Frontend in seinen Browser.
  2. Der Browser bekommt accounts[0]von Web3/MetaMask.
  3. Das Frontend fordert einige spezifische Daten accounts[0]von meiner Node.js-API an.
  4. Auf dem Node.js-Server muss ich überprüfen, ob die Anfrage von jemandem kommt, der tatsächlich die privaten Schlüssel für die Adresse besitzt accounts[0]. Wenn dies gültig ist, dann antworte ich mit den spezifischen Daten.

Ich habe mich lange mit verschiedenen Signierfunktionen von Web3 beschäftigt und war am Ende sehr verwirrt. Es gibt:

  • web3.eth.sign- dazu gibt es kein recoverGegenstück und MetaMask erscheint nicht und fordert den Benutzer nicht auf, etwas zu unterschreiben.
  • web3.eth.personal.sign- Dieser erfordert ein Passwort, ich möchte den Benutzer nicht auffordern, sein Passwort einzugeben, sollte MetaMask dies nicht tun?
  • web3.eth.accounts.sign- Dies scheint eher eine Hash-Funktion zu sein als das, was ich brauche.

Ich habe das Gefühl, dass keine der drei oben genannten Funktionen das ist, was ich brauche. Kann jemand eine Anleitung geben, wie man das angeht?

Ich glaube die Lösung funktioniert nicht mehr. Zumindest bekomme ich nicht die richtige öffentliche Adresse. Könnten Sie mir bitte helfen, herauszufinden, wie es gemacht werden muss?
Die Antworten auf dieser Seite sind veraltet. web3auf einer Seite mit Metamask nicht mehr zugänglich ist.

Antworten (5)

Ich denke, web3.eth.signdas ist, was Sie wollen, aber beachten Sie, dass es eine 32-Byte-Zeichenfolge erwartet (normalerweise ein Hash einer Nachricht).

Das hat bei mir funktioniert:

web3.eth.sign(web3.eth.defaultAccount, web3.sha3('test'), function (err, signature) {
  console.log(signature);  // But maybe do some error checking. :-)
});

Dann auf dem Server mit ethereumjs-util:

const util = require('ethereumjs-util');
const sig = util.fromRpcSig('<signature from front end>');
const publicKey = util.ecrecover(util.sha3('test'), sig.v, sig.r, sig.s);
const address = util.pubToAddress(publicKey).toString('hex');

Sie sagten "MetaMask erscheint nicht und fordert den Benutzer auf, etwas zu signieren", aber das soll es. Wenn es immer noch nicht funktioniert, teilen Sie uns bitte den Code mit, den Sie zum Anrufen verwenden web3.eth.sign.

BEARBEITEN

Beachten Sie, dass es in web3.js 1.0 web3.util.sha3anstelle von web3.sha3.

das war sehr hilfreich, danke. Ich musste jedoch zwei Änderungen vornehmen, damit es funktioniert (da ich Web3 1.0 verwende): (1) web3.utils.sha3Anstelle von web3.sha3, (2) fromRpcSig()musste ich das Ganze mit dem führenden 0x übergeben. Wenn Sie dies in einer Bearbeitung hinzufügen, akzeptiere ich Ihre Antwort.
Danke, der 0xAnfang wird tatsächlich benötigt (unabhängig davon, welche web3.js-Version Sie verwenden). Ich habe die Antwort bearbeitet.
ist das sicher? Soll die Zeichenfolge zufällig von einem Server generiert werden oder kann sie im Front- und Backend fest codiert werden?

Was ich aus meiner Erfahrung sagen kann, ist, dass keine der obigen Antworten für mich sofort funktioniert hat. So habe ich es umgesetzt (getestet mit MetaMask und WalletConnect)

Frontend-Web-App (ReactApp)

Die Herausforderung besteht darin, eine vom Server bereitgestellte Nonce (zufällige Zeichenfolge) zu signieren. So kann ein sicheres Zeichen verlangt werden

let sign = await web3.eth.personal.sign(nonce, walletAddress, "")

Wo:

  • web3ist eine Instanz von web3 (aktuell @ 1.3.6)
  • nonceist die zu signierende zufällige Zeichenfolge (als einfache Zeichenfolge übergeben)
  • walletAddressist die Wallet-Adresse als String (zB 0xaabb....ccdd)

Das erstellte Schild wird dann unverändert an das Server-Backend gesendet

Server-Backend-Überprüfung

Das Ziel ist es, aus dem Zeichen die Wallet-Adresse zu extrahieren, die die Anfrage signiert hat. Auf diese Weise gibt es keine Möglichkeit, es zu fälschen.

import * as util from "ethereumjs-util";

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)

Wenn wir eine Nonce im Browser signieren, wird automatisch eine Füllnachricht vor der Nonce hinzugefügt, und daher müssen wir sie serverseitig neu erstellen, um sie zu überprüfen.

Was wir hier tun, ist die Nonce neu zu erstellen, dann den öffentlichen Schlüssel des Unterzeichners zu extrahieren und die Wallet-Adresse aus dem öffentlichen Schlüssel zu erhalten. Wenn diese Adresse mit der erwarteten übereinstimmt , ist der Benutzer der Eigentümer des privaten Schlüssels, der mit der extrahierten Adresse verbunden ist.

ethereum-jswar Benutzer mit Version 7.0.10 ( npm-Paket )

Was ist der Unterschied zwischen Signatur und Nonce im Backend? woher kommt die signatur??
nonceist eine zufällig zu signierende Zeichenfolge, die vom Backend generiert und zum Signieren an das Frontend gesendet wird. Signatureist die Ausgabe einer Zeichenoperation und wird in unserem Fall mit einem Wallet-Manager wie Metamask generiert. Dies sind nicht nur web3-bezogene Konzepte. Ich empfehle Ihnen, mehr über dieses Thema zu lesen, zum Beispiel stackoverflow.com/questions/4751172/…
Es wird also als unsicher angesehen, dieselbe Zeichenfolge am Front- und Backend zu verwenden, anstatt sie zufällig zu generieren?
Jede Nonce sollte zufällig und eindeutig vom Backend generiert werden, andernfalls haben Sie ein Sicherheitsproblem, da ich eine gestohlene Nonce, die von einem anderen Benutzer signiert wurde, endlos verwenden kann.
Auf der Clientseite ist dies der Code, den ich jetzt verwenden musste, window.web3der zugunsten von veraltet ist window.ethereum:window.ethereum.sendAsync({method: 'personal_sign', params: [window.ethereum.selectedAddress, nonce], from: window.ethereum.selectedAddress}, (err, result) => { console.log('signature', result.result) });
Sie können einfach ethers.utils.verifyMessage(message,signature) verwenden

Das eigene web3 der MetaMask, immer noch 0.2 von Mai 2018, Zeichensyntax ist --

console.log(web3.version.api);
web3.personal.sign(web3.toHex("message to sign"), accounts[0], 
                   function(err, res) {
    // whatever
});

Wenn Sie web3 von MetaMask durch web3.js 1.0.0 (Beta) ersetzen, lautet die zu verwendende Syntax --

window.web3 = new Web3(web3.currentProvider);
console.log(web3.version);
web3.eth.personal.sign('message to sign', accounts[0])
.then(signature => {
    // whatever
});

In beiden Fällen zeigt MetaMask die Zeichenbenachrichtigung an

Ich habe das umgesetzt und es funktioniert super. Ich verwende Python/Flask im Backend, also müssten Sie entsprechenden Backend-Code für Node finden:

Backend: Speichern Sie den Benutzer anhand seiner öffentlichen Adresse in der Datenbank zusammen mit einer Nonce, die für die Anmeldung verwendet wird

Das einfachste Schema für einen Benutzer/Account ist:

public_address = db.Column(db.String(80), primary_key=True, nullable=False, unique=True)
nonce = db.Column(
    INTEGER(unsigned=True),
    nullable=False,
    default=generate_nonce,
)

Wobei Generate Nonce ein Pseudozufallszahlengenerator ist, etwa so:

def generate_nonce(self, length=8):
    return ''.join([str(randint(0, 9)) for i in range(length)])

Frontend GETs und signiert die Nonce mit web3

Rufen Sie die öffentliche Adresse des aktuellen Benutzers ab:

web3.eth.getAccounts()
        .then((response) => {
            const publicAddressResponse = response[0];

            if (!(typeof publicAddressResponse === "undefined")) {
                setPublicAddress(publicAddressResponse);
                getNonce(publicAddressResponse);
            }
        })
        .catch((e) => {
            console.error(e);
        });

Das Frontend sollte eine GET-Anforderung ausführen, um die aktuelle Nonce für die öffentliche Adresse abzurufen, die versucht, sich anzumelden. Wenn das Konto noch nicht existiert, erstellen Sie es und geben Sie die Nonce trotzdem zurück:

GET /api/users?publicAddress=${publicAddress}

und dann die Nonce mit Metamask signieren:

web3.eth.personal.sign(`I am signing my one-time nonce: ${nonce}`, publicAddress, "test password!")
            .then((signature) => {
                handleAuth(publicAddress, signature)
            });

Das Front-End sendet dann die signierte Nonce an das Back-End, um ein JWT zu erhalten

Frontend:

axios.post(props.config.serverUrl + '/sessions/', {
            publicAddress: publicAddress,
            signature: signature,
            auth_type: 'ethereum',
        })
            .then((response) => {
                localStorage.setItem('accessToken', response.data.access_token);
            })
            .catch((e) => {
                console.error(e);
            });

Dann authentifizieren Sie im Back-End mithilfe von web3-Bibliotheken, dass die Signatur von dieser öffentlichen Adresse stammt, und geben bei Authentifizierung ein JWT aus. Von dort aus ist es nur eine normale Sitzungsverwaltung mit JWTs, die nicht speziell ein web3-Problem ist:

@sessions_blueprint.route('/sessions/', methods=['POST'])
def create_session():

    auth_type = request.json.get('auth_type', AuthType.EMAIL)

    public_address = request.json['publicAddress']
    signature = request.json['signature']

    account = EthereumAccount.query.filter_by(public_address=public_address).first()

    if account is None:
        abort(404, 'Public address not registered.')

    original_message = 'I am signing my one-time nonce: {}'.format(account.nonce)
    message_hash = defunct_hash_message(text=original_message)
    signer = w3.eth.account.recoverHash(message_hash, signature=signature)

    if signer == public_address:
        account.nonce = account.generate_nonce()
        db.session.commit()
    else:
        abort(401, 'could not authenticate signature')

    access_token = create_access_token(identity=public_address)
    refresh_token = create_refresh_token(identity=public_address)


    return jsonify({
        'access_token': access_token,
        'refresh_token': refresh_token,
    }), 200

Und voila! Der Benutzer wird nun clientseitig mit einem JWT authentifiziert. Sie können Sitzungen und Autorisierungen für Routen im Backend wie gewohnt verwalten.

Wie das geht, habe ich in diesem Artikel gelernt: https://www.toptal.com/ethereum/one-click-login-flows-a-metamask-tutorial

Ich werde in next.js für weitere Referenzen erklären:

1- Der Server muss eine Nachricht erstellen, eine Sitzung erstellen und der Client wird dies anfordern und in Cookies speichern. Die Verwendung des npm-Pakets iron-session erleichtert das Erstellen einer Sitzung und das Speichern des Cookies in next.js.

  if (req.method === "GET") {
      try {
        // message can be anything. I use id as password
        const message = { contractAddress, id: uuidv4() };
        req.session.messageSession = {
          ...message,
        };
        await req.session.save();
        return res.json(message);
      } catch (error) {
        res.status(422).send({ message: "Cannot generate a message" });
      }

2- Der Benutzer stellt eine GETAnfrage. Wenn Sie ein nft erstellen, müssen Sie json-Daten und ein Bild senden, für beide müssen Sie den Verifizierungsprozess durchführen. Deshalb schreibe ich eine wiederverwendbare Funktion, um die Daten abzurufen und zu signieren:

const createSignedData = async () => {
    const messageToSign = await axios.get("/api/verify");
    const accounts = (await ethereum?.request({
      method: "eth_requestAccounts",
    })) as string[];
    // account will be the signer of this message
    const account = accounts[0];
    // password is the third param as uuid
    const signedData = await ethereum?.request({
      method: "personal_sign",
      params: [
        JSON.stringify(messageToSign.data),
        account,
        messageToSign.data.id,
      ],
    });
    return { signedData, account };
  };

3- Nachdem der Benutzer die signierten Daten erstellt hat, sendet er eine POSTAnfrage an den Server:

const createNft = async () => {
    try {
      const { account, signedData } = await createSignedData();
      await axios.post("/api/verify", {
        address: account,
        signature: signedData,
        nft: nftMeta,
      })
    } catch (error: any) {
      console.log("error in createnft", error);
    }
  };

4- Der Server erhält die signierten Daten, jetzt Überprüfungszeit. Dazu schreibe ich eine Middleware:

  export const addressVerificationMiddleware = async (
      req: NextApiRequest,
      res: NextApiResponse
    ) => {
      return new Promise(async (resolve, reject) => {
        const message = req.session.messageSession;
        // nonce is the representation of something that we are going to sign
        let nonce: string | Buffer =
          "\x19Ethereum Signed Message:\n" +
          JSON.stringify(message).length +
          JSON.stringify(message);
    
        nonce = util.keccak(Buffer.from(nonce, "utf-8"));
        const { v, r, s } = util.fromRpcSig(req.body.signature);
        // matching signature with the unsigned message
        const pubKey = util.ecrecover(util.toBuffer(nonce), v, r, s);
        const addressBuffer = util.pubToAddress(pubKey);
        const address = util.bufferToHex(addressBuffer);
        if (address === req.body.address) {
          resolve("Correct Address");
        } else {
          reject("Wrong Address");
        }
      });
    };

Ich erstelle einen Endpunkt und verwende die obige Middleware

if (req.method === "POST") {
      try {
        const { body } = req;
        const nft = body.nft as NftMeta;
        if (!nft.name || !nft.description || !nft.attributes) {
          return res.status(422).send({ message: "Form data is missing" });
        }
        // addressCheckMiddleware
        await addressVerificationMiddleware(req, res);
        const url = `https://api.pinata.cloud/pinning/pinJSONToIPFS`;
        const jsonResponse = await axios.post(
          url,
          {
            pinataMetadata: {
              name: uuidv4(),
            },
            pinataContent: nft,
          },
          {
            headers: {
              pinata_api_key: pinataApiKey,
              pinata_secret_api_key: pinataSecretApiKey,
            },
          }
        );
        return res.status(200).send(jsonResponse.data);
      } catch (error) {
        console.error("error in verify post req", error);
        res.status(422).send({ message: "Cannot create JSON" });
      }