miden_tx/auth/
tx_authenticator.rs

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