NEP: Standard for Offline Message Signing and Verification
NEP: XXX
Title: Standard for Offline Message Signing and Verification
Author: Adrian (atlazor)
Discussions-To: https://github.com/neo-project/proposals/issues
Status: Draft
Type: Standards Track
Category: Core
Created: 2025-11-10
Simple Summary
Define a standardized JSON format and signing algorithm specification for offline message signing in Neo N3 CLI and compatible wallets, ensuring consistent and verifiable results across implementations.
Abstract
This NEP introduces a standard for offline message signing and verification. It defines:
- A canonical JSON structure for the signed message output.
- A versioning convention for signing standards (e.g.,
NEP-XXX.1). - The
algoproperty to identify the algorithm used for message payload construction. - The
curveproperty to define the elliptic curve used for signing.
The initial algorithm, nep33_safe_message_v1, provides a secure method of signing arbitrary messages without risk of the resulting signature being interpreted as a valid Neo transaction.
Motivation
Governance candidates, node operators, and automated agents often need to sign arbitrary messages securely and verifiably using Neo wallets or CLI tools. Currently, there is no standard output format or canonical signing algorithm for this process.
Different wallets have implemented ad-hoc approaches for signing messages, leading to verification inconsistencies and interoperability issues.
The goal of NEP-33 is to unify how offline messages are signed, serialized, and verified across the Neo ecosystem.
Specification
The standard defines both the output format and the algorithmic method for constructing the payload to be signed.
JSON Output Format
The standardized JSON object MUST include the following fields:
{
"version": "NEP-XXX.1",
"algo": "nepXXX_safe_message_v1",
"curve": "secp256r1",
"signedPayloadBase64": "AQAB8MmMZCPvdYfAA7ScAZZiw8NLil8isWvxfs5Q...AA==",
"signatures": [
{
"address": "NZf33mJcBxW6ZjYGRcbvV2eiVigFD3g2Ak",
"publicKey": "03a9321588342aaecca1f4983a5bca1b16e460ed3d07e40a5d9f76ae7077d9779d",
"signature": "b8170ac6c93a0b79...",
"salt": "42a0df62e01367f430842f2f551d46a4"
}
]
}
Field Descriptions
-
version — NEP reference and sub-version (e.g.,
NEP-XXX.1). -
algo — The algorithm used to construct the signed payload.
-
curve — Elliptic curve used for signing (
secp256r1,secp256k1, etc.). -
signedPayloadBase64 — MUST be the exact byte sequence that was signed, encoded in base64 (no
0xprefix or hex). This enables unambiguous reconstruction and verification across languages and platforms. -
signatures — List of signature entries, one for each signing account.
- address — Neo N3 address of the signer.
- publicKey — Compressed public key of the signer.
- signature — Hex-encoded signature.
- salt — Random 16-byte hex string used in payload construction.
Encoding Rules:
- Implementations MUST base64-encode the raw payload bytes as
signedPayloadBase64. - Implementations SHOULD NOT include a duplicate hex-encoded payload field; hex can be ambiguous across environments (e.g.,
0xprefixes). - If a hex form is exposed for debugging, it MUST be named
signedPayloadHexand is OPTIONAL.
Algorithm Definitions
nepXXX_safe_message_v1
The nepXXX_safe_message_v1 aims to prevent message signatures from being valid Neo transactions by embedding the message inside a non-transactional payload.
Payload Construction:
010001f0 + VarBytes(Salt + Message) + 0000
Verification Steps:
- Rebuild the payload using the provided
saltand message data according to the defined algorithm. - Apply the chosen elliptic curve (
curve) and signature scheme. - Verify using the provided
publicKeyandsignature.
Security Rationale:
The prefix 010001f0 and suffix 0000 ensure the signed payload cannot be interpreted as a valid Neo transaction, mitigating the risk of transaction replay or signature misuse.
Rationale
A common, machine-readable format for offline message signing simplifies cross-tool verification, governance audits, and automated attestation mechanisms. It also ensures that node operators and wallets can independently verify message authenticity without requiring a running Neo node.
Backward Compatibility
This proposal introduces new functionality and does not break any existing behavior. Future wallet or CLI implementations can adopt NEP-XXX incrementally.
Reference Implementation
The Neo CLI implementation added by Adrian (atlazor) in November 2025: https://github.com/neo-project/neo/blob/master/src/Neo.CLI/CLI/MainService.Wallet.cs
[ConsoleCommand("sign message", Category = "Wallet Commands")]
private void OnSignMessageCommand(string message)
Payload construction follows:
010001f0 + VarBytes(Salt + Message) + 0000
Encoding Rules:
* Implementations **MUST** base64-encode the raw payload bytes as `signedPayloadBase64`. * Implementations **SHOULD NOT** include a duplicate hex-encoded payload field; hex can be ambiguous across environments (e.g., `0x` prefixes). * If a hex form is exposed for debugging, it **MUST** be named `signedPayloadHex` and is **OPTIONAL**.
- As it is currently written it is unclear to me if these encoding rules only apply to
signedPayloadBase64or to the whole object? - Can you elaborate on point 2
duplicate hex-encoded payload field. What is the problem of having this? - this might depend on my first point, but can we be strict on describing how to output any byte array string (thus including all the fields under
signatures). I can kind of derive it from the example, but from historical experience I already know there will be some implementation that will write the output where something like thesignaturehas a0xprefix - Can we restrict curve names? Depending on the language / library used a curve like
secp256r1might be calledNISTP-256orprime256v1. So anyone implementing this might justToStringsome enum when writing the output and now all implementations needs to do special handling.
some related reference
https://github.com/neo-ngd/neo-dapi-monorepo/blob/master/packages/neo-dapi/README.md#signmessage https://github.com/neo-project/proposals/pull/145/files#r1009320733 https://neoline.io/dapi/N3.html#signMessage
https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms
We can use the algorithm name listed here.
@do-shwhy
- As it is currently written it is unclear to me if these encoding rules only apply to
signedPayloadBase64or to the whole object?
It is only the payload to be signed that must be base64 encoded. How about:
"The signedPayloadBase64 property MUST base64-encode the raw payload bytes."
- Can you elaborate on point 2
duplicate hex-encoded payload field. What is the problem of having this?
The problem is stated: "hex can be ambiguous across environments". So for me, at least, I always understand how to turn a base64 encoded string into bytes, but with hex it can be unclear because of the endianness of the hex.
- this might depend on my first point, but can we be strict on describing how to output any byte array string (thus including all the fields under
signatures). I can kind of derive it from the example, but from historical experience I already know there will be some implementation that will write the output where something like thesignaturehas a0xprefix
I would rather not. As a developer in the echosystem I still find it hard sometimes to remember the correct endianness of the different hex representations of bytes. And I beleive some tools actually does not adhere to the strict rules set in place.
- Can we restrict curve names? Depending on the language / library used a curve like
secp256r1might be calledNISTP-256orprime256v1. So anyone implementing this might justToStringsome enum when writing the output and now all implementations needs to do special handling.
Yes. We can restrict it to a list provided in the NEP perhaps? Or do you have any other ideas?
https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms
We can use the algorithm name listed here.
Can you please alaborate? I do not understand what part of the proposal you are refering to?
https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms We can use the algorithm name listed here.
Can you please alaborate? I do not understand what part of the proposal you are refering to?
I assumed this is in response to my request to restrict the possible names in the curve field. Specifically
- Can we restrict curve names? Depending on the language / library used a curve like
secp256r1might be calledNISTP-256orprime256v1. So anyone implementing this might justToStringsome enum when writing the output and now all implementations needs to do special handling.
Aha, I see.
But I cannot find the Secp256r1 curve listed there, only Secp256k1, which Jimmy told me we do not use: https://github.com/neo-project/neo/pull/4286#discussion_r2505936693
How about we just KISS (keep it simple s...d): We provide a list in the NEP? 😅
But I cannot find the Secp256r1 curve listed there, only Secp256k1, which Jimmy told me we do not use: neo-project/neo#4286 (comment)
Because it's called P-256 in that document
I don't care if we use the suggestion by Erik or have a fixed list in the NEP that we maintain/update. As long as it is fixed/consistent.
Aha. Then let's use that list and start with reccomend using "P-256" then. Thanks for the reply :)
So based on all the input now, do I proceed with making a PR so that i can implement the changes? Or what is the preferred way to proceed?
And do I assign a number to the NEP myself? In that case, i sugest NEP-33.
Some renaming suggestions:
algo => algorithm curve (remove, because it is already included in algorithm field) signedPayloadBase64 => message
I am happy with the current implementation. I believe it works well the way it is.
Either I add a PR as it stands with the above changes, or I close the issue and end the discussion (feel free to disucss further without me).
This issue originated from adding a very simple method in the CLI to sign messages and I cannot use large resources on such a small change.
Payload Construction: 010001f0 + VarBytes(Salt + Message) + 0000
This should include at least the network, using GetSignData (https://github.com/neo-project/neo/pull/4299)
Any changes to "010001f0 + VarBytes(Salt + Message) + 0000" can break something at this point, this was used for a long long time. Even NeoFS has a notion of it, that's exactly what https://pkg.go.dev/github.com/nspcc-dev/[email protected]/crypto/ecdsa#SignerWalletConnect does in https://github.com/nspcc-dev/neofs-sdk-go/blob/3706963646f0bddaf05b7e1baaea548d37cafde5/crypto/ecdsa/wallet_connect.go#L106
But we also have a format for offline multisignature collection (https://github.com/neo-project/neo/blob/5ea45b27de6b24b71487223c3fe9ce009e265d0c/src/Neo/SmartContract/ContractParametersContext.cs#L58 or https://pkg.go.dev/github.com/nspcc-dev/[email protected]/pkg/smartcontract/context#ParameterContext), how does this NEP relate to it?
Any changes to "010001f0 + VarBytes(Salt + Message) + 0000" can break something at this point, this was used for a long long time. Even NeoFS has a notion of it, that's exactly what https://pkg.go.dev/github.com/nspcc-dev/[email protected]/crypto/ecdsa#SignerWalletConnect does in https://github.com/nspcc-dev/neofs-sdk-go/blob/3706963646f0bddaf05b7e1baaea548d37cafde5/crypto/ecdsa/wallet_connect.go#L106
Yes, so it is the cannonical way to sign arbitrary messages on Neo. There is no standard today, but this NEP aims to formalize a standart that includes the canonical pattern/algorithm of payload generation (not curve).
But we also have a format for offline multisignature collection (https://github.com/neo-project/neo/blob/5ea45b27de6b24b71487223c3fe9ce009e265d0c/src/Neo/SmartContract/ContractParametersContext.cs#L58 or https://pkg.go.dev/github.com/nspcc-dev/[email protected]/pkg/smartcontract/context#ParameterContext), how does this NEP relate to it?
I think it does not relate to that, as this is broad arbitrary message signing, and does not have a context that relates to a network. We just want to be able to generate signatures that can be verified to come from the given public key given a specified curve and algorithm for payload generation.