Maison  >  Article  >  développement back-end  >  Un guide sur les signatures Starknet

Un guide sur les signatures Starknet

WBOY
WBOYoriginal
2024-07-21 11:13:49823parcourir

A guide on Starknet signatures

Abstrait

Cet article décrit le processus de signature et de vérification d'une signature sur Starknet. Cela commence par présenter l’abstraction de compte et comment elle modifie la vérification de signature par rapport aux blockchains traditionnelles comme Ethereum. Il fournit ensuite des exemples de code complets dans TypeScript et Go pour signer un message et vérifier une signature en utilisant deux méthodes disponibles sur Starknet : en utilisant la clé publique de l'utilisateur et en utilisant l'adresse du compte de l'utilisateur.

Un terrain de jeu de signatures en direct est disponible sur https://signatures.felts.xyz

Tous les exemples de code donnés dans cet article sont disponibles dans le dépôt GitHub associé. Je tiens à remercier Thiago pour son aide sur les extraits de code.

Abstraction du compte

Dans Ethereum, les comptes d'utilisateurs individuels, appelés comptes externes (EOA), sont contrôlés par une paire de clés privées et publiques. Les transactions nécessitent une signature de la clé privée pour modifier l'état du compte. Bien que sécurisé, ce système présente des inconvénients importants, tels qu'une perte irréversible d'actifs en cas de perte ou de vol de la clé privée, une fonctionnalité de portefeuille limitée et un manque d'options conviviales de récupération de clé ou de compte.

Starknet résout ces limitations grâce à Account Abstraction (AA), qui gère les comptes via des contrats intelligents au lieu de clés privées. Cette approche permet aux contrats intelligents de valider leurs transactions, permettant des fonctionnalités telles que les frais de gaz couverts par les contrats intelligents, plusieurs signataires pour un seul compte et diverses signatures cryptographiques. AA améliore la sécurité et l'expérience utilisateur en permettant aux développeurs de concevoir des modèles de sécurité personnalisés, tels que différentes clés pour les transactions courantes et de grande valeur et l'authentification biométrique pour une sécurité renforcée. Il simplifie également la récupération et la gestion des clés grâce à des méthodes telles que la récupération sociale et la signature de transactions matérielles. De plus, AA prend en charge la rotation des clés, les clés de session pour les applications Web3 et divers schémas de signature et de validation, permettant des mesures de sécurité sur mesure. En répondant aux limites inhérentes du modèle EOA d'Ethereum, l'AA de Starknet offre une approche plus flexible, sécurisée et conviviale de la gestion des comptes, améliorant considérablement les interactions blockchain.

Signature

Avec une compréhension de l'abstraction de compte, nous pouvons maintenant explorer comment elle modifie la vérification de signature. Tout d’abord, il est essentiel de comprendre la composition d’une signature. La courbe STARK est une courbe elliptique et ses signatures sont des signatures ECDSA, composées de deux valeurs : r et s. La signature est générée en signant un message avec la clé privée et peut être vérifiée à l'aide de la clé publique. Pour plus d'informations sur les signatures ECDSA, reportez-vous à la page Wikipédia.

Signer un message

Dans Starknet, les messages à signer suivent généralement le format EIP-712. Ce format de message comprend quatre champs obligatoires : types, PrimaryType, domaine et message. Le champ types mappe les noms de types à leurs définitions de types correspondantes. Le champ PrimaryType spécifie le type principal du message. Le champ de domaine contient des paires clé-valeur qui spécifient le contexte de la chaîne. Le champ de message comprend des paires clé-valeur qui décrivent le message. Nous représentons généralement le message sous forme d'objet JSON :

{
    types: {
        StarkNetDomain: [
            { name: "name", type: "felt" },
            { name: "chainId", type: "felt" },
            { name: "version", type: "felt" },
        ],
        Message: [{ name: "message", type: "felt" }],
    },
    primaryType: "Message",
    domain: {
        name: "MyDapp",
        chainId: "SN_MAIN",
        version: "0.0.1",
    },
    message: {
        message: "hello world!",
    },
}

