ARC-1: Algorand Wallet Transaction Signing API Source

An API for a function used to sign a list of transactions.

AuthorFabrice Benhamouda
Discussions-Tohttps://github.com/algorandfoundation/ARCs/issues/52
StatusFinal
TypeStandards Track
CategoryInterface
Created2021-07-06

Algorand Wallet Transaction Signing API

Abstract

The goal of this API is to propose a standard way for a dApp to request the signature of a list of transactions to an Algorand wallet. This document also includes detailed security requirements to reduce the risks of users being tricked to sign dangerous transactions. As the Algorand blockchain adds new features, these requirements may change.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC-2119.

Comments like this are non-normative.

Overview

This overview section is non-normative.

After this overview, the syntax of the interfaces are described followed by the semantics and the security requirements.

At a high-level the API allows to sign:

  • A valid group of transaction (aka atomic transfers).
  • (OPTIONAL) A list of groups of transactions.

Signatures are requested by calling a function signTxns(txns) on a list txns of transactions. The dApp may also provide an optional parameter opts.

Each transaction is represented by a WalletTransaction object. The only required field of a WalletTransaction is txn, a base64 encoding of the canonical msgpack encoding of the unsigned transaction. There are three main use cases:

  1. The transaction needs to be signed and the sender of the transaction is an account known by the wallet. This is the most common case. Example:
     {
         "txn": "iaNhbXT..."
     }
    

    The wallet is free to generate the resulting signed transaction in any way it wants. In particular, the signature may be a multisig, may involve rekeying, or for very advanced wallets may use logicsigs.

    Remark: If the wallet uses a large logicsig to sign the transaction and there is congestion, the fee estimated by the dApp may be too low. A future standard may provide a wallet API allowing the dApp to compute correctly the estimated fee. Before such a standard, the dApp may need to retry with a higher fee when this issue arises.

  2. The transaction does not need to be signed. This happens when the transaction is part of a group of transaction and is signed by another party or by a logicsig. In that case, the field signers is set to an empty array. Example:
     {
         "txn": "iaNhbXT...",
         "signers": []
     }
    
  3. (OPTIONAL) The transaction needs to be signed but the sender of the transaction is not an account known by the wallet. This happens when the dApp uses a sender account derived from one or more accounts of the wallet. For example, the sender account may be a multisig account with public keys corresponding to some accounts of the wallet, or the sender account may be rekeyed to an account of the wallet. Example:
     {
         "txn": "iaNhbXT...",
         "authAddr": "HOLQV2G65F6PFM36MEUKZVHK3XM7UEIFLG35UJGND77YDXHKXHKX4UXUQU",
         "msig": {
             "version": 1,
             "threshold": 2,
             "addrs": [
                 "5MF575NQUDMRWOTS27KIBL2MFPJHKQEEF4LZEN6H3CZDAYVUKESMGZPK3Q",
                 "FS7G3AHTDVMQNQQBHZYMGNWAX7NV2XAQSACQH3QDBDOW66DYTAQQW76RYA",
                 "DRSHY5ONWKVMWWASTB7HOELVF5HRUTRQGK53ZK3YNMESZJR6BBLMNH4BBY"
             ]
         },
         "signers": ...
     }
    

    Note that in both the first and the third use cases, the wallet may sign the transaction using a multisig and may use a different authorized address (authAddr) than the sender address (i.e., rekeying). The main difference is that in the first case, the wallet knows how to sign the transaction (i.e., whether the sender address is a multisig and/or rekeyed), while in the third case, the wallet may not know it.

Syntax and Interfaces

Interfaces are defined in TypeScript. All the objects that are defined are valid JSON objects.

Interface SignTxnsFunction

A wallet transaction signing function signTxns is defined by the following interface:

export type SignTxnsFunction = (
   txns: WalletTransaction[],
   opts?: SignTxnsOpts
)
   => Promise<(SignedTxnStr | null)[]>;

where:

  • txns is a non-empty list of WalletTransaction objects (defined below).
  • opts is an optional parameter object SignTxnsOpts (defined below).

