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_objects::account::auth::{AuthSecretKey, PublicKeyCommitment, Signature};
7use miden_objects::crypto::SequentialCommit;
8use miden_objects::transaction::TransactionSummary;
9use miden_objects::{Felt, Hasher, Word};
10use miden_processor::FutureMaybeSend;
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 [Word]'s which are the hashes of the actual public keys.
125pub trait TransactionAuthenticator {
126    /// Retrieves a signature for a specific message as a list of [Felt].
127    ///
128    /// The request is initiated by the VM as a consequence of the SigToStack advice
129    /// injector.
130    ///
131    /// - `pub_key_hash`: The hash of the public key used for signature generation.
132    /// - `message`: The message to sign, usually a commitment to the transaction data.
133    /// - `account_delta`: An informational parameter describing the changes made to the account up
134    ///   to the point of calling `get_signature()`. This allows the authenticator to review any
135    ///   alterations to the account prior to signing. It should not be directly used in the
136    ///   signature computation.
137    fn get_signature(
138        &self,
139        pub_key_commitment: PublicKeyCommitment,
140        signing_inputs: &SigningInputs,
141    ) -> impl FutureMaybeSend<Result<Signature, AuthenticationError>>;
142}
143
144/// A placeholder type for the generic trait bound of `TransactionAuthenticator<'_,'_,_,T>`
145/// when we do not want to provide one, but must provide the `T` in `Option<T>`.
146///
147/// Note: Asserts when `get_signature` is called.
148#[derive(Debug, Clone, Copy)]
149pub struct UnreachableAuth {
150    // ensure the type cannot be instantiated
151    _protect: core::marker::PhantomData<u8>,
152}
153
154impl TransactionAuthenticator for UnreachableAuth {
155    #[allow(clippy::manual_async_fn)]
156    fn get_signature(
157        &self,
158        _pub_key_commitment: PublicKeyCommitment,
159        _signing_inputs: &SigningInputs,
160    ) -> impl FutureMaybeSend<Result<Signature, AuthenticationError>> {
161        async { unreachable!("Type `UnreachableAuth` must not be instantiated") }
162    }
163}
164
165// BASIC AUTHENTICATOR
166// ================================================================================================
167
168/// Represents a signer for [AuthSecretKey] keys.
169#[derive(Clone, Debug)]
170pub struct BasicAuthenticator {
171    /// pub_key |-> secret_key mapping
172    keys: BTreeMap<PublicKeyCommitment, AuthSecretKey>,
173}
174
175impl BasicAuthenticator {
176    pub fn new(keys: &[AuthSecretKey]) -> Self {
177        let mut key_map = BTreeMap::new();
178        for secret_key in keys {
179            let pub_key = secret_key.public_key().to_commitment();
180            key_map.insert(pub_key, secret_key.clone());
181        }
182
183        BasicAuthenticator { keys: key_map }
184    }
185
186    /// Returns a reference to the keys map.
187    ///
188    /// Map keys represent the public key commitments, and values represent the secret keys that
189    /// the authenticator would use to sign messages.
190    pub fn keys(&self) -> &BTreeMap<PublicKeyCommitment, AuthSecretKey> {
191        &self.keys
192    }
193}
194
195impl TransactionAuthenticator for BasicAuthenticator {
196    /// Gets a signature over a message, given a public key commitment.
197    ///
198    /// The key should be included in the `keys` map and should be a variant of [AuthSecretKey].
199    ///
200    /// Supported signature schemes:
201    /// - RpoFalcon512
202    ///
203    /// # Errors
204    /// If the public key is not contained in the `keys` map,
205    /// [`AuthenticationError::UnknownPublicKey`] is returned.
206    fn get_signature(
207        &self,
208        pub_key_commitment: PublicKeyCommitment,
209        signing_inputs: &SigningInputs,
210    ) -> impl FutureMaybeSend<Result<Signature, AuthenticationError>> {
211        let message = signing_inputs.to_commitment();
212
213        async move {
214            match self.keys.get(&pub_key_commitment) {
215                Some(key) => Ok(key.sign(message)),
216                None => Err(AuthenticationError::UnknownPublicKey(pub_key_commitment)),
217            }
218        }
219    }
220}
221
222// HELPER FUNCTIONS
223// ================================================================================================
224
225impl TransactionAuthenticator for () {
226    #[allow(clippy::manual_async_fn)]
227    fn get_signature(
228        &self,
229        _pub_key_commitment: PublicKeyCommitment,
230        _signing_inputs: &SigningInputs,
231    ) -> impl FutureMaybeSend<Result<Signature, AuthenticationError>> {
232        async {
233            Err(AuthenticationError::RejectedSignature(
234                "default authenticator cannot provide signatures".to_string(),
235            ))
236        }
237    }
238}
239
240#[cfg(test)]
241mod test {
242    use miden_lib::utils::{Deserializable, Serializable};
243    use miden_objects::account::auth::AuthSecretKey;
244    use miden_objects::{Felt, Word};
245
246    use super::SigningInputs;
247
248    #[test]
249    fn serialize_auth_key() {
250        let auth_key = AuthSecretKey::new_rpo_falcon512();
251        let serialized = auth_key.to_bytes();
252        let deserialized = AuthSecretKey::read_from_bytes(&serialized).unwrap();
253
254        assert_eq!(auth_key, deserialized);
255    }
256
257    #[test]
258    fn serialize_deserialize_signing_inputs_arbitrary() {
259        let elements = vec![
260            Felt::new(0),
261            Felt::new(1),
262            Felt::new(2),
263            Felt::new(3),
264            Felt::new(4),
265            Felt::new(5),
266            Felt::new(6),
267            Felt::new(7),
268        ];
269        let inputs = SigningInputs::Arbitrary(elements.clone());
270        let bytes = inputs.to_bytes();
271        let decoded = SigningInputs::read_from_bytes(&bytes).unwrap();
272
273        match decoded {
274            SigningInputs::Arbitrary(decoded_elements) => {
275                assert_eq!(decoded_elements, elements);
276            },
277            _ => panic!("expected Arbitrary variant"),
278        }
279    }
280
281    #[test]
282    fn serialize_deserialize_signing_inputs_blind() {
283        let word = Word::from([Felt::new(10), Felt::new(20), Felt::new(30), Felt::new(40)]);
284        let inputs = SigningInputs::Blind(word);
285        let bytes = inputs.to_bytes();
286        let decoded = SigningInputs::read_from_bytes(&bytes).unwrap();
287
288        match decoded {
289            SigningInputs::Blind(w) => {
290                assert_eq!(w, word);
291            },
292            _ => panic!("expected Blind variant"),
293        }
294    }
295}