wamu_core/
quorum_approved_request.rs

1//! Quorum approved request initiation and verification implementation.
2//!
3//! Ref: <https://wamu.tech/specification#quorum-approved-request>.
4
5use crate::crypto::{Random32Bytes, VerifyingKey};
6use crate::errors::{IdentityAuthedRequestError, QuorumApprovedRequestError};
7use crate::payloads::{
8    CommandApprovalPayload, IdentityAuthedRequestPayload, QuorumApprovedChallengeResponsePayload,
9};
10use crate::traits::IdentityProvider;
11use crate::{crypto, identity_authed_request, identity_challenge, utils, wrappers};
12
13/// Given a "command" and an identity provider, returns the payload for initiating an quorum approved request.
14pub fn initiate(
15    command: &'static str,
16    identity_provider: &impl IdentityProvider,
17) -> IdentityAuthedRequestPayload {
18    identity_authed_request::initiate(command, identity_provider)
19}
20
21/// Given a "command" a quorum approved request initialization payload, an identity provider and a list of verifying keys for the other parties,
22/// returns an ok result with a "command" approval payload for initiating an identity challenge and approval acknowledgement for a valid request
23/// or an appropriate error result for an invalid request.
24pub fn verify_request_and_initiate_challenge(
25    command: &str,
26    request: &IdentityAuthedRequestPayload,
27    identity_provider: &impl IdentityProvider,
28    verified_parties: &[VerifyingKey],
29) -> Result<CommandApprovalPayload, IdentityAuthedRequestError> {
30    let challenge_fragment = wrappers::verify_identity_authed_request_and_initiate_challenge(
31        command,
32        request,
33        verified_parties,
34    )?;
35    let signature = identity_provider.sign(&command_approval_message_bytes(
36        &challenge_fragment,
37        request.command,
38        request.timestamp,
39    ));
40    Ok(CommandApprovalPayload {
41        challenge_fragment,
42        verifying_key: identity_provider.verifying_key(),
43        signature,
44    })
45}
46
47/// Given a list of command approval payloads, an identity provider, a quorum approved request initialization payload,
48/// a quorum size and a list of verifying keys for the other parties,
49/// returns an ok result with a quorum approved challenge response payload
50/// or an appropriate error result for an invalid request.
51pub fn challenge_response(
52    approvals: &[CommandApprovalPayload],
53    identity_provider: &impl IdentityProvider,
54    request: &IdentityAuthedRequestPayload,
55    quorum_size: usize,
56    verified_parties: &[VerifyingKey],
57) -> Result<QuorumApprovedChallengeResponsePayload, QuorumApprovedRequestError> {
58    // quorum_size - 1 because of implicit approval from initiator.
59    let valid_approvals = verify_approvals(approvals, request, quorum_size - 1, verified_parties)?;
60    let approving_quorum = valid_approvals
61        .iter()
62        .map(|approval| approval.verifying_key.clone())
63        .collect();
64    Ok(QuorumApprovedChallengeResponsePayload {
65        signature: identity_challenge::respond(
66            &extract_challenge_fragments(&valid_approvals).collect::<Vec<Random32Bytes>>(),
67            identity_provider,
68        ),
69        approving_quorum,
70    })
71}
72
73/// Given a quorum approved challenge response payload, a list of command approval payloads,
74/// a verifying key for challenged party, a quorum approved request initialization payload,
75/// a quorum size and a list of verifying keys for the other parties,
76/// returns an `Ok` result for valid quorum approved challenge response, or an appropriate `Err` result otherwise.
77pub fn verify_challenge_response(
78    response: &QuorumApprovedChallengeResponsePayload,
79    approvals: &[CommandApprovalPayload],
80    verifying_key: &VerifyingKey,
81    request: &IdentityAuthedRequestPayload,
82    quorum_size: usize,
83    verified_parties: &[VerifyingKey],
84) -> Result<(), QuorumApprovedRequestError> {
85    let initiator_acknowledged_approvals: Vec<CommandApprovalPayload> = approvals
86        .iter()
87        .filter(|approval| response.approving_quorum.contains(&approval.verifying_key))
88        .cloned()
89        .collect();
90    verify_approvals(
91        &initiator_acknowledged_approvals,
92        request,
93        // quorum_size - 1 because of implicit approval from initiator.
94        quorum_size - 1,
95        verified_parties,
96    )?;
97    Ok(identity_challenge::verify(
98        &response.signature,
99        &extract_challenge_fragments(&initiator_acknowledged_approvals)
100            .collect::<Vec<Random32Bytes>>(),
101        verifying_key,
102    )?)
103}
104
105/// Given a list of command approval payloads, a quorum approved request initialization payload,
106/// a quorum size and a list of verifying keys for the other parties,
107/// returns an ok result with a list of valid command approval payloads if there are enough valid command approvals
108/// to form a quorum or an appropriate error result otherwise.
109fn verify_approvals(
110    approvals: &[CommandApprovalPayload],
111    request: &IdentityAuthedRequestPayload,
112    quorum_size: usize,
113    verified_parties: &[VerifyingKey],
114) -> Result<Vec<CommandApprovalPayload>, QuorumApprovedRequestError> {
115    let valid_approvals = filter_valid_approvals(approvals, request, verified_parties);
116    if valid_approvals.len() < quorum_size {
117        Err(QuorumApprovedRequestError::InsufficientApprovals)
118    } else {
119        Ok(valid_approvals)
120    }
121}
122
123/// Given a list of command approval payloads, a quorum approved request initialization payload
124/// and a list of verifying keys for the other parties, returns a list of valid command approval payloads.
125fn filter_valid_approvals(
126    approvals: &[CommandApprovalPayload],
127    request: &IdentityAuthedRequestPayload,
128    verified_parties: &[VerifyingKey],
129) -> Vec<CommandApprovalPayload> {
130    approvals
131        .iter()
132        .filter(|approval| {
133            verified_parties.contains(&approval.verifying_key)
134                && crypto::verify_signature(
135                    &approval.verifying_key,
136                    &command_approval_message_bytes(
137                        &approval.challenge_fragment,
138                        request.command,
139                        request.timestamp,
140                    ),
141                    &approval.signature,
142                )
143                .is_ok()
144        })
145        .cloned()
146        .collect()
147}
148
149/// Returns sign-able message bytes for the command approval.
150fn command_approval_message_bytes(
151    challenge_fragment: &Random32Bytes,
152    command: &str,
153    timestamp: u64,
154) -> Vec<u8> {
155    utils::prefix_message_bytes(
156        format!("{}{}{}", challenge_fragment, command, timestamp).as_bytes(),
157    )
158}
159
160/// Given a list of command approval payloads and an identity provider, returns a list of wrapped challenge fragments.
161fn extract_challenge_fragments(
162    approvals: &[CommandApprovalPayload],
163) -> impl Iterator<Item = Random32Bytes> + '_ {
164    approvals.iter().map(|item| item.challenge_fragment)
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::errors::{CryptoError, Error};
171    use crate::test_utils::MockECDSAIdentityProvider;
172    use crypto_bigint::U256;
173
174    #[test]
175    fn quorum_approved_request_initiation_and_verification_works() {
176        // Generates current identity provider.
177        let initiator_identity_provider = MockECDSAIdentityProvider::generate();
178
179        // Creates identity providers for all other parties.
180        let approver_identity_providers: Vec<MockECDSAIdentityProvider> = (0..5)
181            .map(|_| MockECDSAIdentityProvider::generate())
182            .collect();
183
184        // Sets quorum.
185        let quorum_size = 5;
186
187        // Creates a list of verifying keys for all parties.
188        let verified_parties: Vec<VerifyingKey> = approver_identity_providers
189            .iter()
190            .map(|identity_provider| identity_provider.verifying_key())
191            .chain([initiator_identity_provider.verifying_key()])
192            .collect();
193
194        // Sets the command.
195        let command = "command";
196
197        // Generates quorum approved request initialization payload.
198        let init_payload = initiate(command, &initiator_identity_provider);
199
200        // Verifies quorum approved request and initiates challenge.
201        let init_results: Vec<Result<CommandApprovalPayload, IdentityAuthedRequestError>> =
202            approver_identity_providers
203                .iter()
204                .map(|identity_provider| {
205                    verify_request_and_initiate_challenge(
206                        command,
207                        &init_payload,
208                        identity_provider,
209                        &verified_parties,
210                    )
211                })
212                .collect();
213
214        // Verifies expected result.
215        assert!(!init_results.iter().any(|result| result.is_err()));
216
217        // Unwrap challenge fragments.
218        let approvals: Vec<CommandApprovalPayload> = init_results
219            .into_iter()
220            .map(|result| result.unwrap())
221            .collect();
222
223        for (
224            actual_current_signer,
225            approvals_to_sign,
226            quorum_size_to_sign,
227            expected_challenge_result,
228        ) in [
229            // Valid challenge response should be accepted.
230            (
231                &initiator_identity_provider,
232                &approvals,
233                quorum_size,
234                Ok(()),
235            ),
236            (
237                &initiator_identity_provider,
238                &approvals[0..4].to_vec(), // initiator + 4 approvals is a valid quorum (i.e 5 parties)
239                quorum_size,
240                Ok(()),
241            ),
242            // Challenge response from the wrong signer should be rejected.
243            (
244                &MockECDSAIdentityProvider::generate(),
245                &approvals,
246                quorum_size,
247                Err(QuorumApprovedRequestError::Unauthorized(Error::Crypto(
248                    CryptoError::InvalidSignature,
249                ))),
250            ),
251            // Challenge response signing an insufficient number of approvals should be rejected.
252            (
253                &initiator_identity_provider,
254                &approvals[0..3].to_vec(), // initiator + 3 approvals is an insufficient quorum.
255                4, // Allows initiator to successfully sign only 3 approvals (i.e quorum_size - 1).
256                Err(QuorumApprovedRequestError::InsufficientApprovals),
257            ),
258            // Challenge response signing the wrong challenge fragments should be rejected.
259            (
260                &initiator_identity_provider,
261                &approver_identity_providers
262                    .iter()
263                    .map(|identity_provider| {
264                        let challenge_fragment = Random32Bytes::from(U256::ONE);
265                        let signature = identity_provider.sign(&command_approval_message_bytes(
266                            &challenge_fragment,
267                            init_payload.command,
268                            init_payload.timestamp,
269                        ));
270                        CommandApprovalPayload {
271                            challenge_fragment,
272                            verifying_key: identity_provider.verifying_key(),
273                            signature,
274                        }
275                    })
276                    .collect(),
277                quorum_size,
278                Err(QuorumApprovedRequestError::Unauthorized(Error::Crypto(
279                    CryptoError::InvalidSignature,
280                ))),
281            ),
282        ] {
283            // Generates quorum approved challenge response using the "actual signer" and "signing approvals" for this test case.
284            let challenge_response_result = challenge_response(
285                approvals_to_sign,
286                actual_current_signer,
287                &init_payload,
288                quorum_size_to_sign,
289                &verified_parties,
290            );
291
292            // Verifies expected challenge response result.
293            assert!(challenge_response_result.is_ok());
294
295            // Unwraps challenge payload.
296            let challenge_payload = challenge_response_result.unwrap();
297
298            // Verifies quorum approved challenge response using the challenged identity provider and "verification approvals" for this test case.
299            let challenge_result = verify_challenge_response(
300                &challenge_payload,
301                &approvals,
302                &initiator_identity_provider.verifying_key(),
303                &init_payload,
304                quorum_size,
305                &verified_parties,
306            );
307
308            // Verifies expected result.
309            assert_eq!(challenge_result, expected_challenge_result);
310        }
311    }
312}