LIP: 0037
Title: Use message tags and chain identifiers for signatures
Author: Andreas Kendziorra <[email protected]>
Discussions-To: https://research.lisk.com/t/use-message-tags-and-chain-identifiers-for-signatures/280
Status: Active
Type: Standards Track
Created: 2021-04-07
Updated: 2024-01-04
Replaces: 0009
We introduce the concept of message tags for signatures. These message tags are prepended to the binary messages before being signed. A unique tag has to be used for each message type (transaction, block header, etc.), and in particular for each schema. This ensures that a signature for one message cannot be a valid signature for another message that serializes to the same binary message.
Moreover, we generalize the usage of the chain identifiers (called network identifier in previous LIPs), which are currently used for transaction and block header signatures, to arbitrary signatures. This will prevent replay attacks for arbitrary messages.
This LIP is licensed under the Creative Commons Zero 1.0 Universal.
We want to avoid (1) replay attacks and (2) the re-usage of a signature for a different message of a different type.
Case (2) could happen if there are two messages that are of different types but are serialized to the same binary message. For example, consider a block header bh
that is serialized to the binary message bm
. If the binary message bm
can be deserialized against a transaction schema to a transaction tx
, then a signature of bh
would also be a valid signature of tx
for the generator public key of bh
. Note that for several reasons it is very unlikely that a concrete instance of this example passes block validation for bh
and transaction validation for tx
. For instance, the current schemas do not allow thatbh.generatorPublicKey
and tx.senderPublicKey
are encoded in the same position of the binary message.
Nevertheless, it is highly desirable to prevent such re-usage cases in general. In particular for new data structures that require a signature introduced by some future protocol enhancements.
Replay attacks for transactions and blocks, i.e. replaying transactions or blocks on other chains, are already addressed by LIP 0009 and LIP 0024 respectively. However, replay attacks should be prevented for any kind of data structure for the same reason as above.
Prepending a tag to a binary message before signing, where the tag is specific to the type of the message, prevents the signature re-usage in general. Consider again the blockheader bh
and the transaction tx
that are serialized to the same binary message bm
. When the tag "LSK_BH_"
is used for block header signatures and the tag "LSK_TX_"
for transaction signatures, then the signatures for bh
and tx
are computed by signing the messages "LSK_BH_" + bm
and "LSK_TX_" + bm
respectively. Hence, the signature of bh
cannot be a valid signature for tx
and vice versa (assuming no collisions in the signing function).
Note that any updates to the serialization specification of a message type must be accompanied by introducing a new tag. For example, when the transaction schema is updated, a new tag like "LSK_TX:V2_"
must be defined and used.
Chain identifiers for transaction signatures and block signatures were already introduced in LIP 0009 and LIP 0024 (as network identifiers) to prevent replay attacks on other chains. Here, we specify how to use a chain identifier for any kind of message to prevent replay attacks on other chains in general. These specifications supersede the specifications given in LIP 0009.
Chain identifiers are 4-byte values that follow a specific format: the first byte is used to identify the network in which the chain is running (either the mainnet, official testnet, or any other test network). The other 3 bytes identify the chain within the network. We include the network-specific prefix explicitly to ensure that a chain does not use the same chain identifier in the test network as in the mainnet.
The first bit of the chain-identifier prefix is always set to 0
. Thus, we have a total of 128
available network IDs. This is done to prevent the overflow of the max size of uint32
for hardened paths for Ed25519 keys derivation.
Using 4 bytes instead of 32 bytes as in LIP 0009 has the advantage that users can easily verify that they are signing a transaction for the correct blockchain. Furthermore, the chain identifier can be directly set by the blockchain creator, which is more convenient than generating a random 32-byte value.
Name | Type | Value | Description |
---|---|---|---|
CHAIN_ID_LENGTH |
uint32 |
4 | Length of chain IDs in bytes. |
OWN_CHAIN_ID |
ChainID |
set in configuration | The chain ID of the chain under consideration, see below. |
Name | Type | Validation | Description |
---|---|---|---|
ChainID |
bytes |
Must be of length CHAIN_ID_LENGTH . |
ID of a chain. |
Every binary message must be tagged before being signed. This is done as specified by the function tagMessage
in the pseudo code below: A specific ASCII-encoded tag and a chain identifier are prepended to the message.
def tagMessage(tag: bytes, chainID: bytes, message: bytes) -> bytes:
return tag + chainID + message
The tag is chosen depending on the message type. Moreover, different serialization methods for the same message type require different tags. The following table defines the tags currently needed.
Name | Type | Value | Description |
---|---|---|---|
MESSAGE_TAG_TRANSACTION |
bytes |
"LSK_TX_" as ASCII-encoded literal | Message tag for signing transaction with serialization specified in LIP "Define New Transaction Schema". |
MESSAGE_TAG_BLOCK_HEADER |
bytes |
"LSK_BH_" as ASCII-encoded literal | Message tag for signing block headers with serialization specified in LIP 0055. |
MESSAGE_TAG_MULTISIG_REG |
bytes |
"LSK_RMS_" as ASCII-encoded literal | Message tag for the signatures contained in the register multisignature command. |
MESSAGE_TAG_CHAIN_REG_MESSAGE |
bytes |
"LSK_CRM_" as ASCII-encoded literal | Message tag for the signatures contained in the chain registration message. |
MESSAGE_TAG_POA |
bytes |
"LSK_POA_" as ASCII-encoded literal | Message tag for the signatures contained in the update authority command. |
MESSAGE_TAG_CERTIFICATE |
bytes |
"LSK_CE_" as ASCII-encoded literal | Message tag for signing certficates with serialization specified in LIP 0061. |
MESSAGE_TAG_NON_PROTOCOL_MESSAGE |
bytes |
"LSK_NPM_" as ASCII-encoded literal | Message tag for signing non-protocol related messages. Used for the Sign Message feature in Lisk Desktop where arbitrary message can be signed and verified. |
When a new tag is required in the future, the format:
"LSK_" + TAG + "_"
has to be used where:
- Strings in double quotes are ASCII-encoded literals.
- TAG is a unique string indicating the scheme and, optionally, additional information, e.g., to specify a version number as shown above. TAG must contain only ASCII characters between 0x21 and 0x7e (inclusive), except that it must not contain underscore (0x5f).
Chain identifiers are 4-byte values. The first byte is set to CHAIN_ID_PREFIX_MAINNET
for chains running in the mainnet network and to CHAIN_ID_PREFIX_TESTNET
for chains running in the testnet network. The first bit of the chain-identifier prefix is always set to 0
. The other 3 bytes must be chosen uniquely for the respective blockchain, i.e., no other blockchain created with the Lisk SDK should use the same 3 bytes.
The following table defines the chain-identifiers prefixes currently specified.
Name | Type | Value | Description |
---|---|---|---|
CHAIN_ID_PREFIX_MAINNET |
bytes | 0x00 | Chain-identifier prefix for mainnet blockchains. |
CHAIN_ID_PREFIX_TESTNET |
bytes | 0x01 | Chain-identifier prefix for testnet blockchains. |
The chain ID of a chain is denoted by the constant OWN_CHAIN_ID
which is defined as part of the configuration of the chain. We assume this constant is globally available in the engine and application.
For a given network, the chain ID of the mainchain always has the last 3 bytes set to 0. Hence, given any chain ID in a network, the chain ID of the mainchain can be easily obtained by keeping the chain-identifier prefix and setting all other bytes to 0. We define the following utility function to easily obtain the chain ID of the mainchain.
def getMainchainID() -> ChainID:
return OWN_CHAIN_ID[0:1] + b'\0'*3
(Note: The following function definitions are superseded by the ones in LIP 0062.)
Let Sign
and Verify
be the signing and verifying functions of Ed25519 as specified in RFC 8032. Then the signature for a binary message m
and a secret key sk
is generated by signEd25519(sk, tag, chainID, m)
as defined below. tag
must be the correct message tag for m
and chainID
the correct chain identifier for the chain. The resulting signature sig
in combination with the message m
and the matching public key pk
is verified by verifyEd25519(pk, tag, chainID, m, sig)
.
def signEd25519(sk: bytes, tag: bytes, chainID: bytes, m: bytes) -> bytes:
taggedMessage = tagMessage(tag, chainID, m)
return Sign(sk, taggedMessage)
def verifyEd25519(pk: bytes, tag: bytes, chainID: bytes, m: bytes, sig: bytes) -> bool:
taggedMessage = tagMessage(tag, chainID, msg)
return Verify(pk, taggedMessage, sig)
Due to adding message tags to transaction and block signatures, the proposed signature scheme is incompatible with the current one and implies a hard fork.
Update generic message signing
TBA