wamu_core/
identity_authed_request.rs

1//! Identity authenticated request initiation and verification implementation.
2//!
3//! Ref: <https://wamu.tech/specification#identity-authed-request>.
4
5use crate::crypto::VerifyingKey;
6use crate::errors::{Error, IdentityAuthedRequestError};
7use crate::payloads::IdentityAuthedRequestPayload;
8use crate::traits::IdentityProvider;
9use crate::{crypto, utils};
10
11/// How long a request remains valid.
12const EXPIRY_TIMEOUT: u64 = 60 * 60; // 1 hour.
13
14/// How far in the future a request is allowed to be (e.g due to out of sync clocks between parties).
15const FUTURE_TIMESTAMP_TOLERANCE: u64 = 5 * 60; // 5 minutes.
16
17/// Given a "command" and an identity provider, returns the payload for initiating an identity authenticated request.
18///
19/// Ref: <https://wamu.tech/specification#identity-authed-request-initiation>.
20pub fn initiate(
21    command: &'static str,
22    identity_provider: &impl IdentityProvider,
23) -> IdentityAuthedRequestPayload {
24    let timestamp = utils::unix_timestamp();
25    let signature = identity_provider.sign(&command_message_bytes(command, timestamp));
26
27    IdentityAuthedRequestPayload {
28        command,
29        verifying_key: identity_provider.verifying_key(),
30        timestamp,
31        signature,
32    }
33}
34
35/// Given a "command", an identity authenticated request payload and a list of verifying keys for the other parties,
36/// returns an ok result for a valid request or an appropriate error result for an invalid request.
37///
38/// Ref: <https://wamu.tech/specification#identity-authed-request-verification>.
39pub fn verify(
40    request: &IdentityAuthedRequestPayload,
41    verified_parties: &[VerifyingKey],
42) -> Result<(), IdentityAuthedRequestError> {
43    if !verified_parties.contains(&request.verifying_key) {
44        // Sender must be a verified party.
45        Err(IdentityAuthedRequestError::Unauthorized(
46            Error::UnauthorizedParty,
47        ))
48    } else if request.timestamp + EXPIRY_TIMEOUT < utils::unix_timestamp() {
49        // Request should be initiated during the current epoch.
50        Err(IdentityAuthedRequestError::Expired)
51    } else if utils::unix_timestamp() + FUTURE_TIMESTAMP_TOLERANCE < request.timestamp {
52        // Request can't be too far into the future (i.e clocks can't be exactly synchronized but tolerance should be reasonable).
53        Err(IdentityAuthedRequestError::InvalidTimestamp)
54    } else {
55        // Command signature must be valid.
56        Ok(crypto::verify_signature(
57            &request.verifying_key,
58            &command_message_bytes(request.command, request.timestamp),
59            &request.signature,
60        )?)
61    }
62}
63
64/// Returns sign-able message bytes for the command and timestamp.
65fn command_message_bytes(command: &str, timestamp: u64) -> Vec<u8> {
66    utils::prefix_message_bytes(format!("{}{}", command, timestamp).as_bytes())
67}
68
69#[cfg(test)]
70mod test {
71    use super::*;
72    use crate::errors::CryptoError;
73    use crate::test_utils::MockECDSAIdentityProvider;
74
75    #[test]
76    fn identity_authed_request_initiation_and_verification_works() {
77        // Generates identity provider.
78        let identity_provider = MockECDSAIdentityProvider::generate();
79
80        // Generates identity authenticated request payload.
81        let payload = initiate("command", &identity_provider);
82
83        for (verified_parties, timestamp_modification, signature_modification, expected_result) in [
84            // Valid request from a verified party should be ok.
85            (vec![identity_provider.verifying_key()], None, None, Ok(())),
86            // Request from an unverified party should fail.
87            (
88                vec![],
89                None,
90                None,
91                Err(IdentityAuthedRequestError::Unauthorized(
92                    Error::UnauthorizedParty,
93                )),
94            ),
95            // Request with a timestamp set to a past value outside the expiry timeout should fail.
96            (
97                vec![identity_provider.verifying_key()],
98                Some(-(EXPIRY_TIMEOUT as i64 + 1)),
99                None,
100                Err(IdentityAuthedRequestError::Expired),
101            ),
102            // Request with a timestamp set to a future value outside the future timestamp tolerance should fail.
103            (
104                vec![identity_provider.verifying_key()],
105                Some(FUTURE_TIMESTAMP_TOLERANCE as i64 + 1),
106                None,
107                Err(IdentityAuthedRequestError::InvalidTimestamp),
108            ),
109            // Request with an invalid signature should fail.
110            (
111                vec![identity_provider.verifying_key()],
112                None,
113                Some(identity_provider.sign(b"Hello, world!")),
114                Err(IdentityAuthedRequestError::Unauthorized(Error::Crypto(
115                    CryptoError::InvalidSignature,
116                ))),
117            ),
118        ] {
119            // Creates a copy of payload for this test case.
120            let mut modified_payload = payload.clone();
121
122            // Applies test case timestamp modification (if any).
123            if let Some(delta) = timestamp_modification {
124                modified_payload.timestamp = (modified_payload.timestamp as i64 + delta) as u64;
125            }
126
127            // Applies test case signature modification (if any).
128            if let Some(modified_signature) = signature_modification {
129                modified_payload.signature = modified_signature;
130            }
131
132            // Verifies identity authenticated request payload.
133            let result = verify(&modified_payload, &verified_parties);
134
135            // Verifies expected result.
136            assert_eq!(result, expected_result);
137        }
138    }
139}