Skip to main content

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