In case of error, the wallet (i.e., the signTxns function in this document) MUST reject the promise with an error object SignTxnsError defined below. This ARC uses interchangeably the terms “throw an error” and “reject a promise with an error”.

Interface AlgorandAddress

An Algorand address is represented by a 58-character base32 string. It includes the checksum.

export type AlgorandAddress = string;

An Algorand address is valid is it is a valid base32 string without padding and if the checksum is valid.

Example: "6BJ32SU3ABLWSBND7U5H2QICQ6GGXVD7AXSSMRYM2GO3RRNHCZIUT4ISAQ" is a valid Algorand address.

Interface SignedTxnStr

SignedTxnStr is the base64 encoding of the canonical msgpack encoding of a SignedTxn object, as defined in the Algorand specs. For Algorand version 2.5.5, see the authorization and signatures Section of the specs or the Go structure

export type SignedTxnStr = string;

Interface MultisigMetadata

A MultisigMetadata object specifies the parameters of an Algorand multisig address.

export interface MultisigMetadata {
   /**
    * Multisig version.
    */
   version: number;

   /**
    * Multisig threshold value. Authorization requires a subset of signatures,
    * equal to or greater than the threshold value.
    */
   threshold: number;

   /**
    * List of Algorand addresses of possible signers for this
    * multisig. Order is important.
    */
   addrs: AlgorandAddress[];
}
  • version should always be 1.
  • threshold should be between 1 and the length of addrs.

Interface originally from github.com/algorand/js-algorand-sdk/blob/e07d99a2b6bd91c4c19704f107cfca398aeb9619/src/types/multisig.ts, where string has been replaced by AlgorandAddress.

Interface WalletTransaction

A WalletTransaction object represents a transaction to be signed by a wallet.

export interface WalletTransaction {
   /**
    * Base64 encoding of the canonical msgpack encoding of a Transaction.
    */
   txn: string;

   /**
    * Optional authorized address used to sign the transaction when the account
    * is rekeyed. Also called the signor/sgnr.
    */
   authAddr?: AlgorandAddress;

   /**
    * Multisig metadata used to sign the transaction
    */
   msig?: MultisigMetadata;

   /**
    * Optional list of addresses that must sign the transactions
    */
   signers?: AlgorandAddress[];

   /**
    * Optional base64 encoding of the canonical msgpack encoding of a
    * SignedTxn corresponding to txn, when signers=[]
    */
   stxn?: SignedTxnStr;

   /**
    * Optional message explaining the reason of the transaction
    */
   message?: string;

   /**
    * Optional message explaining the reason of this group of transaction
    * Field only allowed in the first transaction of a group
    */
   groupMessage?: string;
}

Interface SignTxnsOpts

A SignTxnsOps specifies optional parameters of the signTxns function:

export type SignTxnsOpts = {
   /**
    * Optional message explaining the reason of the group of transactions
    */
   message?: string;
}

Error Interface SignTxnsError

In case of error, the signTxns function MUST return a SignTxnsError object

interface SignTxnsError extends Error {
  code: number;
  data?: any;
}

where:

  • message:
    • MUST be a human-readable string
    • SHOULD adhere to the specifications in the Error Standards section below
  • code:
    • MUST be an integer number
    • MUST adhere to the specifications in the Error Standards section below
  • data:
    • SHOULD contain any other useful information about the error

Inspired from github.com/ethereum/EIPs/blob/master/EIPS/eip-1193.md

Error Standards

Status Code Name Description
4001 User Rejected Request The user rejected the request.
4100 Unauthorized The requested operation and/or account has not been authorized by the user.
4200 Unsupported Operation The wallet does not support the requested operation.
4201 Too Many Transactions The wallet does not support signing that many transactions at a time.
4202 Uninitialized Wallet The wallet was not initialized properly beforehand.
4300 Invalid Input The input provided is invalid.

Wallet-specific extensions

Wallets MAY use specific extension fields in WalletTransaction and in SignTxnsOpts. These fields must start with: _walletName, where walletName is the name of the wallet. Wallet designers SHOULD ensure that their wallet name is not already used.

Example of a wallet-specific fields in opts (for the wallet theBestAlgorandWallet): _theBestAlgorandWalletIcon for displaying an icon related to the transactions.

Wallet-specific extensions MUST be designed such that a wallet not understanding them would not provide a lower security level.

Example of a forbidden wallet-specific field in WalletTransaction: _theWorstAlgorandWalletDisable requires this transaction not to be signed. This is dangerous for security as any signed transaction may leak and be committed by an attacker. Therefore, the dApp should never submit transactions that should not be signed, and that some wallets (not supporting this extension) may still sign.

Semantic and Security Requirements

The call signTxns(txns, opts) MUST either throws an error or return an array ret of the same length of the txns array:

  1. If txns[i].signers is an empty array, the wallet MUST NOT sign the transaction txns[i], and:
    • if txns[i].stxn is not present, ret[i] MUST be set to null.
    • if txns[i].stxn is present and is a valid SignedTxnStr with the underlying transaction exactly matching txns[i].txn, ret[i] MUST be set to txns[i].stxn. (See section on the semantic of WalletTransaction for the exact requirements on txns[i].stxn.)
    • otherwise, the wallet MUST throw a 4300 error.
  2. Otherwise, the wallet MUST sign the transaction txns[i].txn and ret[i] MUST be set to the corresponding SignedTxnStr.

Note that if any transaction txns[i] that should be signed (i.e., where txns[i].signers is not an empty array) cannot be signed for any reason, the wallet MUST throw an error.

Terminology: Validation, Warnings, Fields

All the field names below are the ones in the Go SignedTxn structure and . Field of the actual transaction are prefixed with txn. (as opposed to fields of the WalletTransaction such as signers). For example, the sender of a transaction is txn.Sender.

Rejecting means throwing a 4300 error.

Strong warning / warning / weak warning / informational messages are different level of alerts. Strong warnings MUST be displayed in such a way that the user cannot miss the importance of them.

Semantic of WalletTransaction

  • txn:
    • Must a base64 encoding of the canonical msgpack encoding of a Transaction object as defined in the Algorand specs. For Algorand version 2.5.5, see the authorization and signatures Section of the specs or the Go structure.
    • If txn is not a base64 string or cannot be decoded into a Transaction object, the wallet MUST reject.
  • authAddr:
    • The wallet MAY not support this field. In that case, it MUST throw a 4200 error.
    • If specified, it must be a valid Algorand address. If this is not the case, the wallet MUST reject.
    • If specified and supported, the wallet MUST sign the transaction using this authorized address even if it sees the sender address txn.Sender was not rekeyed to authAddr. This is because the sender may be rekeyed before the transaction is committed. The wallet SHOULD display an informational message.
  • msig:
    • The wallet MAY not support this field. In that case, it MUST throw a 4200 error.
    • If specified, it must be a valid MultisigMetadata object. If this is not the case, the wallet MUST reject.
    • If specified and supported, the wallet MUST verify msig matches authAddr (if authAddr is specified and supported) or the sender address txn.Sender (otherwise). The wallet MUST reject if this is not the case.
    • If specified and supported and if signers is not specified, the wallet MUST return a SignedTxn with all the subsigs that it can provide and that the wallet user agrees to provide. If the wallet can sign more subsigs than the requested threshold (msig.threshold), it MAY only provide msig.threshold subsigs. It is also possible that the wallet cannot provide at least msig.threshold subsigs (either because the user prevented signing with some keys or because the wallet does not know enough keys). In that case, the wallet just provide the subsigs it can provide. However, the wallet MUST provide at least one subsig or throw an error.
  • signers:
    • If specified and if not a list of valid Algorand addresses, the wallet MUST reject.
    • If signers is an empty array, the transaction is for information purpose only and the wallet SHALL NOT sign it, even if it can (e.g., know the secret key of the sender address).
    • If signers is an array with more than 1 Algorand addresses:
      • The wallet MUST reject if msig is not specified.
      • The wallet MUST reject if signers is not a subset of msig.addrs.
      • The wallet MUST try to return a SignedTxn with all the subsigs corresponding to signers signed. If it cannot, it SHOULD throw a 4001 error. Note that this is different than when signers is not provided, where the signing is only “best effort”.
    • If signers is an array with a single Algorand address:
      • If msig is specified, the rules as when signers is an array with more than 1 Algorand addresses apply.
      • If authAddr is specified but msig is not, the wallet MUST reject if signers[0] is not equal to authAddr.
      • If neither authAddr nor msig are specified, the wallet MUST reject if signers[0] is not the sender address txn.Sender.
      • In all cases, the wallet MUST only try to provide signatures for signers[0]. In particular, if the sender address txn.Sender was rekeyed or is a multisig and if authAddr and msig are not specified, then the wallet MUST reject.
  • stxn if specified:
    • If specified and if signers is not the empty array, the wallet MUST reject.
    • If specified:
      • It must be a valid SignedTxnStr. The wallet MUST reject if this is not the case.
      • The wallet MUST reject if the field txn inside the SignedTxn object does not match exactly the Transaction object in txn.
      • The wallet MAY NOT check whether the other fields of the SignedTxn are valid. In particular, it MAY accept stxn even in the following cases: it contains an invalid signature sig, it contains both a signature sig and a logicsig lsig, it contains a logicsig lsig that always reject.
  • message:
    • The wallet MAY decide to never print the message, to only print the first characters, or to make any changes to the messages that may be used to ensure a higher level of security. The wallet MUST be designed to ensure that the message cannot be easily used to trick the user to do an incorrect action. In particular, if displayed, the message must appear in an area that is easily and clearly identifiable as not trusted by the wallet.
    • The wallet MUST prevent HTML/JS injection and must only display plaintext messages.
  • groupMessage obeys the same rules as message, except it is a message common to all the transactions of the group containing the current transaction. In addition, the wallet MUST reject if groupMessage is provided for a transaction that is not the first transaction of the group. Note that txns may contain multiple groups of transactions, one after the other (see the Group Validation section for details).
Particular Case without signers, nor msig, nor senders

When neither signers, nor msig, nor authAddr are specified, the wallet MAY still sign the transaction using a multisig or a different authorized address than the sender address txn.Sender. It may also sign the transaction using a logicsig.

However, in all these cases, the resulting SignedTxn MUST be such that it can be committed to the blockchain (assuming the transaction itself can be executed and that the account is not rekeyed in the meantime).

In particular, if a multisig is used, the numbers of subsigs provided must be at least equal to the multisig threshold. This is different from the case where msig is provided, where the wallet MAY provide fewer subsigs than the threshold.

Semantic of SignTxnsOpts

  • message obeys the rules as WalletTransaction.message except it is a message common to all transactions.

General Validation

The goal is to ensure the highest level of security for the end-user, even when the transaction is generated by a malicious dApp. Every input must be validated.

Validation:

  • SHALL NOT rely on TypeScript typing as this can be bypassed. Types MUST be manually verified.
  • SHALL NOT assume the Algorand SDK does any validation, as the Algorand SDK is not meant to receive maliciously generated inputs. Furthermore, the SDK allows for dangerous transactions (such as rekeying). The only exception for the above rule is for de-serialization of transactions. Once de-serialized, every field of the transaction must be manually validated.

Note: We will be working with the algosdk team to provide helper functions for validation in some cases and to ensure the security of the de-serialization of potentially malicious transactions.

If there is any unexpected field at any level (both in the transaction itself or in the object WalletTransaction), the wallet MUST immediately reject. The only exception is for the “wallet-specific extension” fields (see above).

Group Validation

