diff --git a/services/identity/src/grpc_utils.rs b/services/identity/src/grpc_utils.rs index 25426babf..f0ba94950 100644 --- a/services/identity/src/grpc_utils.rs +++ b/services/identity/src/grpc_utils.rs @@ -1,285 +1,295 @@ use base64::{engine::general_purpose, Engine as _}; use ed25519_dalek::{PublicKey, Signature, Verifier}; use serde::Deserialize; use tonic::Status; use crate::{ database::DeviceRow, ddb_utils::Identifier as DBIdentifier, grpc_services::protos::{ auth::{EthereumIdentity, Identity, InboundKeyInfo, OutboundKeyInfo}, unauth::{ DeviceKeyUpload, ExistingDeviceLoginRequest, OpaqueLoginStartRequest, RegistrationStartRequest, ReservedRegistrationStartRequest, ReservedWalletRegistrationRequest, SecondaryDeviceKeysUploadRequest, WalletAuthRequest, }, }, }; #[derive(Deserialize)] pub struct SignedNonce { nonce: String, signature: String, } impl TryFrom<&SecondaryDeviceKeysUploadRequest> for SignedNonce { type Error = Status; fn try_from( value: &SecondaryDeviceKeysUploadRequest, ) -> Result { Ok(Self { nonce: value.nonce.to_string(), signature: value.nonce_signature.to_string(), }) } } impl TryFrom<&ExistingDeviceLoginRequest> for SignedNonce { type Error = Status; fn try_from(value: &ExistingDeviceLoginRequest) -> Result { Ok(Self { nonce: value.nonce.to_string(), signature: value.nonce_signature.to_string(), }) } } impl SignedNonce { pub fn verify_and_get_nonce( self, signing_public_key: &str, ) -> Result { - let signature_bytes = general_purpose::STANDARD_NO_PAD - .decode(&self.signature) - .map_err(|_| Status::invalid_argument("signature invalid"))?; - - let signature = Signature::from_bytes(&signature_bytes) - .map_err(|_| Status::invalid_argument("signature invalid"))?; - - let public_key_bytes = general_purpose::STANDARD_NO_PAD - .decode(signing_public_key) - .map_err(|_| Status::failed_precondition("malformed key"))?; - - let public_key: PublicKey = PublicKey::from_bytes(&public_key_bytes) - .map_err(|_| Status::failed_precondition("malformed key"))?; - - public_key - .verify(self.nonce.as_bytes(), &signature) - .map_err(|_| Status::permission_denied("verification failed"))?; - + ed25519_verify(signing_public_key, &self.nonce, &self.signature)?; Ok(self.nonce) } } +/// Verifies ed25519-signed message. Returns Ok if the signature is valid. +/// Public key and signature should be base64-encoded strings. +pub fn ed25519_verify( + signing_public_key: &str, + message: &str, + signature: &str, +) -> Result<(), Status> { + let signature_bytes = general_purpose::STANDARD_NO_PAD + .decode(signature) + .map_err(|_| Status::invalid_argument("signature invalid"))?; + + let signature = Signature::from_bytes(&signature_bytes) + .map_err(|_| Status::invalid_argument("signature invalid"))?; + + let public_key_bytes = general_purpose::STANDARD_NO_PAD + .decode(signing_public_key) + .map_err(|_| Status::failed_precondition("malformed key"))?; + + let public_key: PublicKey = PublicKey::from_bytes(&public_key_bytes) + .map_err(|_| Status::failed_precondition("malformed key"))?; + + public_key + .verify(message.as_bytes(), &signature) + .map_err(|_| Status::permission_denied("verification failed"))?; + Ok(()) +} + pub struct DeviceKeysInfo { pub device_info: DeviceRow, pub content_one_time_key: Option, pub notif_one_time_key: Option, } impl From for DeviceKeysInfo { fn from(device_info: DeviceRow) -> Self { Self { device_info, content_one_time_key: None, notif_one_time_key: None, } } } impl From for InboundKeyInfo { fn from(info: DeviceKeysInfo) -> Self { let DeviceKeysInfo { device_info, .. } = info; InboundKeyInfo { identity_info: Some(device_info.device_key_info.into()), content_prekey: Some(device_info.content_prekey.into()), notif_prekey: Some(device_info.notif_prekey.into()), } } } impl From for OutboundKeyInfo { fn from(info: DeviceKeysInfo) -> Self { let DeviceKeysInfo { device_info, content_one_time_key, notif_one_time_key, } = info; OutboundKeyInfo { identity_info: Some(device_info.device_key_info.into()), content_prekey: Some(device_info.content_prekey.into()), notif_prekey: Some(device_info.notif_prekey.into()), one_time_content_prekey: content_one_time_key, one_time_notif_prekey: notif_one_time_key, } } } pub trait DeviceKeyUploadData { fn device_key_upload(&self) -> Option<&DeviceKeyUpload>; } impl DeviceKeyUploadData for RegistrationStartRequest { fn device_key_upload(&self) -> Option<&DeviceKeyUpload> { self.device_key_upload.as_ref() } } impl DeviceKeyUploadData for ReservedRegistrationStartRequest { fn device_key_upload(&self) -> Option<&DeviceKeyUpload> { self.device_key_upload.as_ref() } } impl DeviceKeyUploadData for OpaqueLoginStartRequest { fn device_key_upload(&self) -> Option<&DeviceKeyUpload> { self.device_key_upload.as_ref() } } impl DeviceKeyUploadData for WalletAuthRequest { fn device_key_upload(&self) -> Option<&DeviceKeyUpload> { self.device_key_upload.as_ref() } } impl DeviceKeyUploadData for ReservedWalletRegistrationRequest { fn device_key_upload(&self) -> Option<&DeviceKeyUpload> { self.device_key_upload.as_ref() } } impl DeviceKeyUploadData for SecondaryDeviceKeysUploadRequest { fn device_key_upload(&self) -> Option<&DeviceKeyUpload> { self.device_key_upload.as_ref() } } pub trait DeviceKeyUploadActions { fn payload(&self) -> Result; fn payload_signature(&self) -> Result; fn content_prekey(&self) -> Result; fn content_prekey_signature(&self) -> Result; fn notif_prekey(&self) -> Result; fn notif_prekey_signature(&self) -> Result; fn one_time_content_prekeys(&self) -> Result, Status>; fn one_time_notif_prekeys(&self) -> Result, Status>; fn device_type(&self) -> Result; } impl DeviceKeyUploadActions for T { fn payload(&self) -> Result { self .device_key_upload() .and_then(|upload| upload.device_key_info.as_ref()) .map(|info| info.payload.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn payload_signature(&self) -> Result { self .device_key_upload() .and_then(|upload| upload.device_key_info.as_ref()) .map(|info| info.payload_signature.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn content_prekey(&self) -> Result { self .device_key_upload() .and_then(|upload| upload.content_upload.as_ref()) .map(|prekey| prekey.prekey.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn content_prekey_signature(&self) -> Result { self .device_key_upload() .and_then(|upload| upload.content_upload.as_ref()) .map(|prekey| prekey.prekey_signature.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn notif_prekey(&self) -> Result { self .device_key_upload() .and_then(|upload| upload.notif_upload.as_ref()) .map(|prekey| prekey.prekey.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn notif_prekey_signature(&self) -> Result { self .device_key_upload() .and_then(|upload| upload.notif_upload.as_ref()) .map(|prekey| prekey.prekey_signature.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn one_time_content_prekeys(&self) -> Result, Status> { self .device_key_upload() .map(|upload| upload.one_time_content_prekeys.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn one_time_notif_prekeys(&self) -> Result, Status> { self .device_key_upload() .map(|upload| upload.one_time_notif_prekeys.clone()) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } fn device_type(&self) -> Result { self .device_key_upload() .map(|upload| upload.device_type) .ok_or_else(|| Status::invalid_argument("unexpected message data")) } } impl From for Identity { fn from(value: DBIdentifier) -> Self { match value { DBIdentifier::Username(username) => Identity { username, eth_identity: None, }, DBIdentifier::WalletAddress(eth_identity) => Identity { username: eth_identity.wallet_address.clone(), eth_identity: Some(EthereumIdentity { wallet_address: eth_identity.wallet_address, siwe_message: eth_identity.social_proof.message, siwe_signature: eth_identity.social_proof.signature, }), }, } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_challenge_response_verification() { let expected_nonce = "hello"; let signing_key = "jnBariweGMSdfmJYvuObTu4IGT1fpaJTo/ovbkU0SAY"; let request = SecondaryDeviceKeysUploadRequest { nonce: expected_nonce.to_string(), nonce_signature: "LWlgCDND3bmgIS8liW/0eKJvuNs4Vcb4iMf43zD038/MnC0cSAYl2l3bO9dFc0fa2w6/2ABsUlPDMVr+isE0Aw".to_string(), user_id: "foo".to_string(), device_key_upload: None, }; let challenge_response = SignedNonce::try_from(&request) .expect("failed to parse challenge response"); let retrieved_nonce = challenge_response .verify_and_get_nonce(signing_key) .expect("verification failed"); assert_eq!(retrieved_nonce, expected_nonce); } } diff --git a/services/identity/src/reserved_users.rs b/services/identity/src/reserved_users.rs index 07dd7324d..ed5ce279b 100644 --- a/services/identity/src/reserved_users.rs +++ b/services/identity/src/reserved_users.rs @@ -1,128 +1,114 @@ -use base64::{engine::general_purpose, Engine as _}; use chrono::{DateTime, Utc}; use constant_time_eq::constant_time_eq; -use ed25519_dalek::{PublicKey, Signature, Verifier}; use serde::Deserialize; use tonic::Status; use crate::config::CONFIG; // This type should not be changed without making equivalent changes to // `ReservedUsernameMessage` in lib/types/crypto-types.js #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct Message { statement: String, payload: T, issued_at: String, } // This type should not be changed without making equivalent changes to // `ReservedUsernameMessage` in lib/types/crypto-types.js #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct UserDetail { pub username: String, #[serde(rename = "userID")] pub user_id: String, } fn validate_and_decode_message( keyserver_message: &str, keyserver_signature: &str, expected_statement: &[u8], ) -> Result, Status> { let deserialized_message: Message = serde_json::from_str(keyserver_message) .map_err(|_| Status::invalid_argument("message format invalid"))?; if !constant_time_eq( deserialized_message.statement.as_bytes(), expected_statement, ) { return Err(Status::invalid_argument("message invalid")); } let issued_at: DateTime = deserialized_message .issued_at .parse() .map_err(|_| Status::invalid_argument("message format invalid"))?; let now = Utc::now(); if (now - issued_at).num_seconds() > 5 { return Err(Status::invalid_argument("message invalid")); } - let signature_bytes = general_purpose::STANDARD_NO_PAD - .decode(keyserver_signature) - .map_err(|_| Status::invalid_argument("signature invalid"))?; - - let signature = Signature::from_bytes(&signature_bytes) - .map_err(|_| Status::invalid_argument("signature invalid"))?; - let public_key_string = CONFIG .keyserver_public_key - .clone() + .as_deref() .ok_or_else(|| Status::failed_precondition("missing key"))?; - let public_key_bytes = general_purpose::STANDARD_NO_PAD - .decode(public_key_string) - .map_err(|_| Status::failed_precondition("malformed key"))?; - - let public_key: PublicKey = PublicKey::from_bytes(&public_key_bytes) - .map_err(|_| Status::failed_precondition("malformed key"))?; - - public_key - .verify(keyserver_message.as_bytes(), &signature) - .map_err(|_| Status::permission_denied("verification failed"))?; + crate::grpc_utils::ed25519_verify( + public_key_string, + keyserver_message, + keyserver_signature, + )?; Ok(deserialized_message) } pub fn validate_account_ownership_message_and_get_user_id( username: &str, keyserver_message: &str, keyserver_signature: &str, ) -> Result { // Note that username in this context includes wallet addresses, too. const EXPECTED_STATEMENT: &[u8; 60] = b"This user is the owner of the following username and user ID"; let deserialized_message = validate_and_decode_message::( keyserver_message, keyserver_signature, EXPECTED_STATEMENT, )?; if deserialized_message.payload.username != username { return Err(Status::invalid_argument("message invalid")); } Ok(deserialized_message.payload.user_id) } pub fn validate_add_reserved_usernames_message( keyserver_message: &str, keyserver_signature: &str, ) -> Result, Status> { let deserialized_message = validate_and_decode_message::>( keyserver_message, keyserver_signature, b"Add the following usernames to reserved list", )?; Ok(deserialized_message.payload) } pub fn validate_remove_reserved_username_message( keyserver_message: &str, keyserver_signature: &str, ) -> Result { let deserialized_message = validate_and_decode_message::( keyserver_message, keyserver_signature, b"Remove the following username from reserved list", )?; Ok(deserialized_message.payload) }