Pour signer un message, vous avez besoin de la clé privée. Pour une compréhension approfondie du processus de signature, reportez-vous à l'algorithme de signature ECDSA. Ci-dessous le code pour signer un message.

TypeScript :

import { ec, encode, TypedData, Signer, typedData, WeierstrassSignatureType } from 'starknet';

//--------------------------------------------------------------------------
// Account
//--------------------------------------------------------------------------
const privateKey = '0x1234567890987654321';

const starknetPublicKey = ec.starkCurve.getStarkKey(privateKey);

const fullPublicKey = encode.addHexPrefix(
    encode.buf2hex(ec.starkCurve.getPublicKey(privateKey, false))
);

const pubX = starknetPublicKey
const pubY = encode.addHexPrefix(fullPublicKey.slice(68))

//--------------------------------------------------------------------------
// Message
//--------------------------------------------------------------------------

const messageStructure: TypedData = {
    types: {
        StarkNetDomain: [
            { name: "name", type: "felt" },
            { name: "chainId", type: "felt" },
            { name: "version", type: "felt" },
        ],
        Message: [{ name: "message", type: "felt" }],
    },
    primaryType: "Message",
    domain: {
        name: "MyDapp",
        chainId: "SN_MAIN",
        version: "0.0.1",
    },
    message: {
        message: "hello world!",
    },
};

const messageHash = typedData.getMessageHash(messageStructure, BigInt(starknetPublicKey))

//--------------------------------------------------------------------------
// Signature
//--------------------------------------------------------------------------

const signer = new Signer(privateKey)

let signature: WeierstrassSignatureType;
try {
    signature = (await signer.signMessage(messageStructure, starknetPublicKey)) as WeierstrassSignatureType
} catch (error) {
    console.error("Error signing the message:", error);
}

// signature has properties r and s

Allez :

package main

import (
    "fmt"
    "math/big"
    "strconv"

    "github.com/NethermindEth/starknet.go/curve"
    "github.com/NethermindEth/starknet.go/typed"
    "github.com/NethermindEth/starknet.go/utils"
)

// NOTE: at the time of writing, starknet.go forces us to create a custom
// message type as well as a method to format the message encoding since
// there is no built-in generic way to encode messages.
type MessageType struct {
    Message string
}

// FmtDefinitionEncoding is a method that formats the encoding of the message
func (m MessageType) FmtDefinitionEncoding(field string) (fmtEnc []*big.Int) {
    if field == "message" {
        if v, err := strconv.Atoi(m.Message); err == nil {
            fmtEnc = append(fmtEnc, big.NewInt(int64(v)))
        } else {
            fmtEnc = append(fmtEnc, utils.UTF8StrToBig(m.Message))
        }
    }
    return fmtEnc
}

