miden_tx/auth/
tx_authenticator.rs

1use alloc::boxed::Box;
2use alloc::collections::BTreeMap;
3use alloc::string::ToString;
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6
7use miden_objects::account::AuthSecretKey;
8use miden_objects::crypto::SequentialCommit;
9use miden_objects::transaction::TransactionSummary;
10use miden_objects::{Felt, Hasher, Word};
11use miden_processor::FutureMaybeSend;
12use rand::Rng;
13use tokio::sync::RwLock;
14
15use super::signatures::get_falcon_signature;
16use crate::errors::AuthenticationError;
17use crate::utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable};
18
19// SIGNATURE DATA
20// ================================================================================================
21
22/// Data types on which a signature can be requested.
23///
24/// It supports three modes:
25/// - `TransactionSummary`: Structured transaction summary, recommended for authenticating
26///   transactions.
27/// - `Arbitrary`: Arbitrary payload provided by the application. It is up to the authenticator to
28///   display it appropriately.
29/// - `Blind`: The underlying data is not meant to be displayed in a human-readable format. It must
30///   be a cryptographic commitment to some data.
31#[derive(Debug, Clone)]
32pub enum SigningInputs {
33    TransactionSummary(Box<TransactionSummary>),
34    Arbitrary(Vec<Felt>),
35    Blind(Word),
36}
37
38impl SequentialCommit for SigningInputs {
39    type Commitment = Word;
40
41    fn to_elements(&self) -> Vec<Felt> {
42        match self {
43            SigningInputs::TransactionSummary(tx_summary) => tx_summary.as_ref().to_elements(),
44            SigningInputs::Arbitrary(elements) => elements.clone(),
45            SigningInputs::Blind(word) => word.as_elements().to_vec(),
46        }
47    }
48
49    fn to_commitment(&self) -> Self::Commitment {
50        match self {
51            // `TransactionSummary` knows how to derive a commitment to itself.
52            SigningInputs::TransactionSummary(tx_summary) => tx_summary.as_ref().to_commitment(),
53            // use the default implementation.
54            SigningInputs::Arbitrary(elements) => Hasher::hash_elements(elements),
55            // `Blind` is assumed to already be a commitment.
56            SigningInputs::Blind(word) => *word,
57        }
58    }
59}
60
61/// Convenience methods for [SigningInputs].
62impl SigningInputs {
63    /// Computes the commitment to [SigningInputs].
64    pub fn to_commitment(&self) -> Word {
65        <Self as SequentialCommit>::to_commitment(self)
66    }
67
68    /// Returns a representation of the [SigningInputs] as a sequence of field elements.
69    pub fn to_elements(&self) -> Vec<Felt> {
70        <Self as SequentialCommit>::to_elements(self)
71    }
72}
73
74// SERIALIZATION
75// ================================================================================================
76
77impl Serializable for SigningInputs {
78    fn write_into<W: ByteWriter>(&self, target: &mut W) {
79        match self {
80            SigningInputs::TransactionSummary(tx_summary) => {
81                target.write_u8(0);
82                tx_summary.as_ref().write_into(target);
83            },
84            SigningInputs::Arbitrary(elements) => {
85                target.write_u8(1);
86                elements.write_into(target);
87            },
88            SigningInputs::Blind(word) => {
89                target.write_u8(2);
90                word.write_into(target);
91            },
92        }
93    }
94}
95
96impl Deserializable for SigningInputs {
97    fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
98        let discriminant = source.read_u8()?;
99        match discriminant {
100            0 => {
101                let tx_summary: TransactionSummary = source.read()?;
102                Ok(SigningInputs::TransactionSummary(Box::new(tx_summary)))
103            },
104            1 => {
105                let elements: Vec<Felt> = source.read()?;
106                Ok(SigningInputs::Arbitrary(elements))
107            },
108            2 => {
109                let word: Word = source.read()?;
110                Ok(SigningInputs::Blind(word))
111            },
112            other => Err(DeserializationError::InvalidValue(format!(
113                "invalid SigningInputs variant: {other}"
114            ))),
115        }
116    }
117}
118
119// TRANSACTION AUTHENTICATOR
120// ================================================================================================
121
122/// Defines an authenticator for transactions.
123///
124/// The main purpose of the authenticator is to generate signatures for a given message against
125/// a key managed by the authenticator. That is, the authenticator maintains a set of public-
126/// private key pairs, and can be requested to generate signatures against any of the managed keys.
127///
128/// The public keys are defined by [Word]'s which are the hashes of the actual public keys.
129pub trait TransactionAuthenticator {
130    /// Retrieves a signature for a specific message as a list of [Felt].
131    ///
132    /// The request is initiated by the VM as a consequence of the SigToStack advice
133    /// injector.
134    ///
135    /// - `pub_key_hash`: The hash of the public key used for signature generation.
136    /// - `message`: The message to sign, usually a commitment to the transaction data.
137    /// - `account_delta`: An informational parameter describing the changes made to the account up
138    ///   to the point of calling `get_signature()`. This allows the authenticator to review any
139    ///   alterations to the account prior to signing. It should not be directly used in the
140    ///   signature computation.
141    fn get_signature(
142        &self,
143        pub_key: Word,
144        signing_inputs: &SigningInputs,
145    ) -> impl FutureMaybeSend<Result<Vec<Felt>, AuthenticationError>>;
146}
147
148/// A placeholder type for the generic trait bound of `TransactionAuthenticator<'_,'_,_,T>`
149/// when we do not want to provide one, but must provide the `T` in `Option<T>`.
150///
151/// Note: Asserts when `get_signature` is called.
152#[derive(Debug, Clone, Copy)]
153pub struct UnreachableAuth {
154    // ensure the type cannot be instantiated
155    _protect: core::marker::PhantomData<u8>,
156}
157
158impl TransactionAuthenticator for UnreachableAuth {
159    #[allow(clippy::manual_async_fn)]
160    fn get_signature(
161        &self,
162        _pub_key: Word,
163        _signing_inputs: &SigningInputs,
164    ) -> impl FutureMaybeSend<Result<Vec<Felt>, AuthenticationError>> {
165        async { unreachable!("Type `UnreachableAuth` must not be instantiated") }
166    }
167}
168
169// BASIC AUTHENTICATOR
170// ================================================================================================
171
172/// Represents a signer for [AuthSecretKey] keys.
173#[derive(Clone, Debug)]
174pub struct BasicAuthenticator<R> {
175    /// pub_key |-> secret_key mapping
176    keys: BTreeMap<Word, AuthSecretKey>,
177    rng: Arc<RwLock<R>>,
178}
179
180impl<R: Rng> BasicAuthenticator<R> {
181    #[cfg(feature = "std")]
182    pub fn new(keys: &[(Word, AuthSecretKey)]) -> BasicAuthenticator<rand::rngs::StdRng> {
183        use rand::SeedableRng;
184        use rand::rngs::StdRng;
185
186        let rng = StdRng::from_os_rng();
187        BasicAuthenticator::<StdRng>::new_with_rng(keys, rng)
188    }
189
190    pub fn new_with_rng(keys: &[(Word, AuthSecretKey)], rng: R) -> Self {
191        let mut key_map = BTreeMap::new();
192        for (word, secret_key) in keys {
193            key_map.insert(*word, secret_key.clone());
194        }
195
196        BasicAuthenticator {
197            keys: key_map,
198            rng: Arc::new(RwLock::new(rng)),
199        }
200    }
201
202    /// Returns a reference to the keys map. Map keys represent the public keys, and values
203    /// represent the secret keys that the authenticator would use to sign messages.
204    pub fn keys(&self) -> &BTreeMap<Word, AuthSecretKey> {
205        &self.keys
206    }
207}
208
209impl<R: Rng + Send + Sync> TransactionAuthenticator for BasicAuthenticator<R> {
210    /// Gets a signature over a message, given a public key.
211    /// The key should be included in the `keys` map and should be a variant of [AuthSecretKey].
212    ///
213    /// Supported signature schemes:
214    /// - RpoFalcon512
215    ///
216    /// # Errors
217    /// If the public key is not contained in the `keys` map,
218    /// [`AuthenticationError::UnknownPublicKey`] is returned.
219    fn get_signature(
220        &self,
221        pub_key: Word,
222        signing_inputs: &SigningInputs,
223    ) -> impl FutureMaybeSend<Result<Vec<Felt>, AuthenticationError>> {
224        let message = signing_inputs.to_commitment();
225
226        async move {
227            let mut rng = self.rng.write().await;
228            match self.keys.get(&pub_key) {
229                Some(key) => match key {
230                    AuthSecretKey::RpoFalcon512(falcon_key) => {
231                        get_falcon_signature(falcon_key, message, &mut *rng)
232                    },
233                },
234                None => Err(AuthenticationError::UnknownPublicKey(format!(
235                    "public key {pub_key} is not contained in the authenticator's keys",
236                ))),
237            }
238        }
239    }
240}
241
242// HELPER FUNCTIONS
243// ================================================================================================
244
245impl TransactionAuthenticator for () {
246    #[allow(clippy::manual_async_fn)]
247    fn get_signature(
248        &self,
249        _pub_key: Word,
250        _signing_inputs: &SigningInputs,
251    ) -> impl FutureMaybeSend<Result<Vec<Felt>, AuthenticationError>> {
252        async {
253            Err(AuthenticationError::RejectedSignature(
254                "default authenticator cannot provide signatures".to_string(),
255            ))
256        }
257    }
258}
259
260#[cfg(test)]
261mod test {
262    use miden_lib::utils::{Deserializable, Serializable};
263    use miden_objects::account::AuthSecretKey;
264    use miden_objects::crypto::dsa::rpo_falcon512::SecretKey;
265    use miden_objects::{Felt, Word};
266
267    use super::SigningInputs;
268
269    #[test]
270    fn serialize_auth_key() {
271        let secret_key = SecretKey::new();
272        let auth_key = AuthSecretKey::RpoFalcon512(secret_key.clone());
273        let serialized = auth_key.to_bytes();
274        let deserialized = AuthSecretKey::read_from_bytes(&serialized).unwrap();
275
276        match deserialized {
277            AuthSecretKey::RpoFalcon512(key) => assert_eq!(secret_key.to_bytes(), key.to_bytes()),
278        }
279    }
280
281    #[test]
282    fn serialize_deserialize_signing_inputs_arbitrary() {
283        let elements = vec![
284            Felt::new(0),
285            Felt::new(1),
286            Felt::new(2),
287            Felt::new(3),
288            Felt::new(4),
289            Felt::new(5),
290            Felt::new(6),
291            Felt::new(7),
292        ];
293        let inputs = SigningInputs::Arbitrary(elements.clone());
294        let bytes = inputs.to_bytes();
295        let decoded = SigningInputs::read_from_bytes(&bytes).unwrap();
296
297        match decoded {
298            SigningInputs::Arbitrary(decoded_elements) => {
299                assert_eq!(decoded_elements, elements);
300            },
301            _ => panic!("expected Arbitrary variant"),
302        }
303    }
304
305    #[test]
306    fn serialize_deserialize_signing_inputs_blind() {
307        let word = Word::from([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]);
308        let inputs = SigningInputs::Blind(word);
309        let bytes = inputs.to_bytes();
310        let decoded = SigningInputs::read_from_bytes(&bytes).unwrap();
311
312        match decoded {
313            SigningInputs::Blind(w) => {
314                assert_eq!(w, word);
315            },
316            _ => panic!("expected Blind variant"),
317        }
318    }
319}