shared_crypto/
intent.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use eyre::eyre;
5use fastcrypto::encoding::decode_bytes_hex;
6use serde::{Deserialize, Serialize};
7use serde_repr::Deserialize_repr;
8use serde_repr::Serialize_repr;
9use std::str::FromStr;
10
11pub const INTENT_PREFIX_LENGTH: usize = 3;
12
13/// The version here is to distinguish between signing different versions of the struct
14/// or enum. Serialized output between two different versions of the same struct/enum
15/// might accidentally (or maliciously on purpose) match.
16#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, PartialEq, Eq, Debug, Hash)]
17#[repr(u8)]
18pub enum IntentVersion {
19    V0 = 0,
20}
21
22impl TryFrom<u8> for IntentVersion {
23    type Error = eyre::Report;
24    fn try_from(value: u8) -> Result<Self, Self::Error> {
25        bcs::from_bytes(&[value]).map_err(|_| eyre!("Invalid IntentVersion"))
26    }
27}
28
29/// This enums specifies the application ID. Two intents in two different applications
30/// (i.e., Narwhal, Sui, Ethereum etc) should never collide, so that even when a signing
31/// key is reused, nobody can take a signature designated for app_1 and present it as a
32/// valid signature for an (any) intent in app_2.
33#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, Default, PartialEq, Eq, Debug, Hash)]
34#[repr(u8)]
35pub enum AppId {
36    #[default]
37    Sui = 0,
38    Narwhal = 1,
39    Consensus = 2,
40}
41
42// TODO(joyqvq): Use num_derive
43impl TryFrom<u8> for AppId {
44    type Error = eyre::Report;
45    fn try_from(value: u8) -> Result<Self, Self::Error> {
46        bcs::from_bytes(&[value]).map_err(|_| eyre!("Invalid AppId"))
47    }
48}
49
50/// This enums specifies the intent scope. Two intents for different scope should
51/// never collide, so no signature provided for one intent scope can be used for
52/// another, even when the serialized data itself may be the same.
53#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, PartialEq, Eq, Debug, Hash)]
54#[repr(u8)]
55pub enum IntentScope {
56    TransactionData = 0,         // Used for a user signature on a transaction data.
57    TransactionEffects = 1,      // Used for an authority signature on transaction effects.
58    CheckpointSummary = 2,       // Used for an authority signature on a checkpoint summary.
59    PersonalMessage = 3,         // Used for a user signature on a personal message.
60    SenderSignedTransaction = 4, // Used for an authority signature on a user signed transaction.
61    ProofOfPossession = 5, // Used as a signature representing an authority's proof of possession of its authority protocol key.
62    HeaderDigest = 6,      // Used for narwhal authority signature on header digest.
63    BridgeEventUnused = 7, // for bridge purposes but it's currently not included in messages.
64    ConsensusBlock = 8,    // Used for consensus authority signature on block's digest.
65    DiscoveryPeers = 9,    // Used for reporting peer addresses in discovery.
66}
67
68impl TryFrom<u8> for IntentScope {
69    type Error = eyre::Report;
70    fn try_from(value: u8) -> Result<Self, Self::Error> {
71        bcs::from_bytes(&[value]).map_err(|_| eyre!("Invalid IntentScope"))
72    }
73}
74
75/// An intent is a compact struct serves as the domain separator for a message that a signature commits to.
76/// It consists of three parts: [enum IntentScope] (what the type of the message is), [enum IntentVersion], [enum AppId] (what application that the signature refers to).
77/// It is used to construct [struct IntentMessage] that what a signature commits to.
78///
79/// The serialization of an Intent is a 3-byte array where each field is represented by a byte.
80#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Hash)]
81pub struct Intent {
82    pub scope: IntentScope,
83    pub version: IntentVersion,
84    pub app_id: AppId,
85}
86
87impl Intent {
88    pub fn to_bytes(&self) -> [u8; INTENT_PREFIX_LENGTH] {
89        [self.scope as u8, self.version as u8, self.app_id as u8]
90    }
91
92    pub fn from_bytes(bytes: &[u8]) -> Result<Self, eyre::Report> {
93        if bytes.len() != INTENT_PREFIX_LENGTH {
94            return Err(eyre!("Invalid Intent"));
95        }
96        Ok(Self {
97            scope: bytes[0].try_into()?,
98            version: bytes[1].try_into()?,
99            app_id: bytes[2].try_into()?,
100        })
101    }
102}
103
104impl FromStr for Intent {
105    type Err = eyre::Report;
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        let bytes: Vec<u8> = decode_bytes_hex(s).map_err(|_| eyre!("Invalid Intent"))?;
108        Self::from_bytes(bytes.as_slice())
109    }
110}
111
112impl Intent {
113    pub fn sui_app(scope: IntentScope) -> Self {
114        Self {
115            version: IntentVersion::V0,
116            scope,
117            app_id: AppId::Sui,
118        }
119    }
120
121    pub fn sui_transaction() -> Self {
122        Self {
123            scope: IntentScope::TransactionData,
124            version: IntentVersion::V0,
125            app_id: AppId::Sui,
126        }
127    }
128
129    pub fn personal_message() -> Self {
130        Self {
131            scope: IntentScope::PersonalMessage,
132            version: IntentVersion::V0,
133            app_id: AppId::Sui,
134        }
135    }
136
137    pub fn narwhal_app(scope: IntentScope) -> Self {
138        Self {
139            scope,
140            version: IntentVersion::V0,
141            app_id: AppId::Narwhal,
142        }
143    }
144
145    pub fn consensus_app(scope: IntentScope) -> Self {
146        Self {
147            scope,
148            version: IntentVersion::V0,
149            app_id: AppId::Consensus,
150        }
151    }
152}
153
154/// Intent Message is a wrapper around a message with its intent. The message can
155/// be any type that implements [trait Serialize]. *ALL* signatures in Sui must commits
156/// to the intent message, not the message itself. This guarantees any intent
157/// message signed in the system cannot collide with another since they are domain
158/// separated by intent.
159///
160/// The serialization of an IntentMessage is compact: it only appends three bytes
161/// to the message itself.
162#[derive(Debug, PartialEq, Eq, Serialize, Clone, Hash, Deserialize)]
163pub struct IntentMessage<T> {
164    pub intent: Intent,
165    pub value: T,
166}
167
168impl<T> IntentMessage<T> {
169    pub fn new(intent: Intent, value: T) -> Self {
170        Self { intent, value }
171    }
172}
173
174/// A person message that wraps around a byte array.
175#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
176pub struct PersonalMessage {
177    pub message: Vec<u8>,
178}
179
180pub trait SecureIntent: Serialize + private::SealedIntent {}
181
182pub(crate) mod private {
183    use super::IntentMessage;
184
185    pub trait SealedIntent {}
186    impl<T> SealedIntent for IntentMessage<T> {}
187}
188
189/// A 1-byte domain separator for hashing Object ID in Sui. It is starting from 0xf0
190/// to ensure no hashing collision for any ObjectID vs SuiAddress which is derived
191/// as the hash of `flag || pubkey`. See `sui_types::crypto::SignatureScheme::flag()`.
192#[derive(Serialize_repr, Deserialize_repr, Copy, Clone, PartialEq, Eq, Debug, Hash)]
193#[repr(u8)]
194pub enum HashingIntentScope {
195    ChildObjectId = 0xf0,
196    RegularObjectId = 0xf1,
197}