func main() {
    //--------------------------------------------------------------------------
    // Account
    //--------------------------------------------------------------------------
    privateKey, _ := new(big.Int).SetString("1234567890987654321", 16)

    pubX, pubY, err := curve.Curve.PrivateToPoint(privateKey)
    if err != nil {
        fmt.Printf("Error: %s\n", err)
        return
    }
    if !curve.Curve.IsOnCurve(pubX, pubY) {
        fmt.Printf("Point is not on curve\n")
        return
    }

    starknetPublicKey := pubX

    // IMPORTANT: this is not a standard way to retrieve the full public key, it
    // is just for demonstration purposes as starknet.go does not provide a way
    // to retrieve the full public key at the time of writing.
    // Rule of thumb: never write your own cryptography code!
    fullPublicKey := new(big.Int).SetBytes(append(append(
        []byte{0x04},                       // 0x04 is the prefix for uncompressed public keys
        pubX.Bytes()...), pubY.Bytes()...), // concatenate x and y coordinates
    )

    //--------------------------------------------------------------------------
    // Message
    //--------------------------------------------------------------------------

    types := map[string]typed.TypeDef{
        "StarkNetDomain": {
            Definitions: []typed.Definition{
                {Name: "name", Type: "felt"},
                {Name: "chainId", Type: "felt"},
                {Name: "version", Type: "felt"},
            },
        },
        "Message": {
            Definitions: []typed.Definition{
                {Name: "message", Type: "felt"},
            },
        },
    }

    primaryType := "Message"

    domain := typed.Domain{
        Name:    "MyDapp",
        ChainId: "SN_MAIN",
        Version: "0.0.1",
    }

    message := MessageType{
        Message: "hello world!",
    }

    td, err := typed.NewTypedData(types, primaryType, domain)
    if err != nil {
        fmt.Println("Error creating TypedData:", err)
        return
    }

    hash, err := td.GetMessageHash(starknetPublicKey, message, curve.Curve)
    if err != nil {
        fmt.Println("Error getting message hash:", err)
        return
    }

    //--------------------------------------------------------------------------
    // Signature
    //--------------------------------------------------------------------------

    r, s, err := curve.Curve.Sign(hash, privateKey)
    if err != nil {
        fmt.Println("Error signing message:", err)
        return
    }
}

Si vous développez une dApp, vous n'aurez pas accès à la clé privée de l'utilisateur. Au lieu de cela, vous pouvez utiliser la bibliothèque starknet.js pour signer le message. Le code interagira avec le portefeuille du navigateur (généralement ArgentX ou Braavos) pour signer le message. Vous pouvez trouver une démo en direct sur https://signatures.felts.xyz. Voici le code simplifié pour signer un message en TypeScript à l'aide du wallet du navigateur (code complet disponible dans le dépôt GitHub) :

import { connect } from "get-starknet";

const starknet = await connect(); // Connect to the browser wallet

const messageStructure: TypedData = {
    types: {
        StarkNetDomain: [
            { name: "name", type: "felt" },
            { name: "chainId", type: "felt" },
            { name: "version", type: "felt" },
        ],
        Message: [{ name: "message", type: "felt" }],
    },
    primaryType: "Message",
    domain: {
        name: "MyDapp",
        chainId: "SN_MAIN",
        version: "0.0.1",
    },
    message: {
        message: "hello world!",
    },
};

// skipDeploy allows not-deployed accounts to sign messages
const signature = await starknet.account.signMessage(messageStructure, { skipDeploy: true });

Une fois le message signé, la signature est obtenue sous la forme de r, s et v. La valeur v est l'identifiant de récupération, qui peut être utilisé pour récupérer la clé publique de la signature (voir Wikipédia pour plus d'informations ). Cependant, ce processus de récupération ne peut pas être entièrement fiable pour vérifier une signature à moins que la clé publique du signataire ne soit connue au préalable. Les valeurs r et s sont les valeurs de signature utilisées pour vérifier la signature.

IMPORTANT : selon le portefeuille du navigateur, la signature peut renvoyer uniquement les valeurs r et s. La valeur v n'est pas toujours fournie.

Verifying a Signature

To verify a signature, the public key is required from a cryptographic perspective. However, due to Account Abstraction in Starknet, access to the public key is not always available. Currently, the public key cannot be retrieved through the browser wallet. Therefore, two methods are distinguished for verifying a signature: using the user's public key (if available) or using the user's address (i.e., account smart contract address).

Using the User's Public Key

If the user's public key is available, the signature can be verified using the public key. Here is the code to verify a signature.

TypeScript:

// following the previous code
const isValid = ec.starkCurve.verify(signature, messageHash, fullPublicKey)

Go:

// following the previous code
isValid := curve.Curve.Verify(hash, r, s, starknetPublicKey, pubY)

Using the User's Address

NOTE: This method works only if the user's account smart contract has been deployed (activated) on the Starknet network. This deployment is typically done through the browser wallet when the user creates an account and requires some gas fees. The skipDeploy parameter is specified in the JavaScript code when signing with the browser wallet. The example code provided earlier will not work with signatures different from the browser wallet since a sample private key was used to sign the message.

