shared_crypto/
intent.rs

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