Skip to main content

bluefin_pro/
authenticate.rs

1use std::borrow::Cow;
2use std::fmt;
3
4use crate::core::PrivateKey;
5use crate::env::Environment;
6use crate::env::auth::url;
7use crate::signature;
8use base64::Engine;
9use base64::prelude::BASE64_STANDARD;
10use bluefin_api::apis::auth_api::{auth_token_refresh_put, auth_v2_token_post};
11use bluefin_api::apis::configuration::Configuration;
12use bluefin_api::models::{LoginRequest, LoginResponse, RefreshTokenRequest, RefreshTokenResponse};
13use secp256k1::Message;
14use sui_crypto::SuiSigner;
15use sui_crypto::ed25519::Ed25519PrivateKey;
16use sui_sdk_types::{PersonalMessage, SignatureScheme};
17
18#[derive(Debug)]
19pub enum Error {
20    AuthenticationRequestFailed(String),
21    AuthenticationRequestSerializationFailed(String),
22}
23
24impl fmt::Display for Error {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Error::AuthenticationRequestFailed(error) => {
28                write!(f, "Authentication request failed: {error}")
29            }
30            Error::AuthenticationRequestSerializationFailed(error) => {
31                write!(f, "Authentication request serialization failed: {error}")
32            }
33        }
34    }
35}
36
37impl std::error::Error for Error {}
38
39type AuthenticationResult<T> = Result<T, Error>;
40
41// Additional options for authenticating and getting a auth token.
42#[derive(Default)]
43pub struct AuthenticationOptions {
44    // The number of seconds the refresh token is valid for. Default is 30 days.
45    pub refresh_token_valid_for_seconds: Option<i64>,
46    // If the auth token should be read-only. Default is false.
47    // A Read-Only token is a token that can only be used to read data from the API.
48    // It cannot be used to write or update data.
49    pub read_only: Option<bool>,
50}
51
52pub trait Authenticate {
53    /// Authenticates using the provided authentication request and the signature.
54    /// Returns the Authentication JWT.
55    ///
56    /// # Errors
57    ///
58    /// Will return `Err` if the request fails.
59    fn authenticate(
60        self,
61        signature: &str,
62        environment: Environment,
63    ) -> impl std::future::Future<Output = AuthenticationResult<LoginResponse>> + Send;
64
65    /// Authenticates using the provided authentication request and the signature.
66    /// Returns the Authentication JWT.
67    ///
68    /// The `AuthenticationOptions` struct is used to specify additional options for the auth token
69    /// such as the refresh token validity period or if the auth token should be read-only.
70    ///
71    /// # Errors
72    ///
73    /// Will return `Err` if the request fails.
74    fn authenticate_with_options(
75        self,
76        signature: &str,
77        environment: Environment,
78        options: AuthenticationOptions,
79    ) -> impl std::future::Future<Output = AuthenticationResult<LoginResponse>> + Send;
80}
81
82pub trait RequestExt: Sized {
83    /// Generates a signature for this request.  The signature will contain the public key.
84    ///
85    /// # Errors
86    ///
87    /// Will return `Err` if the specified private key is invalid.
88    fn signature(
89        &self,
90        scheme: SignatureScheme,
91        private_key: PrivateKey,
92    ) -> signature::Result<String>;
93}
94
95pub trait Refresh {
96    fn refresh(
97        self,
98        environment: Environment,
99    ) -> impl std::future::Future<Output = AuthenticationResult<RefreshTokenResponse>> + Send;
100}
101
102impl RequestExt for LoginRequest {
103    /// Generates a signature for this request.  The signature will contain the public key.
104    ///
105    /// # Errors
106    ///
107    /// Will return `Err` if the specified private key is invalid.
108    fn signature(
109        &self,
110        scheme: SignatureScheme,
111        private_key: PrivateKey,
112    ) -> signature::Result<String> {
113        let bytes = serde_json::to_vec(self).map_err(|_| signature::Error::Serialization)?;
114
115        let personal_message = PersonalMessage(Cow::Borrowed(bytes.as_slice()));
116
117        match scheme {
118            SignatureScheme::Ed25519 => {
119                let private_key = Ed25519PrivateKey::new(private_key);
120
121                let signature = private_key
122                    .sign_personal_message(&personal_message)
123                    .map_err(|e| signature::Error::Signature(e.to_string()))?;
124
125                Ok(signature.to_base64())
126            }
127            SignatureScheme::Secp256k1 => {
128                const RECOVERY_CODE: u8 = 31;
129
130                let secp = secp256k1::Secp256k1::signing_only();
131                let private_key = secp256k1::SecretKey::from_byte_array(&private_key)
132                    .map_err(|error| signature::Error::PrivateKey(error.to_string()))?;
133
134                let signature = secp.sign_ecdsa_recoverable(
135                    &Message::from_digest(personal_message.signing_digest()),
136                    &private_key,
137                );
138
139                let public_key = private_key.public_key(&secp256k1::Secp256k1::signing_only());
140
141                let (recovery_id, signature) = signature.serialize_compact();
142
143                let mut components = vec![SignatureScheme::Secp256k1 as u8];
144                components.push(
145                    RECOVERY_CODE
146                        + u8::try_from(i32::from(recovery_id))
147                            .map_err(|_| signature::Error::PublicKeyRecoveryId)?,
148                );
149                components.extend(signature);
150                components.extend(public_key.serialize());
151
152                Ok(BASE64_STANDARD.encode(&components))
153            }
154            _ => Err(signature::Error::UnsupportedSignatureScheme(scheme)),
155        }
156    }
157}
158
159impl Authenticate for LoginRequest {
160    async fn authenticate(
161        self,
162        signature: &str,
163        environment: Environment,
164    ) -> AuthenticationResult<LoginResponse> {
165        self.authenticate_with_options(signature, environment, AuthenticationOptions::default())
166            .await
167    }
168
169    async fn authenticate_with_options(
170        self,
171        signature: &str,
172        environment: Environment,
173        options: AuthenticationOptions,
174    ) -> AuthenticationResult<LoginResponse> {
175        let base_url = url(environment);
176
177        let mut configuration = Configuration::new();
178        configuration.base_path = String::from(base_url);
179
180        let response = auth_v2_token_post(
181            &configuration,
182            signature,
183            self,
184            options.refresh_token_valid_for_seconds,
185            options.read_only,
186        )
187        .await
188        .map_err(|error| Error::AuthenticationRequestFailed(error.to_string()))?;
189
190        Ok(response)
191    }
192}
193
194impl Refresh for RefreshTokenRequest {
195    async fn refresh(self, environment: Environment) -> AuthenticationResult<RefreshTokenResponse> {
196        let base_url = url(environment);
197
198        let mut configuration = Configuration::new();
199        configuration.base_path = String::from(base_url);
200
201        let response = auth_token_refresh_put(&configuration, self)
202            .await
203            .map_err(|error| Error::AuthenticationRequestFailed(error.to_string()))?;
204
205        Ok(response)
206    }
207}
208
209#[cfg(test)]
210pub mod tests {
211    use crate::env;
212
213    use super::*;
214    use base64::Engine;
215    use base64::prelude::BASE64_STANDARD;
216    use chrono::Utc;
217    use rand::rngs::OsRng;
218    use secp256k1::Message;
219    use secp256k1::ecdsa::RecoveryId;
220    use sui_crypto::{SuiVerifier, ed25519::Ed25519Verifier};
221    use sui_sdk_types::{Ed25519PublicKey, Secp256k1PublicKey, SimpleSignature, UserSignature};
222
223    fn verify_ed25519_signature(
224        encoded_signature: &str,
225        login_payload: &LoginRequest,
226    ) -> Result<(), Box<dyn std::error::Error>> {
227        let signature = UserSignature::from_base64(encoded_signature)
228            .map_err(|_| "Could not base64 decode signature".to_string())?;
229
230        let bytes = serde_json::to_vec(login_payload)
231            .map_err(|_| "Could not serialize auth request".to_string())?;
232
233        let personal_message = PersonalMessage(Cow::Borrowed(bytes.as_slice()));
234
235        match signature {
236            UserSignature::Simple(SimpleSignature::Ed25519 { public_key, .. }) => {
237                let sui_address = public_key.derive_address().to_hex();
238
239                Ed25519Verifier::new()
240                    .verify_personal_message(&personal_message, &signature)
241                    .map_err(|_| "Invalid signature".to_string())?;
242
243                assert_eq!(sui_address, login_payload.account_address);
244            }
245            _ => Err("Unsupported signature scheme".to_string())?,
246        }
247
248        Ok(())
249    }
250
251    // secp256k1
252    fn verify_secp256k1_signature(
253        encoded_signature: &str,
254        login_payload: &LoginRequest,
255    ) -> Result<(), Box<dyn std::error::Error>> {
256        const RECOVERY_CODE: u8 = 31;
257        let signature_bytes = BASE64_STANDARD
258            .decode(encoded_signature)
259            .map_err(|_| "Could not base64 decode signature".to_string())?;
260        // 27 is an old magic number inherited from Bitcoin and is used as a "magic constant"
261        // 4 is a number used to indicate that the public key is compressed
262        // Extract the signature and public key bytes
263        if signature_bytes.len() != 99 {
264            // 1 signature type flag byte + 65 for the signature (1 recovery byte + 64 signature bytes) + 33 public key
265            return Err("Invalid signature length".into());
266        }
267        let recovery_bit = signature_bytes[1] - RECOVERY_CODE;
268
269        let signature = secp256k1::ecdsa::RecoverableSignature::from_compact(
270            &signature_bytes[2..signature_bytes.len() - 33],
271            RecoveryId::try_from(i32::from(recovery_bit))
272                .map_err(|_| "Invalid secp256k1 recovery ID".to_string())?,
273        )?;
274        let public_key = secp256k1::PublicKey::from_slice(&signature_bytes[(1 + 65)..])?;
275
276        // get message bytes
277        let bytes = serde_json::to_vec(login_payload)
278            .map_err(|_| "Could not serialize auth request".to_string())?;
279        let personal_message = PersonalMessage(Cow::Borrowed(bytes.as_slice()));
280
281        // Verify the signature
282        let message = Message::from_digest(personal_message.signing_digest());
283
284        let recovered_public_key = signature
285            .recover(&message)
286            .map_err(|_| "Invalid secp256k1 signature".to_string())?;
287
288        assert_eq!(public_key, recovered_public_key);
289        Ok(())
290    }
291
292    #[test]
293    fn sign_auth_request() -> Result<(), Box<dyn std::error::Error>> {
294        // ed25519
295        let private_key = ed25519_dalek::SigningKey::generate(&mut OsRng);
296        let public_key = private_key.verifying_key().to_bytes();
297        let public_key = Ed25519PublicKey::new(public_key);
298
299        let sui_address = public_key.derive_address().to_hex();
300
301        let auth_request = LoginRequest {
302            account_address: sui_address,
303            audience: env::auth::staging::AUDIENCE.into(),
304            signed_at_millis: Utc::now().timestamp_millis(),
305        };
306
307        let signature = auth_request
308            .signature(SignatureScheme::Ed25519, private_key.to_bytes())
309            .map_err(|error| format!("{error:?}"))?;
310        verify_ed25519_signature(&signature, &auth_request)?;
311
312        let signature = auth_request
313            .signature(SignatureScheme::Secp256k1, private_key.to_bytes())
314            .map_err(|error| format!("{error:?}"))?;
315        verify_secp256k1_signature(&signature, &auth_request)?;
316
317        Ok(())
318    }
319
320    #[tokio::test]
321    async fn authenticate_staging_ed25519() -> Result<(), Box<dyn std::error::Error>> {
322        let private_key = ed25519_dalek::SigningKey::generate(&mut OsRng);
323        let public_key = private_key.verifying_key().to_bytes();
324        let public_key = Ed25519PublicKey::new(public_key);
325
326        let sui_address = public_key.derive_address().to_hex();
327
328        let auth_request = LoginRequest {
329            account_address: sui_address,
330            audience: env::auth::staging::AUDIENCE.into(),
331            signed_at_millis: Utc::now().timestamp_millis(),
332        };
333
334        let signature = auth_request
335            .signature(SignatureScheme::Ed25519, private_key.to_bytes())
336            .map_err(|error| format!("{error:?}"))?;
337
338        auth_request
339            .authenticate(&signature, Environment::Staging)
340            .await
341            .map_err(|error| format!("{error:?}"))?;
342        Ok(())
343    }
344
345    #[tokio::test]
346    async fn authenticate_staging_ed25519_with_options() -> Result<(), Box<dyn std::error::Error>> {
347        let private_key = ed25519_dalek::SigningKey::generate(&mut OsRng);
348        let public_key = private_key.verifying_key().to_bytes();
349        let public_key = Ed25519PublicKey::new(public_key);
350
351        let sui_address = public_key.derive_address().to_hex();
352
353        let auth_request = LoginRequest {
354            account_address: sui_address,
355            audience: env::auth::staging::AUDIENCE.into(),
356            signed_at_millis: Utc::now().timestamp_millis(),
357        };
358
359        let signature = auth_request
360            .signature(SignatureScheme::Ed25519, private_key.to_bytes())
361            .map_err(|error| format!("{error:?}"))?;
362
363        let response = auth_request
364            .authenticate_with_options(
365                &signature,
366                Environment::Staging,
367                AuthenticationOptions {
368                    read_only: Some(true),
369                    refresh_token_valid_for_seconds: Some(10),
370                },
371            )
372            .await
373            .map_err(|error| format!("{error:?}"))?;
374
375        assert_eq!(response.refresh_token_valid_for_seconds, 10);
376        Ok(())
377    }
378
379    #[tokio::test]
380    async fn authenticate_staging_secp256k1() -> Result<(), Box<dyn std::error::Error>> {
381        let (private_key, public_key) = secp256k1::generate_keypair(&mut OsRng);
382        let public_key = Secp256k1PublicKey::new(public_key.serialize());
383
384        let sui_address = public_key.derive_address().to_hex();
385
386        let auth_request = LoginRequest {
387            account_address: sui_address,
388            audience: env::auth::staging::AUDIENCE.into(),
389            signed_at_millis: Utc::now().timestamp_millis(),
390        };
391
392        let signature = auth_request
393            .signature(SignatureScheme::Secp256k1, private_key.secret_bytes())
394            .map_err(|error| format!("{error:?}"))?;
395
396        auth_request
397            .authenticate(&signature, Environment::Staging)
398            .await
399            .map_err(|error| format!("{error:?}"))?;
400        Ok(())
401    }
402}