IMPORTANT: Avoid using your own private key when experimenting with the code. Always sign transactions with the browser wallet.

If the user's public key is not available, the signature can be verified using the user's account smart contract. By the standard SRC-6, the user account smart contract has a function fn is_valid_signature(hash: felt252, signature: Array) -> felt252; that takes the hash of the message and the signature (in the form of an array of 2 felt252 values: r and s) and returns the string VALID if the signature is valid, or fails otherwise. Here is the code to verify a signature using the user's account address in TypeScript and Go.

TypeScript (simplified for readability):

import { Account, RpcProvider } from "starknet";

const provider = new RpcProvider({ nodeUrl: "https://your-rpc-provider-url" });

// '0x123' is a placeholder for the user's private key since we don't have access to it
const account = new Account(provider, address, '0x123')

try {
    // messageStructure and signature are obtained from the previous code when signing the message with the browser wallet
    const isValid = account.verifyMessage(messageStructure, signature)
    console.log("Signature is valid:", isValid)
} catch (error) {
    console.error("Error verifying the signature:", error);
}

Go (simplified for readability):

import (
    "context"
    "encoding/hex"
    "fmt"
    "math/big"

    "github.com/NethermindEth/juno/core/felt"
    "github.com/NethermindEth/starknet.go/curve"
    "github.com/NethermindEth/starknet.go/rpc"
    "github.com/NethermindEth/starknet.go/utils"
)

...

provider, err := rpc.NewProvider("https://your-rpc-provider-url")
if err != nil {
    // handle error
}

// we import the account address, r, and s values from the frontend (typescript)
accountAddress, _ := new(big.Int).SetString("0xabc123", 16)
r, _ := new(big.Int).SetString("0xabc123", 16)
s, _ := new(big.Int).SetString("0xabc123", 16)

// we need to get the message hash, but, this time, we use the account address instead of the public key. `message` is the same as the in the previous Go code
hash, err := td.GetMessageHash(accountAddress, message, curve.Curve)
if err != nil {
    // handle error
}

callData := []*felt.Felt{
    utils.BigIntToFelt(hash),
    (&felt.Felt{}).SetUint64(2), // size of the array [r, s]
    utils.BigIntToFelt(r),
    utils.BigIntToFelt(s),
}

tx := rpc.FunctionCall{
    ContractAddress: utils.BigIntToFelt(accountAddress),
    EntryPointSelector: utils.GetSelectorFromNameFelt(
        "is_valid_signature",
    ),
    Calldata: callData,
}

result, err := provider.Call(context.Background(), tx, rpc.BlockID{Tag: "latest"})
if err != nil {
    // handle error
}

isValid, err := hex.DecodeString(result[0].Text(16))
if err != nil {
    // handle error
}

fmt.Println("Signature is valid:", string(isValid) == "VALID")

Usage

Signatures can be used in various applications, with user authentication in web3 dApps being a primary use case. To achieve this, use the structure provided above for signature verification using the user's account address. Here is the complete workflow:

  1. The user signs a message with the browser wallet.
  2. Send the user address, message, and signature (r, s) to the backend.
  3. The backend verifies the signature using the user's account smart contract.

Make sure that the message structure is the same on the frontend and backend to ensure the signature is verified correctly.

Conclusion

I hope that this article provided you with a comprehensive understanding of the signatures on Starknet and helped you implement it in your applications. If you have any questions or feedback, feel free to comment or reach out to me on Twitter or GitHub. Thank you for reading!

Sources:

  • https://book.starknet.io/ch04-00-account-abstraction.html
  • https://www.starknetjs.com/docs/guides/signature/
  • https://docs.starknet.io/architecture-and-concepts/accounts/introduction/
  • https://docs.openzeppelin.com/contracts-cairo/0.4.0/accounts#isvalidsignature
  • https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
  • https://eips.ethereum.org/EIPS/eip-712

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn