ARC-1: Algorand Wallet Transaction Signing API
An API for a function used to sign a list of transactions.
Author | Fabrice Benhamouda |
---|---|
Discussions-To | https://github.com/algorandfoundation/ARCs/issues/52 |
Status | Final |
Type | Standards Track |
Category | Interface |
Created | 2021-07-06 |
Table of Contents
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:
- 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.
- 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": [] }
- (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 ofWalletTransaction
objects (defined below).opts
is an optional parameter objectSignTxnsOpts
(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 ofaddrs
.
Interface originally from github.com/algorand/js-algorand-sdk/blob/e07d99a2b6bd91c4c19704f107cfca398aeb9619/src/types/multisig.ts, where
string
has been replaced byAlgorandAddress
.
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 wallettheBestAlgorandWallet
):_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:
- If
txns[i].signers
is an empty array, the wallet MUST NOT sign the transactiontxns[i]
, and:- if
txns[i].stxn
is not present,ret[i]
MUST be set tonull
. - if
txns[i].stxn
is present and is a validSignedTxnStr
with the underlying transaction exactly matchingtxns[i].txn
,ret[i]
MUST be set totxns[i].stxn
. (See section on the semantic ofWalletTransaction
for the exact requirements ontxns[i].stxn
.) - otherwise, the wallet MUST throw a
4300
error.
- if
- Otherwise, the wallet MUST sign the transaction
txns[i].txn
andret[i]
MUST be set to the correspondingSignedTxnStr
.
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 aTransaction
object, the wallet MUST reject.
- Must a base64 encoding of the canonical msgpack encoding of a
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 toauthAddr
. This is because the sender may be rekeyed before the transaction is committed. The wallet SHOULD display an informational message.
- The wallet MAY not support this field. In that case, it MUST throw a
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
matchesauthAddr
(ifauthAddr
is specified and supported) or the sender addresstxn.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 aSignedTxn
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 providemsig.threshold
subsigs. It is also possible that the wallet cannot provide at leastmsig.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.
- The wallet MAY not support this field. In that case, it MUST throw a
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 ofmsig.addrs
. - The wallet MUST try to return a
SignedTxn
with all the subsigs corresponding tosigners
signed. If it cannot, it SHOULD throw a4001
error. Note that this is different than whensigners
is not provided, where the signing is only “best effort”.
- The wallet MUST reject if
- If
signers
is an array with a single Algorand address:- If
msig
is specified, the rules as whensigners
is an array with more than 1 Algorand addresses apply. - If
authAddr
is specified butmsig
is not, the wallet MUST reject ifsigners[0]
is not equal toauthAddr
. - If neither
authAddr
normsig
are specified, the wallet MUST reject ifsigners[0]
is not the sender addresstxn.Sender
. - In all cases, the wallet MUST only try to provide signatures for
signers[0]
. In particular, if the sender addresstxn.Sender
was rekeyed or is a multisig and ifauthAddr
andmsig
are not specified, then the wallet MUST reject.
- If
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 theSignedTxn
object does not match exactly theTransaction
object intxn
. - The wallet MAY NOT check whether the other fields of the
SignedTxn
are valid. In particular, it MAY acceptstxn
even in the following cases: it contains an invalid signaturesig
, it contains both a signaturesig
and a logicsiglsig
, it contains a logicsiglsig
that always reject.
- It must be a valid
- If specified and if
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 asmessage
, except it is a message common to all the transactions of the group containing the current transaction. In addition, the wallet MUST reject ifgroupMessage
is provided for a transaction that is not the first transaction of the group. Note thattxns
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 asWalletTransaction.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:
- (REQUIRED)
txns
is a non-empty array of transactions that belong to the same group of transactions. In other words, eithertxns
is an array of a single transaction with a zero group ID (txn.Group
), ortxns
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.
- (OPTIONAL)
txns
is a concatenation oftxns
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
ortxn.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 withsigners=[]
). - 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 ofWalletTransaction
and ofSignTxnsOpts
. - 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 asTransaction.toByte
except it outputs a base64 stringAlgodv2.sendBase64RawTransactions
that does the same asAlgodv2.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
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.