From b753ccfe681f1ec761c8cf01bda995ae19547659 Mon Sep 17 00:00:00 2001 From: gemcoder21 <104884878+gemcoder21@users.noreply.github.com> Date: Mon, 25 May 2026 21:57:22 +0000 Subject: [PATCH] Remove legacy device authentication --- apps/api/src/auth/guard.rs | 37 ++----------------- apps/api/src/devices/auth_config.rs | 5 +-- apps/api/src/devices/constants.rs | 6 --- apps/api/src/devices/error.rs | 2 + apps/api/src/devices/guard/auth.rs | 23 ++---------- .../guard/authenticated_device_wallet.rs | 3 +- apps/api/src/devices/mod.rs | 4 +- apps/api/src/devices/signature.rs | 37 +++---------------- apps/api/src/main.rs | 4 +- crates/gem_auth/src/device_signature.rs | 15 +------- crates/gem_auth/src/lib.rs | 2 +- docs/DEVICE_AUTHENTICATION.md | 37 ++----------------- docs/WALLET_AUTHENTICATION.md | 8 +--- 13 files changed, 28 insertions(+), 155 deletions(-) diff --git a/apps/api/src/auth/guard.rs b/apps/api/src/auth/guard.rs index 5a1cd69b5..0d3cb8cfb 100644 --- a/apps/api/src/auth/guard.rs +++ b/apps/api/src/auth/guard.rs @@ -1,7 +1,8 @@ +use crate::devices::signature::parse_auth_components; use crate::responders::cache_error; use gem_auth::{AuthClient, verify_auth_signature}; use gem_hash::sha2::sha256; -use primitives::{AuthMessage, AuthenticatedRequest, WalletId}; +use primitives::{AuthMessage, AuthenticatedRequest}; use rocket::data::{FromData, Outcome, ToByteUnit}; use rocket::http::Status; use rocket::outcome::Outcome::{Error, Success}; @@ -33,7 +34,8 @@ async fn verify_wallet_signature<'r, T: DeserializeOwned + Send, O>(req: &'r Req let raw_body = bytes.into_inner(); - if let Some(expected_hash) = req.headers().get_one("x-device-body-hash") { + if let Ok(components) = parse_auth_components(req) { + let expected_hash = components.body_hash; let actual_hash = hex::encode(sha256(&raw_body)); if actual_hash != expected_hash { return Err(error_outcome(req, Status::BadRequest, "Body hash mismatch")); @@ -76,12 +78,6 @@ pub struct WalletSigned { pub data: T, } -impl WalletSigned { - pub fn matches_multicoin_wallet(&self, wallet_id: &WalletId) -> bool { - WalletId::Multicoin(self.address.clone()) == *wallet_id - } -} - #[rocket::async_trait] impl<'r, T: DeserializeOwned + Send> FromData<'r> for WalletSigned { type Error = String; @@ -98,28 +94,3 @@ impl<'r, T: DeserializeOwned + Send> FromData<'r> for WalletSigned { }) } } - -#[cfg(test)] -mod tests { - use super::WalletSigned; - use primitives::{Chain, WalletId}; - - const ADDRESS: &str = "0x1111111111111111111111111111111111111111"; - const OTHER_ADDRESS: &str = "0x2222222222222222222222222222222222222222"; - - fn signed_wallet(address: &str) -> WalletSigned<()> { - WalletSigned { - address: address.to_string(), - data: (), - } - } - - #[test] - fn test_matches_multicoin_wallet() { - let request = signed_wallet(ADDRESS); - - assert!(request.matches_multicoin_wallet(&WalletId::Multicoin(ADDRESS.to_string()))); - assert!(!request.matches_multicoin_wallet(&WalletId::Multicoin(OTHER_ADDRESS.to_string()))); - assert!(!request.matches_multicoin_wallet(&WalletId::Single(Chain::Ethereum, ADDRESS.to_string()))); - } -} diff --git a/apps/api/src/devices/auth_config.rs b/apps/api/src/devices/auth_config.rs index 03082ce7d..5c6d20f15 100644 --- a/apps/api/src/devices/auth_config.rs +++ b/apps/api/src/devices/auth_config.rs @@ -6,13 +6,12 @@ pub struct JwtConfig { } pub struct AuthConfig { - pub enabled: bool, pub tolerance: Duration, pub jwt: JwtConfig, } impl AuthConfig { - pub fn new(enabled: bool, tolerance: Duration, jwt: JwtConfig) -> Self { - Self { enabled, tolerance, jwt } + pub fn new(tolerance: Duration, jwt: JwtConfig) -> Self { + Self { tolerance, jwt } } } diff --git a/apps/api/src/devices/constants.rs b/apps/api/src/devices/constants.rs index 105746673..e6eadfeb1 100644 --- a/apps/api/src/devices/constants.rs +++ b/apps/api/src/devices/constants.rs @@ -1,9 +1,3 @@ -pub const HEADER_DEVICE_ID: &str = "x-device-id"; -pub const HEADER_WALLET_ID: &str = "x-wallet-id"; -pub const HEADER_DEVICE_SIGNATURE: &str = "x-device-signature"; -pub const HEADER_DEVICE_TIMESTAMP: &str = "x-device-timestamp"; -pub const HEADER_DEVICE_BODY_HASH: &str = "x-device-body-hash"; - pub const DEVICE_ID_LENGTH: usize = 64; pub const AUTHORIZATION_HEADER: &str = "Authorization"; diff --git a/apps/api/src/devices/error.rs b/apps/api/src/devices/error.rs index 8b7051120..708607210 100644 --- a/apps/api/src/devices/error.rs +++ b/apps/api/src/devices/error.rs @@ -8,6 +8,7 @@ pub enum DeviceError { InvalidSignature, DeviceNotFound, WalletNotFound, + MissingWalletId, DatabaseUnavailable, InvalidAuthorizationFormat, DatabaseError, @@ -23,6 +24,7 @@ impl fmt::Display for DeviceError { Self::InvalidSignature => write!(f, "Invalid signature"), Self::DeviceNotFound => write!(f, "Device not found"), Self::WalletNotFound => write!(f, "Wallet not found"), + Self::MissingWalletId => write!(f, "Missing wallet ID"), Self::DatabaseUnavailable => write!(f, "Database not available"), Self::InvalidAuthorizationFormat => write!(f, "Invalid authorization format"), Self::DatabaseError => write!(f, "Database error"), diff --git a/apps/api/src/devices/guard/auth.rs b/apps/api/src/devices/guard/auth.rs index 9b3fae474..daad7b72c 100644 --- a/apps/api/src/devices/guard/auth.rs +++ b/apps/api/src/devices/guard/auth.rs @@ -7,7 +7,7 @@ use storage::models::{DeviceRow, WalletRow}; use storage::{Database, DatabaseClient, WalletsRepository}; use crate::devices::auth_config::AuthConfig; -use crate::devices::constants::{DEVICE_ID_LENGTH, HEADER_DEVICE_ID, HEADER_WALLET_ID}; +use crate::devices::constants::DEVICE_ID_LENGTH; use crate::devices::error::DeviceError; use crate::devices::signature::{parse_auth_components, verify_request_signature}; use crate::responders::cache_error; @@ -24,6 +24,7 @@ pub(super) fn auth_error_outcome(req: &Request<'_>, error: DeviceError, devic | DeviceError::InvalidTimestamp | DeviceError::TimestampExpired | DeviceError::InvalidSignature + | DeviceError::MissingWalletId | DeviceError::InvalidAuthorizationFormat => Status::Unauthorized, DeviceError::DeviceNotFound | DeviceError::WalletNotFound => Status::NotFound, DeviceError::DatabaseUnavailable | DeviceError::DatabaseError => Status::InternalServerError, @@ -49,22 +50,6 @@ pub(super) async fn authenticate(req: &Request<'_>) -> Result(req: &Request<'_>) -> Result FromRequest<'r> for AuthenticatedDeviceWallet { }; let Some(wallet_id_str) = auth.wallet_id else { - return auth_error_outcome(req, DeviceError::MissingHeader(HEADER_WALLET_ID), Some(&auth.device_id), None); + return auth_error_outcome(req, DeviceError::MissingWalletId, Some(&auth.device_id), None); }; let (device_row, wallet_row) = match lookup_device_wallet(req, &auth.device_id, &wallet_id_str).await { diff --git a/apps/api/src/devices/mod.rs b/apps/api/src/devices/mod.rs index 4d7d7b5ba..a23ca4f3c 100644 --- a/apps/api/src/devices/mod.rs +++ b/apps/api/src/devices/mod.rs @@ -29,7 +29,7 @@ use primitives::rewards::{RedemptionRequest, RedemptionResult, RewardRedemptionO use primitives::{ AddressName, AssetId, AuthNonce, ChainAddress, FiatAssets, FiatQuote, FiatQuoteRequest, FiatQuoteType, FiatQuoteUrl, FiatQuotes, InAppNotification, NFTData, PortfolioAssets, PortfolioAssetsRequest, PriceAlerts, ReportNft, RewardEvent, Rewards, ScanTransaction, ScanTransactionPayload, Transaction, TransactionsResponse, WalletConfigurationResult, - WalletSubscriptionChains, + WalletId, WalletSubscriptionChains, }; use rocket::{State, delete, get, post, put, serde::json::Json, tokio::sync::Mutex}; use std::sync::Arc; @@ -193,7 +193,7 @@ pub async fn redeem_device_rewards_v2( request: WalletSigned, client: &State>, ) -> Result, ApiError> { - if !request.matches_multicoin_wallet(&device.wallet_identifier) { + if WalletId::Multicoin(request.address.clone()) != device.wallet_identifier { return Err(ApiError::BadRequest("Wallet signature mismatch".to_string())); } diff --git a/apps/api/src/devices/signature.rs b/apps/api/src/devices/signature.rs index 6e9e8da5f..33c09198e 100644 --- a/apps/api/src/devices/signature.rs +++ b/apps/api/src/devices/signature.rs @@ -1,33 +1,15 @@ use std::time::{SystemTime, UNIX_EPOCH}; -use gem_auth::{DeviceAuthPayload, decode_signature, parse_device_auth, verify_device_signature}; +use gem_auth::{DeviceAuthPayload, parse_device_auth, verify_device_signature}; use rocket::Request; use rocket::http::Status; -use crate::devices::constants::{AUTHORIZATION_HEADER, HEADER_DEVICE_BODY_HASH, HEADER_DEVICE_ID, HEADER_DEVICE_SIGNATURE, HEADER_DEVICE_TIMESTAMP}; +use crate::devices::constants::AUTHORIZATION_HEADER; use crate::devices::error::DeviceError; pub fn parse_auth_components(req: &Request<'_>) -> Result { - if let Some(auth_value) = req.headers().get_one(AUTHORIZATION_HEADER) - && auth_value.starts_with(gem_auth::GEM_AUTH_SCHEME) - { - return parse_device_auth(auth_value).ok_or(DeviceError::InvalidAuthorizationFormat); - } - - let device_id = req.headers().get_one(HEADER_DEVICE_ID).ok_or(DeviceError::MissingHeader(HEADER_DEVICE_ID))?; - let timestamp = req.headers().get_one(HEADER_DEVICE_TIMESTAMP).ok_or(DeviceError::MissingHeader(HEADER_DEVICE_TIMESTAMP))?; - let body_hash = req.headers().get_one(HEADER_DEVICE_BODY_HASH).ok_or(DeviceError::MissingHeader(HEADER_DEVICE_BODY_HASH))?; - let signature = req.headers().get_one(HEADER_DEVICE_SIGNATURE).ok_or(DeviceError::MissingHeader(HEADER_DEVICE_SIGNATURE))?; - let signature = decode_signature(signature).ok_or(DeviceError::InvalidSignature)?; - - Ok(DeviceAuthPayload { - scheme: gem_auth::AuthScheme::Legacy, - device_id: device_id.to_string(), - timestamp: timestamp.to_string(), - wallet_id: None, - body_hash: body_hash.to_string(), - signature, - }) + let auth_value = req.headers().get_one(AUTHORIZATION_HEADER).ok_or(DeviceError::MissingHeader(AUTHORIZATION_HEADER))?; + parse_device_auth(auth_value).ok_or(DeviceError::InvalidAuthorizationFormat) } pub fn verify_request_signature(req: &Request<'_>, components: &DeviceAuthPayload, tolerance_ms: u64) -> Result<(), (Status, String)> { @@ -46,15 +28,8 @@ pub fn verify_request_signature(req: &Request<'_>, components: &DeviceAuthPayloa let method = req.method().as_str(); let path = req.uri().path().as_str(); - let message = match components.scheme { - gem_auth::AuthScheme::Gem => { - let wallet_id = components.wallet_id.as_deref().unwrap_or(""); - format!("{}.{}.{}.{}.{}", components.timestamp, method, path, wallet_id, components.body_hash) - } - gem_auth::AuthScheme::Legacy => { - format!("v1.{}.{}.{}.{}", components.timestamp, method, path, components.body_hash) - } - }; + let wallet_id = components.wallet_id.as_deref().unwrap_or(""); + let message = format!("{}.{}.{}.{}.{}", components.timestamp, method, path, wallet_id, components.body_hash); if !verify_device_signature(&components.device_id, &message, &components.signature) { return Err((Status::Unauthorized, DeviceError::InvalidSignature.to_string())); diff --git a/apps/api/src/main.rs b/apps/api/src/main.rs index 7fcb92557..7f272b9ac 100644 --- a/apps/api/src/main.rs +++ b/apps/api/src/main.rs @@ -251,7 +251,7 @@ async fn rocket_api(settings: Settings) -> Result, Box Rocket { secret: settings.api.auth.jwt.secret.clone(), expiry: settings.api.auth.jwt.expiry, }; - let auth_config = devices::auth_config::AuthConfig::new(settings.api.auth.enabled, settings.api.auth.tolerance, jwt_config); + let auth_config = devices::auth_config::AuthConfig::new(settings.api.auth.tolerance, jwt_config); rocket::build() .manage(auth_config) diff --git a/crates/gem_auth/src/device_signature.rs b/crates/gem_auth/src/device_signature.rs index 8c1452d94..6ab1a7a04 100644 --- a/crates/gem_auth/src/device_signature.rs +++ b/crates/gem_auth/src/device_signature.rs @@ -2,16 +2,9 @@ use alloy_primitives::hex; use ed25519_dalek::{Signature, VerifyingKey}; use gem_encoding::decode_base64; -pub const GEM_AUTH_SCHEME: &str = "Gem "; - -#[derive(Debug, PartialEq)] -pub enum AuthScheme { - Gem, - Legacy, -} +const GEM_AUTH_SCHEME: &str = "Gem "; pub struct DeviceAuthPayload { - pub scheme: AuthScheme, pub device_id: String, pub timestamp: String, pub wallet_id: Option, @@ -28,7 +21,6 @@ pub fn parse_device_auth(header_value: &str) -> Option { return None; } Some(DeviceAuthPayload { - scheme: AuthScheme::Gem, device_id: parts[0].to_string(), timestamp: parts[1].to_string(), wallet_id: if parts[2].is_empty() { None } else { Some(parts[2].to_string()) }, @@ -37,11 +29,6 @@ pub fn parse_device_auth(header_value: &str) -> Option { }) } -// TODO: remove base64 fallback once all clients use hex signatures -pub fn decode_signature(value: &str) -> Option> { - hex::decode(value).ok().or_else(|| decode_base64(value).ok()) -} - pub fn verify_device_signature(public_key_hex: &str, message: &str, signature: &[u8]) -> bool { let Ok(pk_bytes) = hex::decode(public_key_hex) else { return false; diff --git a/crates/gem_auth/src/lib.rs b/crates/gem_auth/src/lib.rs index 20b59fecb..9c4dfc3d0 100644 --- a/crates/gem_auth/src/lib.rs +++ b/crates/gem_auth/src/lib.rs @@ -7,7 +7,7 @@ mod signature; #[cfg(feature = "client")] pub use client::AuthClient; -pub use device_signature::{AuthScheme, DeviceAuthPayload, GEM_AUTH_SCHEME, decode_signature, parse_device_auth, verify_device_signature}; +pub use device_signature::{DeviceAuthPayload, parse_device_auth, verify_device_signature}; #[cfg(feature = "client")] pub use jwt::{JwtClaims, create_device_token, verify_device_token}; pub use signature::{AuthMessageData, create_auth_hash, verify_auth_signature}; diff --git a/docs/DEVICE_AUTHENTICATION.md b/docs/DEVICE_AUTHENTICATION.md index 23db87e18..997552eaf 100644 --- a/docs/DEVICE_AUTHENTICATION.md +++ b/docs/DEVICE_AUTHENTICATION.md @@ -2,9 +2,7 @@ ## Overview -All `/v2/devices/*` endpoints require Ed25519 request signing. New clients should use the Gem `Authorization` header. Individual `x-device-*` headers remain supported for existing clients and should be treated as legacy compatibility. - -## Gem Authorization Header +All `/v2/devices/*` endpoints require Ed25519 request signing with a single Gem `Authorization` header. Legacy individual `x-device-*` and `x-wallet-id` headers are no longer accepted. ``` Authorization: Gem base64(....) @@ -32,51 +30,22 @@ Examples: 1706000000000.GET./v2/devices/assets.multicoin_0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb.e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ``` -## Legacy Individual Headers - -The server still accepts individual headers for compatibility: - -- `x-device-id`: 64-character hex Ed25519 public key -- `x-device-signature`: Ed25519 signature, hex or base64 -- `x-device-timestamp`: Unix timestamp in milliseconds -- `x-device-body-hash`: 64-character hex SHA256 hash of the request body -- `x-wallet-id`: wallet identifier for wallet-scoped endpoints - -**Signed message:** - -``` -v1.{timestamp}.{method}.{path}.{bodyHash} -``` - -The legacy signed message does not include `walletId`; new clients should not add new dependencies on this format. - ## Request Examples -### Gem Wallet-scoped Endpoint +### Wallet-scoped Endpoint ```http GET /v2/devices/assets?from_timestamp=1234567890 Authorization: Gem base64(abc123...def456.1706000000000.multicoin_0x742d...f0bEb.e3b0c44...b855.aabb11...) ``` -### Gem Non-wallet Endpoint +### Non-wallet Endpoint ```http GET /v2/devices Authorization: Gem base64(abc123...def456.1706000000000..e3b0c44...b855.aabb11...) ``` -### Legacy Individual Headers - -```http -GET /v2/devices/assets?from_timestamp=1234567890 -x-device-id: abc123...def456 -x-wallet-id: multicoin_0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb -x-device-signature: aabb11... -x-device-timestamp: 1706000000000 -x-device-body-hash: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -``` - ## Implementation - Request signature verification: [`apps/api/src/devices/signature.rs`](../apps/api/src/devices/signature.rs) diff --git a/docs/WALLET_AUTHENTICATION.md b/docs/WALLET_AUTHENTICATION.md index d45e8f840..c48bc6345 100644 --- a/docs/WALLET_AUTHENTICATION.md +++ b/docs/WALLET_AUTHENTICATION.md @@ -29,12 +29,7 @@ Wallet authentication endpoints require proof of wallet ownership via blockchain } ``` -Wallet-authenticated requests are still device-authenticated requests. Use the Gem `Authorization` header for device authentication where possible; existing clients may still use the legacy individual headers documented in [Device Authentication](DEVICE_AUTHENTICATION.md). - -For the current `WalletSigned` guard, include: -- `x-device-body-hash`: SHA256 hash of request body (hex) - -This binds the wallet-signed JSON body to the request body read by the guard. Moving this check fully into the Gem `Authorization` payload should be done with the legacy-removal PR. +Wallet-authenticated requests are still device-authenticated requests. The request body hash is included in the signed `Authorization: Gem ...` device-auth payload, which binds the wallet-signed JSON body to the request. ## Nonce Request @@ -77,7 +72,6 @@ GET /v2/devices/auth/nonce POST https://api.gemwallet.com/v2/devices/rewards/referrals/create Content-Type: application/json Authorization: Gem base64(....) -x-device-body-hash: a1b2c3d4e5f6... { "auth": {