This is an attempt to specify an encoding for a ‘sign-in’ data standard to enable a Sign in with Starknet for websites, services and accounts.
Context
We need a standard sign-in message and data format for Starknet similar to eip-4361 in the Ethereum ecosystem. Since Starknet is a green field, we/nethermind would like to take the opportunity to improve upon eip-4361 by taking inspiration from eip-712: we will use structured JSON data to enable strong typing and better user communication. This proposal uses starknet.js
’s eip-712 style off-chain signing, however it means we are restricted to 31 character strings, more discussion on this is below. We will make this into a SNIP after some discussion on the forum.
Since Starknet supports native account abstraction the sign in with Starknet functionality is even more powerful than in Ethereum. Any message hash associated with any piece of data which passes an account’s isValidSignature
method is considered a valid signature. This means that:
- Users can revoke signatures simply by changing their account keys
- Any signature method invented in the future is supported without any changes required to the scheme(or even using secp256r1 <> FaceID)
- Novel signing methods can implement off-chain signing without requiring any special support from the target domain - an off-chain multisig, for example.
There have been a few previous attempts and foundational steps made by the community already:
- GitHub - Web3Auth/sign-in-with-starkware
- EIP712-cairo-article/src/validate.cairo at main · software-mansion-labs/EIP712-cairo-article · GitHub
- Feature/typed structured data hashing and signing by janek26 · Pull Request #87 · starknet-io/starknet.js · GitHub
Definitions
- Contract Address: a unique identifier of a contract on Starknet
- Account: An abstract account on Starknet identified by a Contract Address
- User: An owner of an account on Starknet
- Challenger: A domain, app or backend wishing to authenticate a User as the owner of an Account
Specification
Sign in with Starknet will work as follows.
- The challenger prepares a challenge which a User must sign to authenticate the user as the owner of some account on Starknet. The sign-in JSON object following the
starknet.js
standard, includes:
- the domain requesting the signature
- the address of the contract against which the signature will be verified
- a statement by the Domain to be presented to the User
- the version of the domain/app
- the version of the message
- the chain-id
- the URI for scoping
- a nonce acceptable to the domain/app
- and the issued-at timestampThese fields are defined precisely below.
- The challenge is presented to the User by the wallet through a signature request. The wallet MUST present this challenge cleanly and SHOULD validate the challenge in some way to protect the user from phishing attacks. This is left open for now, we invite those interested in preventing this to comment below.
- The User signs the message if they find it acceptable.
- The signed sign-in data is returned to the challenger, who uses an RPC call to the account’s
isValidSignature
method to verify the signature’s authenticity. Note that due to account abstraction, the challenger cannot predict the signature scheme; the challenger MUST interact with an up-to-date Starknet Node to authenticate the message successfully.
Data format
We propose the following sign-in data format per the spec in starknet.js. We define the two JSON objects, domain and message, that the challenger MUST populate.
domain
chainId
: can be one of these three values ‘SN_GOERLI’ | ‘SN_GOERLI2’ | ‘SN_MAIN’name
: name of the dApp limited to 31 ASCII characters OR the RFC 4501 DNS authority that is requesting the signing, limited to 31 Characters - OPEN QUESTION, please commentversion
: version of the app in the form of x.y.z Limited to 31 characters
message
address
: Starknet Contract Address to validate againstissuedAt
: ISO 8601 datetime string of the current time, limited to 31 Charactersnonce
: Randomized token used to prevent replay attacks, at least eight alphanumeric characters. Limited to 31 charactersstatement
: Human-readable ASCII assertion that the user will sign. Limited to 31 Charactersuri
: RFC 3986 URI referring to the resource that is the subject of the signing. Limited to 31 Charactersversion
: version of the message in x.y.x form. Note that this is the message version, not the app domain version mentioned above. Limited to 31 charactersexpirationTime
: optional, ISO 8601 datetime string that, if present, indicates when the signed authentication message is no longer valid. Limited to 31 charactersnotBefore
: optional, ISO 8601 datetime string that, if present, indicates when the signed authentication message will become valid. Limited to 31 characters
Example Payload/Data
{
"domain": {
"version": "0.0.1",
"chainId": "SN_GOERLI",
"name": "siws.nethermind.io"
},
"message": {
"address": "0x1c62c52c1709acb3eb9195594e39c04323658463cfe0c641e39b99a83ba11a1",
"statement": "Please Sign in",
"uri": "<https://siws.nethermind.io>",
"version": "0.0.5",
"nonce": "FIrowza8YUhluXSrb",
"issuedAt": "2023-06-27T21:19:52.929Z"
},
"primaryType": "Message",
"types": {
"Message": [
{
"name": "address",
"type": "felt"
},
{
"name": "statement",
"type": "string"
},
{
"name": "uri",
"type": "string"
},
{
"name": "nonce",
"type": "string"
},
{
"name": "issuedAt",
"type": "string"
},
{
"name": "version",
"type": "felt"
}
],
"StarkNetDomain": [
{
"name": "name",
"type": "string"
},
{
"name": "chainId",
"type": "string"
},
{
"name": "version",
"type": "string"
}
]
}
}
JSON Schema :
{
"$schema": "<https://json-schema.org/draft/2020-12/schema>",
"type": "object",
"properties": {
"domain": {
"type": "object",
"properties": {
"chainId": {
"type": "string",
"enum": ["SN_GOERLI", "SN_GOERLI2", "SN_MAIN"],
"errorMessage": "ChainId must be one of 'SN_GOERLI', 'SN_GOERLI2', 'SN_MAIN'"
},
"name": {
"type": "string",
"maxLength": 31,
"errorMessage": "Name must be a string and cannot exceed 31 characters"
},
"version": {
"type": "string",
"maxLength": 31,
"pattern": "^\\\\d+\\\\.\\\\d+\\\\.\\\\d+$",
"errorMessage": "Version must be a string in the format x.y.z"
}
},
"required": ["chainId", "name", "version"],
"additionalProperties": false,
"errorMessage": "Domain must include chainId, name, version"
},
"message": {
"type": "object",
"properties": {
"version": {
"type": "string",
"maxLength": 31,
"pattern": "^\\\\d+\\\\.\\\\d+\\\\.\\\\d+$",
"errorMessage": "Version must be a string in the format x.y.z"
},
"address": {
"type": "string",
"pattern": "^0x[a-fA-F0-9]{63,64}$",
"errorMessage": "Address must be a hexadecimal string with 66 characters, including the '0x' prefix"
},
"issuedAt": {
"type": "string",
"format": "date-time",
"errorMessage": "IssuedAt must be a valid date-time string"
},
"nonce": {
"type": "string",
"minLength": 8,
"maxLength": 31,
"pattern": "^[a-zA-Z0-9]*$",
"errorMessage": "Nonce must be an alphanumeric string between 8 and 31 characters"
},
"statement": {
"type": "string",
"maxLength": 31,
"errorMessage": "Statement must be a string and cannot exceed 31 characters"
},
"uri": {
"type": "string",
"format": "uri",
"errorMessage": "Uri must be a valid URI string"
},
"expirationTime": {
"type": "string",
"format": "date-time",
"errorMessage": "ExpirationTime, if present, must be a valid date-time string"
},
"notBefore": {
"type": "string",
"format": "date-time",
"errorMessage": "NotBefore, if present, must be a valid date-time string"
}
},
"required": ["address", "issuedAt", "nonce", "statement", "uri", "version"],
"additionalProperties": false,
"errorMessage": "Message must include address, issuedAt, nonce, statement, uri, version"
},
"primaryType": {
"type": "string",
"const": "Message",
"errorMessage": "PrimaryType must be 'Message'"
},
"types": {
"type": "object",
"properties": {
"Message": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [ "version" ,"address", "statement", "uri", "nonce", "issuedAt", "expirationTime", "notBefore"],
"errorMessage": "Name must be one of 'version', 'address', 'statement', 'uri', 'nonce', 'issuedAt', 'expirationTime', 'notBefore'"
},
"type": {
"type": "string",
"enum": ["string", "felt"],
"errorMessage": "Type must be either 'string' or 'felt'"
}
},
"required": ["name", "type"],
"additionalProperties": false,
"errorMessage": "Items must include name and type"
},
"minItems": 6,
"maxItems": 8,
"uniqueItems": true,
"errorMessage": "Message must contain min 6-8 unique items "
},
"StarkNetDomain": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": ["name", "chainId", "version"],
"errorMessage": "Name must be one of 'name', 'chainId', 'version'"
},
"type": {
"type": "string",
"enum": ["felt", "string"],
"errorMessage": "Type must be 'felt' or 'string'"
}
},
"required": ["name", "type"],
"additionalProperties": false,
"errorMessage": "Items must include name and type"
},
"minItems": 3,
"maxItems": 3,
"uniqueItems": true,
"errorMessage": "StarkNetDomain must contain exactly 3 unique items"
}
},
"required": ["Message", "StarkNetDomain"],
"additionalProperties": false,
"errorMessage": "Types must include Message and StarkNetDomain"
}
},
"required": ["domain", "message", "primaryType", "types"],
"additionalProperties": false,
"errorMessage": "Data must include domain, message, primaryType, types"
}
We have made available a typescript library to help generate and validate the sign-in message and also an example of the implementation at https://siws.nethermind.io. GitHub - NethermindEth/sign-in-with-starknet: Reference implementation to use Starknet accounts to sign in
Areas for improvement
- The 31-character limit of the message data in starknet.js signing standard significantly limits the statement and domain. We want to develop a string representation in Starknet, which is unbounded and efficient; we invite the community or Nethermind to take this on.
Next steps
- Wallets need to support this standard to present it cleanly to users. However, this is not required for this standard to be usable.
- Update
starknet.js
so thatStarkNetDomain
isStarknetDomain
- Include
isValidSignature
in the account standard, as__is_valid_signature__
, make it so that all accounts must be deployed with this method. Accounts which do not support off-chain signing should simply make this call revert. - Create an SNIP with this proposal