The wallet should support the following two use cases:

  1. (REQUIRED) txns is a non-empty array of transactions that belong to the same group of transactions. In other words, either txns is an array of a single transaction with a zero group ID (txn.Group), or txns is an array of one or more transactions with the same non-zero group ID. The wallet MUST reject if the transactions do not match their group ID. (The dApp must provide the transactions in the order defined by the group ID.)

    An early draft of this ARC required that the size of a group of transactions must be greater than 1 but, since the Algorand protocol supports groups of size 1, this requirement had been changed so dApps don’t have to have special cases for single transactions and can always send a group to the wallet.

  2. (OPTIONAL) txns is a concatenation of txns arrays of transactions of type 1:
    • All transactions with the same non-zero group ID must be consecutive and must match their group ID. The wallet MUST reject if the above is not satisfied.
    • The wallet UI MUST be designed so that it is clear to the user when transactions are grouped (aka form an atomic transfers) and when they are not. It SHOULD provide very clear explanations that are understandable by beginner users, so that they cannot easily be tricked to sign what they believe is an atomic exchange while it is in actuality a one-sided payment.

If txns does not match any of the formats above, the wallet MUST reject.

The wallet MAY choose to restrict the maximum size of the array txns. The maximum size allowed by a wallet MUST be at least the maximum size of a group of transactions in the current Algorand protocol on MainNet. (When this ARC was published, this maximum size was 16.) If the wallet rejects txns because of its size, it MUST throw a 4201 error.

An early draft of this API allowed to sign single transactions in a group without providing the other transactions in the group. For security reasons, this use case is now deprecated and SHALL not be allowed in new implementations. Existing implementations may continue allowing for single transactions to be signed if a very clear warning is displayed to the user. The warning MUST stress that signing the transaction may incur losses that are much higher than the amount of tokens indicated in the transaction. That is because potential future features of Algorand may later have such consequences (e.g., a signature of a transaction may actually authorize the full group under some circumstances).

Transaction Validation

Inputs that Must Be Systematically Rejected
  • Transactions WalletTransaction.txn with fields that are not known by the wallet MUST be systematically rejected. In particular:
    • Every field MUST be validated.
    • Any extra field MUST systematically make the wallet reject.
    • This is to prevent any security issue in case of the introduction of new dangerous fields (such as txn.RekeyTo or txn.CloseRemainderTo).
  • Transactions of an unknown type (field txn.Type) MUST be rejected.
  • Transactions containing fields of a different transaction type (e.g., txn.Receiver in an asset transfer transaction) MUST be rejected.
Inputs that Warrant Display of Warnings

The wallet MUST:

  • Display a strong warning message when signing a transaction with one of the following fields: txn.RekeyTo, txn.CloseRemainderTo, txn.AssetCloseTo. The warning message MUST clearly explain the risks. No warning message is necessary for transactions that are provided for informational purposes in a group and are not signed (i.e., transactions with signers=[]).
  • Display a strong warning message in case the transaction is signed in the future (first valid round is after current round plus some number, e.g. 500). This is to prevent surprises in the future where a user forgot that they signed a transaction and the dApp maliciously play it later.
  • Display a warning message when the fee is too high. The threshold MAY depend on the load of the Algorand network.
  • Display a weak warning message when signing a transaction that can increase the minimum balance in a way that may be hard or impossible to undo (asset creation or application creation)
  • Display an informational message when signing a transaction that can increase the minimum balance in a way that can be undone (opt-in to asset or transaction)

The above is for version 2.5.6 of the Algorand software. Future consensus versions may require additional checks.

Before supporting any new transaction field or type (for a new version of the Algorand blockchain), the wallet authors MUST be perform a careful security analysis.

Genesis Validation

The wallet MUST check that the genesis hash (field txn.GenesisHash) and the genesis ID (field txn.GenesisID, if provided) match the network used by the wallet. If the wallet supports multiple networks, it MUST make clear to the user which network is used.

UI

In general, the UI MUST ensure that the user cannot be confused by the dApp to perform dangerous operations. In particular, the wallet MUST make clear to the user what is part of the wallet UI from what is part of what the dApp provided.

