Implement EIP-4361 authentication

This commit is contained in:
silverpill 2022-02-09 00:09:08 +00:00
parent d3e3c1eb3e
commit e8a29a3af1
8 changed files with 279 additions and 30 deletions

175
Cargo.lock generated
View file

@ -101,7 +101,7 @@ dependencies = [
"http",
"httparse",
"indexmap",
"itoa",
"itoa 0.4.7",
"language-tags",
"lazy_static",
"log",
@ -743,13 +743,14 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-bigint"
version = "0.2.2"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b32a398eb1ccfbe7e4f452bc749c44d38dd732e9a253f19da224c416f00ee7f4"
checksum = "f83bd3bb4314701c568e340cd8cf78c975aa0ca79e03d3f6d1677d5b0c9c0c03"
dependencies = [
"generic-array",
"rand_core 0.6.2",
"subtle",
"zeroize",
]
[[package]]
@ -762,6 +763,16 @@ dependencies = [
"subtle",
]
[[package]]
name = "crypto-mac"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
dependencies = [
"generic-array",
"subtle",
]
[[package]]
name = "deadpool"
version = "0.5.2"
@ -839,12 +850,39 @@ version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56899898ce76aaf4a0f24d914c97ea6ed976d42fec6ad33fcbb0a1103e07b2b0"
[[package]]
name = "ecdsa"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43ee23aa5b4f68c7a092b5c3beb25f50c406adc75e2363634f242f28ab255372"
dependencies = [
"der",
"elliptic-curve",
"hmac 0.11.0",
"signature",
]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "elliptic-curve"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beca177dcb8eb540133e7680baff45e7cc4d93bf22002676cec549f82343721b"
dependencies = [
"crypto-bigint",
"ff",
"generic-array",
"group",
"rand_core 0.6.2",
"subtle",
"zeroize",
]
[[package]]
name = "encoding_rs"
version = "0.8.28"
@ -924,6 +962,16 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
[[package]]
name = "ff"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f"
dependencies = [
"rand_core 0.6.2",
"subtle",
]
[[package]]
name = "fixed-hash"
version = "0.7.0"
@ -931,7 +979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c"
dependencies = [
"byteorder",
"rand 0.8.3",
"rand 0.8.4",
"rustc-hex",
"static_assertions",
]
@ -1152,6 +1200,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "group"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912"
dependencies = [
"ff",
"rand_core 0.6.2",
"subtle",
]
[[package]]
name = "h2"
version = "0.2.7"
@ -1208,7 +1267,17 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deae6d9dbb35ec2c502d62b8f7b1c000a0822c3b0794ba36b3149c0a1c840dff"
dependencies = [
"crypto-mac",
"crypto-mac 0.9.1",
"digest",
]
[[package]]
name = "hmac"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
dependencies = [
"crypto-mac 0.11.1",
"digest",
]
@ -1239,13 +1308,13 @@ dependencies = [
[[package]]
name = "http"
version = "0.2.4"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03"
dependencies = [
"bytes 1.0.1",
"fnv",
"itoa",
"itoa 1.0.1",
]
[[package]]
@ -1285,7 +1354,7 @@ dependencies = [
"http-body",
"httparse",
"httpdate",
"itoa",
"itoa 0.4.7",
"pin-project 1.0.6",
"socket2",
"tokio",
@ -1420,12 +1489,27 @@ version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9"
[[package]]
name = "iri-string"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f0f7638c1e223529f1bfdc48c8b133b9e0b434094d1d28473161ee48b235f78"
dependencies = [
"nom 7.1.0",
]
[[package]]
name = "itoa"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "itoa"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "js-sys"
version = "0.3.51"
@ -1448,6 +1532,18 @@ dependencies = [
"serde_json",
]
[[package]]
name = "k256"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea"
dependencies = [
"cfg-if 1.0.0",
"ecdsa",
"elliptic-curve",
"sha3",
]
[[package]]
name = "keccak"
version = "0.1.0"
@ -1632,6 +1728,12 @@ dependencies = [
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.4.4"
@ -1711,7 +1813,7 @@ dependencies = [
"postgres-types",
"postgres_query",
"postgres_query_macro",
"rand 0.8.3",
"rand 0.8.4",
"refinery",
"regex",
"reqwest",
@ -1723,6 +1825,7 @@ dependencies = [
"serde_yaml",
"serial_test",
"sha2",
"siwe",
"thiserror",
"tokio",
"tokio-postgres",
@ -1788,6 +1891,17 @@ dependencies = [
"version_check 0.9.3",
]
[[package]]
name = "nom"
version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d11e1ef389c76fe5b81bcaf2ea32cf88b62bc494e19f493d0b30e7a930109"
dependencies = [
"memchr",
"minimal-lexical",
"version_check 0.9.3",
]
[[package]]
name = "num-bigint-dig"
version = "0.7.0"
@ -1801,7 +1915,7 @@ dependencies = [
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.3",
"rand 0.8.4",
"smallvec",
"zeroize",
]
@ -2112,7 +2226,7 @@ dependencies = [
"byteorder",
"bytes 0.5.6",
"fallible-iterator",
"hmac",
"hmac 0.9.0",
"md5",
"memchr",
"rand 0.7.3",
@ -2290,9 +2404,9 @@ dependencies = [
[[package]]
name = "rand"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"libc",
"rand_chacha 0.3.0",
@ -2612,7 +2726,7 @@ dependencies = [
"num-traits",
"pkcs1",
"pkcs8",
"rand 0.8.3",
"rand 0.8.4",
"subtle",
"zeroize",
]
@ -2758,7 +2872,7 @@ version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
dependencies = [
"itoa",
"itoa 0.4.7",
"ryu",
"serde",
]
@ -2770,7 +2884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edfa57a7f8d9c1d260a549e7224100f6c43d43f9103e06dd8b4095a9b2b43ce9"
dependencies = [
"form_urlencoded",
"itoa",
"itoa 0.4.7",
"ryu",
"serde",
]
@ -2862,12 +2976,37 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4"
dependencies = [
"digest",
"rand_core 0.6.2",
]
[[package]]
name = "siphasher"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbce6d4507c7e4a3962091436e56e95290cb71fa302d0d270e32130b75fbff27"
[[package]]
name = "siwe"
version = "0.2.0"
source = "git+https://github.com/silverpill/siwe-rs?branch=edition2018#9328228930af06f96ed8e0fe548bf498e959302c"
dependencies = [
"chrono",
"hex",
"http",
"iri-string",
"k256",
"rand 0.8.4",
"sha3",
"thiserror",
]
[[package]]
name = "slab"
version = "0.4.2"
@ -3042,7 +3181,7 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
"cfg-if 1.0.0",
"libc",
"rand 0.8.3",
"rand 0.8.4",
"redox_syscall",
"remove_dir_all",
"winapi 0.3.9",

View file

@ -44,7 +44,7 @@ num_cpus = "1.13.0"
# Used for working with regular expressions
regex = "1.5.4"
# Used to generate random numbers
rand = "0.8.3"
rand = "0.8.4"
# Used for managing database migrations
refinery = { version = "0.4.0", features = ["tokio-postgres"] }
# Used for making async HTTP requests
@ -64,6 +64,8 @@ serde_json = "1.0"
serde_yaml = "0.8.17"
# Used to calculate SHA2 hashes
sha2 = "0.9.5"
# Used to verify EIP-4361 signatures
siwe = { git = "https://github.com/silverpill/siwe-rs", branch = "edition2018" }
# Used for creating error types
thiserror = "1.0.24"
# Async runtime ( required for #[tokio::main] )

View file

@ -19,18 +19,28 @@ paths:
enum:
- password
- ethereum
- eip4361
username:
description: User name (required if grant type is "password").
type: string
message:
description: EIP-4361 message (required if grant type is "eip4361").
type: string
example: "example.com wants you to sign in with your Ethereum account:"
signature:
description: EIP-4361 signature (required if grant type is "eip4361").
type: string
example: 0x905...
wallet_address:
description: Ethereum wallet address (required if grant type is "ethereum").
type: string
example: null
password:
description: Password (required if grant type is "password" or "ethereum").
type: string
example: null
required:
- grant_type
- password
responses:
200:
description: Successful operation

74
src/ethereum/eip4361.rs Normal file
View file

@ -0,0 +1,74 @@
/// Sign-In with Ethereum https://eips.ethereum.org/EIPS/eip-4361
use hex::FromHex;
use siwe::Message;
use web3::types::H160;
use crate::errors::ValidationError;
/// Verifies EIP-4361 signature and returns wallet address
pub fn verify_eip4361_signature(
message: &str,
signature: &str,
instance_host: &str,
login_message: &str,
) -> Result<String, ValidationError> {
let message: Message = message.parse()
.map_err(|_| ValidationError("invalid EIP4361 message"))?;
let signature_bytes = <[u8; 65]>::from_hex(signature.trim_start_matches("0x"))
.map_err(|_| ValidationError("invalid signature string"))?;
message.verify(signature_bytes)
.map_err(|_| ValidationError("invalid signature"))?;
if message.domain != instance_host {
return Err(ValidationError("domain doesn't match instance host"));
};
let statement = message.statement
.ok_or(ValidationError("statement is missing"))?;
if statement != login_message {
return Err(ValidationError("statement doesn't match login message"));
};
if message.not_before.is_some() || message.expiration_time.is_some() {
return Err(ValidationError("message shouldn't have expiration time"));
};
// Return wallet address in lower case
let wallet_address = format!("{:#x}", H160(message.address));
Ok(wallet_address)
}
#[cfg(test)]
mod tests {
use super::*;
const INSTANCE_HOST: &str = "example.com";
const LOGIN_MESSAGE: &str = "test";
#[test]
fn test_verify_eip4361_signature() {
let message = "example.com wants you to sign in with your Ethereum account:
0x70997970C51812dc3A010C7d01b50e0d17dc79C8
test
URI: https://example.com
Version: 1
Chain ID: 1
Nonce: 3cb7760eac2f
Issued At: 2022-02-14T22:27:35.500Z";
let signature = "0x9059c9a69c31e87d887262a574abcc33f320d5b778bea8a35c6fbdea94a17e9652b99f7cdd146ed67fa8e4bb02462774b958a129c421fe8d743a43bf67dcbcd61c";
let wallet_address = verify_eip4361_signature(
message, signature,
INSTANCE_HOST, LOGIN_MESSAGE,
).unwrap();
assert_eq!(wallet_address, "0x70997970c51812dc3a010c7d01b50e0d17dc79c8");
}
#[test]
fn test_verify_eip4361_signature_invalid() {
let message = "abc";
let signature = "xyz";
let error = verify_eip4361_signature(
message, signature,
INSTANCE_HOST, LOGIN_MESSAGE,
).unwrap_err();
assert_eq!(error.to_string(), "invalid EIP4361 message");
}
}

View file

@ -1,5 +1,6 @@
mod api;
pub mod contracts;
pub mod eip4361;
mod errors;
pub mod gate;
pub mod nft;

View file

@ -5,7 +5,11 @@ pub struct TokenRequest {
pub grant_type: String,
pub username: Option<String>,
pub wallet_address: Option<String>,
pub password: String,
// Required only with "password" and "ethereum" grant types
pub password: Option<String>,
// EIP4361 message and signature
pub message: Option<String>,
pub signature: Option<String>,
}
/// https://docs.joinmastodon.org/entities/token/

View file

@ -1,8 +1,10 @@
use actix_web::{post, web, HttpResponse, Scope as ActixScope};
use chrono::{Duration, Utc};
use crate::config::Config;
use crate::database::{Pool, get_database_client};
use crate::errors::{HttpError, ValidationError};
use crate::ethereum::eip4361::verify_eip4361_signature;
use crate::models::oauth::queries::save_oauth_token;
use crate::models::users::queries::{
get_user_by_name,
@ -19,6 +21,7 @@ const ACCESS_TOKEN_EXPIRES_IN: i64 = 86400 * 7;
/// https://oauth.net/2/grant-types/password/
#[post("/token")]
async fn token_view(
config: web::Data<Config>,
db_pool: web::Data<Pool>,
request_data: web::Json<TokenRequest>,
) -> Result<HttpResponse, HttpError> {
@ -35,18 +38,34 @@ async fn token_view(
validate_wallet_address(wallet_address)?;
get_user_by_wallet_address(db_client, wallet_address).await?
},
"eip4361" => {
let message = request_data.message.as_ref()
.ok_or(ValidationError("message is required"))?;
let signature = request_data.signature.as_ref()
.ok_or(ValidationError("signature is required"))?;
let wallet_address = verify_eip4361_signature(
&message,
&signature,
&config.instance().host(),
&config.login_message,
)?;
get_user_by_wallet_address(db_client, &wallet_address).await?
},
_ => {
return Err(ValidationError("unsupported grant type").into());
},
};
if request_data.grant_type == "password" || request_data.grant_type == "ethereum" {
let password = request_data.password.as_ref()
.ok_or(ValidationError("password is required"))?;
let password_correct = verify_password(
&user.password_hash,
&request_data.password,
&password,
).map_err(|_| HttpError::InternalError)?;
if !password_correct {
// Invalid signature/password
return Err(ValidationError("incorrect password").into());
}
};
};
let access_token = generate_access_token();
let created_at = Utc::now();
let expires_at = created_at + Duration::seconds(ACCESS_TOKEN_EXPIRES_IN);

View file

@ -35,7 +35,7 @@ pub async fn get_user_by_oauth_token(
JOIN actor_profile ON user_account.id = actor_profile.id
WHERE
oauth_token.token = $1
AND oauth_token.expires_at > now()
AND oauth_token.expires_at > CURRENT_TIMESTAMP
",
&[&access_token],
).await?;