Special care MUST be taken of when:

  • Displaying the message field of WalletTransaction and of SignTxnsOpts.
  • Displaying any arbitrary field of transactions including note field (txn.Note), genesis ID (txn.genesisID), asset configuration fields (txn.AssetName, txn.UnitName, txn.URL, …)
  • Displaying message hidden in fields that are expected to be base32/base64-strings or addresses. Using a different font for those fields MAY be an option to prevent such confusion.

Usual precautions MUST be taken regarding the fact that the inputs are provided by an untrusted dApp (e.g., preventing code injection and so on).

Rationale

The API was designed to:

  • Be easily implementable by all Algorand wallets
  • Rely on the official specs and the official source code.
  • Only use types supported by JSON to simplify interoperability (avoid Uint8Array for example) and to allow easy serialization / deserialization
  • Be easy to extend to support future features of Algorand
  • Be secure by design: making it hard for malicious dApps to cause the wallet to sign a transaction without the user understanding the implications of their signature.

The API was not designed to:

  • Directly support of the SDK objects. SDK objects must first be serialized.
  • Support any listing accounts, connecting to the wallet, sending transactions, …
  • Support of signing logic signatures.

The last two items are expected to be defined in other documents.

Rationale for Group Validation

The requirements around group validation have been designed to prevent the following attack.

The dApp pretends to buy 1 Algo for 10 USDC, but instead creates an atomic transfer with the user sending 1 Algo to the dApp and the dApp sending 0.01 USDC to the user. However, it sends to the wallet a 1 Algo and 10 USDC transactions. If the wallet does not verify that this is a valid group, it will make the user believe that they are signing for the correct atomic transfer.

Reference Implementation

This section is non-normative.

Sign a Group of Two Transactions

Here is an example in node.js how to use the wallet interface to sign a group of two transactions and send them to the network. The function signTxns is assumed to be a method of algorandWallet.

Note: We will be working with the algosdk development to add two helper functions to facilitate the use of the wallet. Current idea is to add: Transaction.toBase64 that does the same as Transaction.toByte except it outputs a base64 string Algodv2.sendBase64RawTransactions that does the same as Algodv2.sendRawTransactions except it takes an array of base64 string instead of an array of Uint8array

import algosdk from 'algosdk';
import * as algorandWallet from './wallet';
import {Buffer} from "buffer";

const firstRound = 13809129;

const suggestedParams = {
   flatFee: false,
   fee: 0,
   firstRound: firstRound,
   lastRound: firstRound + 1000,
   genesisID: 'testnet-v1.0',
   genesisHash: 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI='
};

const txn1 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
   from: "37MSZIPXHGNCKTDJTJDSYIOF4C57JAL2FTKESD2HBVELXYHEIXVZ4JVGFU",
   to: "PKSE2TARC645D4O2IO6QNWVW6PLJDTR6IOKNKMGSHQL7JIJHNGNFVISUHI",
   amount: 1000,
   suggestedParams,
});

const txn2 = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
   from: "37MSZIPXHGNCKTDJTJDSYIOF4C57JAL2FTKESD2HBVELXYHEIXVZ4JVGFU",
   to: "PKSE2TARC645D4O2IO6QNWVW6PLJDTR6IOKNKMGSHQL7JIJHNGNFVISUHI",
   amount: 2000,
   suggestedParams,
});

const txs = [txn1, txn2];
algosdk.assignGroupID(txs);

const txn1B64 = Buffer.from(txn1.toByte()).toString("base64");
const txn2B64 = Buffer.from(txn2.toByte()).toString("base64");

(async () => {
   const signedTxs = await algorandWallet.signTxns([
       {txn: txn1B64},
       {txn: txn2B64, signers: []}
   ]);

   const algodClient = new algosdk.Algodv2("", "...", "");

   algodClient.sendRawTransaction(
       signedTxs.map(stxB64 => Buffer.from(stxB64, "base64"))
   )
})();

Security Considerations

None.

Copyright and related rights waived via CCO.

Citation

Please cite this document as:

Fabrice Benhamouda, "ARC-1: Algorand Wallet Transaction Signing API," Algorand Requests for Comments, no. 1, July 2021. [Online serial]. Available: https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0001.md.