mirror of
https://github.com/Blah-IM/blahrs.git
synced 2025-05-01 00:31:09 +00:00
refactor(types,register): introduce IdUrl
and related types into submod
- `IdUrl` does basic validation for identity URL. Server could enforce additional restrictions on their own need. - single-label doamins are now rejected by default. - More tests are added for `IdUrl` validation.
This commit is contained in:
parent
25936cc4f7
commit
fac380fe55
7 changed files with 262 additions and 81 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -282,6 +282,7 @@ dependencies = [
|
||||||
"serde_jcs",
|
"serde_jcs",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
|
"thiserror",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ serde = { version = "1", features = ["derive"] }
|
||||||
serde_jcs = "0.1"
|
serde_jcs = "0.1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_with = "3.9.0"
|
serde_with = "3.9.0"
|
||||||
|
thiserror = "1.0.63"
|
||||||
url = { version = "2", features = ["serde"] }
|
url = { version = "2", features = ["serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
184
blah-types/src/identity.rs
Normal file
184
blah-types/src/identity.rs
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
use core::fmt;
|
||||||
|
use std::ops;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
use url::{Host, Position, Url};
|
||||||
|
|
||||||
|
use crate::{PubKey, Signed};
|
||||||
|
|
||||||
|
/// User identity description structure.
|
||||||
|
// TODO: Revise and shrink duplicates (pubkey fields).
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct UserIdentityDesc {
|
||||||
|
/// User primary identity key, only for signing action keys.
|
||||||
|
pub id_key: PubKey,
|
||||||
|
/// User action subkeys, signed by the identity key.
|
||||||
|
pub act_keys: Vec<Signed<UserActKeyDesc>>,
|
||||||
|
/// User profile, signed by any valid action key.
|
||||||
|
pub profile: Signed<UserProfile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserIdentityDesc {
|
||||||
|
pub const WELL_KNOWN_PATH: &str = "/.well-known/blah/identity.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: JWS or alike?
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "typ", rename = "user_act_key")]
|
||||||
|
pub struct UserActKeyDesc {
|
||||||
|
pub act_key: PubKey,
|
||||||
|
pub expire_time: u64,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "typ", rename = "user_profile")]
|
||||||
|
pub struct UserProfile {
|
||||||
|
pub preferred_chat_server_urls: Vec<Url>,
|
||||||
|
pub id_urls: Vec<IdUrl>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
|
#[serde(try_from = "Url")]
|
||||||
|
pub struct IdUrl(Url);
|
||||||
|
|
||||||
|
impl fmt::Display for IdUrl {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for IdUrl {
|
||||||
|
fn serialize<S>(&self, ser: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
self.0.serialize(ser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdUrl {
|
||||||
|
/// Max domain length is limited by TLS certificate CommonName `ub-common-name`,
|
||||||
|
/// which is 64. Adding the schema and port, it should still be below 80.
|
||||||
|
///
|
||||||
|
/// Ref: <https://www.rfc-editor.org/rfc/rfc3280>
|
||||||
|
pub const MAX_LEN: usize = 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ops::Deref for IdUrl {
|
||||||
|
type Target = Url;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Error)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub enum IdUrlError {
|
||||||
|
#[error(transparent)]
|
||||||
|
ParseUrl(#[from] url::ParseError),
|
||||||
|
#[error("id-URL too long")]
|
||||||
|
TooLong,
|
||||||
|
#[error("id-URL scheme must be https or http")]
|
||||||
|
InvalidScheme,
|
||||||
|
#[error("id-URL must not have username or password")]
|
||||||
|
HasAuth,
|
||||||
|
#[error("id-URL host must not be an IP")]
|
||||||
|
InvalidHost,
|
||||||
|
#[error("id-URL must has root path `/` without query or fragment")]
|
||||||
|
InvalidPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Url> for IdUrl {
|
||||||
|
type Error = IdUrlError;
|
||||||
|
|
||||||
|
/// Validate identity URL.
|
||||||
|
///
|
||||||
|
/// We only accept simple HTTPS (and HTTP, if configured) domains. It must not be an IP host
|
||||||
|
/// and must not have other parts like username, query, and etc.
|
||||||
|
fn try_from(url: Url) -> Result<Self, Self::Error> {
|
||||||
|
// ```text
|
||||||
|
// url =
|
||||||
|
// scheme ":"
|
||||||
|
// [ "//" [ username [ ":" password ]? "@" ]? host [ ":" port ]? ]?
|
||||||
|
// path [ "?" query ]? [ "#" fragment ]?
|
||||||
|
// ```
|
||||||
|
if url.as_str().len() > Self::MAX_LEN {
|
||||||
|
return Err(IdUrlError::TooLong);
|
||||||
|
}
|
||||||
|
if !["https", "http"].contains(&url.scheme()) {
|
||||||
|
return Err(IdUrlError::InvalidScheme);
|
||||||
|
}
|
||||||
|
if &url[Position::AfterScheme..Position::BeforeHost] != "://" {
|
||||||
|
return Err(IdUrlError::HasAuth);
|
||||||
|
}
|
||||||
|
if !url
|
||||||
|
.host()
|
||||||
|
.is_some_and(|host| matches!(host, Host::Domain(_)))
|
||||||
|
{
|
||||||
|
return Err(IdUrlError::InvalidHost);
|
||||||
|
}
|
||||||
|
if &url[Position::BeforePath..] != "/" {
|
||||||
|
return Err(IdUrlError::InvalidPath);
|
||||||
|
}
|
||||||
|
Ok(Self(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for IdUrl {
|
||||||
|
type Err = IdUrlError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
Url::parse(s)?.try_into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_id_url() {
|
||||||
|
let parse = <str>::parse::<IdUrl>;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
parse("not-a-url").unwrap_err(),
|
||||||
|
IdUrlError::ParseUrl(_)
|
||||||
|
));
|
||||||
|
|
||||||
|
macro_rules! check_err {
|
||||||
|
($($s:expr, $err:expr;)*) => {
|
||||||
|
$(
|
||||||
|
assert_eq!(parse(&$s), Err($err));
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
check_err! {
|
||||||
|
format!("https://{}.com/", "a".repeat(IdUrl::MAX_LEN)), IdUrlError::TooLong;
|
||||||
|
"file:///etc/passwd", IdUrlError::InvalidScheme;
|
||||||
|
|
||||||
|
"https://user@example.com/", IdUrlError::HasAuth;
|
||||||
|
"https://user:passwd@example.com/", IdUrlError::HasAuth;
|
||||||
|
"https://:passwd@example.com/", IdUrlError::HasAuth;
|
||||||
|
|
||||||
|
"https://[::1]/", IdUrlError::InvalidHost;
|
||||||
|
"https://127.0.0.1/", IdUrlError::InvalidHost;
|
||||||
|
|
||||||
|
"https://example.com/path", IdUrlError::InvalidPath;
|
||||||
|
"https://example.com//", IdUrlError::InvalidPath;
|
||||||
|
"https://example.com/?query", IdUrlError::InvalidPath;
|
||||||
|
"https://example.com/#hash", IdUrlError::InvalidPath;
|
||||||
|
"https://example.com?query", IdUrlError::InvalidPath;
|
||||||
|
"https://example.com#hash", IdUrlError::InvalidPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto normalized.
|
||||||
|
let expect = parse("https://example.com/").unwrap();
|
||||||
|
assert_eq!(parse("https://example.com").unwrap(), expect);
|
||||||
|
assert_eq!(parse("https://:@example.com").unwrap(), expect);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ use ed25519_dalek::{
|
||||||
Signature, SignatureError, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH,
|
Signature, SignatureError, Signer, SigningKey, VerifyingKey, PUBLIC_KEY_LENGTH,
|
||||||
SIGNATURE_LENGTH,
|
SIGNATURE_LENGTH,
|
||||||
};
|
};
|
||||||
|
use identity::IdUrl;
|
||||||
use rand_core::RngCore;
|
use rand_core::RngCore;
|
||||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use serde_with::{serde_as, DisplayFromStr};
|
use serde_with::{serde_as, DisplayFromStr};
|
||||||
|
@ -15,42 +16,13 @@ use url::Url;
|
||||||
// Re-export of public dependencies.
|
// Re-export of public dependencies.
|
||||||
pub use bitflags;
|
pub use bitflags;
|
||||||
pub use ed25519_dalek;
|
pub use ed25519_dalek;
|
||||||
|
pub use url;
|
||||||
|
|
||||||
|
pub mod identity;
|
||||||
|
|
||||||
pub const X_BLAH_NONCE: &str = "x-blah-nonce";
|
pub const X_BLAH_NONCE: &str = "x-blah-nonce";
|
||||||
pub const X_BLAH_DIFFICULTY: &str = "x-blah-difficulty";
|
pub const X_BLAH_DIFFICULTY: &str = "x-blah-difficulty";
|
||||||
|
|
||||||
/// User identity description structure.
|
|
||||||
// TODO: Revise and shrink duplicates (pubkey fields).
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct UserIdentityDesc {
|
|
||||||
/// User primary identity key, only for signing action keys.
|
|
||||||
pub id_key: PubKey,
|
|
||||||
/// User action subkeys, signed by the identity key.
|
|
||||||
pub act_keys: Vec<Signed<UserActKeyDesc>>,
|
|
||||||
/// User profile, signed by any valid action key.
|
|
||||||
pub profile: Signed<UserProfile>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserIdentityDesc {
|
|
||||||
pub const WELL_KNOWN_PATH: &str = "/.well-known/blah/identity.json";
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: JWS or alike?
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "typ", rename = "user_act_key")]
|
|
||||||
pub struct UserActKeyDesc {
|
|
||||||
pub act_key: PubKey,
|
|
||||||
pub expire_time: u64,
|
|
||||||
pub comment: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
#[serde(tag = "typ", rename = "user_profile")]
|
|
||||||
pub struct UserProfile {
|
|
||||||
pub preferred_chat_server_urls: Vec<Url>,
|
|
||||||
pub id_urls: Vec<Url>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An opaque server-specific ID for rooms, messages, and etc.
|
/// An opaque server-specific ID for rooms, messages, and etc.
|
||||||
/// It's currently serialized as a string for JavaScript's convenience.
|
/// It's currently serialized as a string for JavaScript's convenience.
|
||||||
#[serde_as]
|
#[serde_as]
|
||||||
|
@ -177,7 +149,7 @@ impl<T: Serialize> Signed<T> {
|
||||||
#[serde(tag = "typ", rename = "user_register")]
|
#[serde(tag = "typ", rename = "user_register")]
|
||||||
pub struct UserRegisterPayload {
|
pub struct UserRegisterPayload {
|
||||||
pub server_url: Url,
|
pub server_url: Url,
|
||||||
pub id_url: Url,
|
pub id_url: IdUrl,
|
||||||
pub id_key: PubKey,
|
pub id_key: PubKey,
|
||||||
pub challenge_nonce: u32,
|
pub challenge_nonce: u32,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,10 @@ use std::path::{Path, PathBuf};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use anyhow::{ensure, Context, Result};
|
use anyhow::{ensure, Context, Result};
|
||||||
|
use blah_types::identity::{IdUrl, UserActKeyDesc, UserIdentityDesc, UserProfile};
|
||||||
use blah_types::{
|
use blah_types::{
|
||||||
bitflags, get_timestamp, ChatPayload, CreateGroup, CreateRoomPayload, Id, PubKey, RichText,
|
bitflags, get_timestamp, ChatPayload, CreateGroup, CreateRoomPayload, Id, PubKey, RichText,
|
||||||
RoomAttrs, ServerPermission, Signed, UserActKeyDesc, UserIdentityDesc, UserProfile,
|
RoomAttrs, ServerPermission, Signed,
|
||||||
};
|
};
|
||||||
use ed25519_dalek::pkcs8::spki::der::pem::LineEnding;
|
use ed25519_dalek::pkcs8::spki::der::pem::LineEnding;
|
||||||
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey};
|
use ed25519_dalek::pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey};
|
||||||
|
@ -74,7 +75,7 @@ enum IdCommand {
|
||||||
/// The identity description file should be available at
|
/// The identity description file should be available at
|
||||||
/// `<id_url>/.well-known/blah/identity.json`.
|
/// `<id_url>/.well-known/blah/identity.json`.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
id_url: Url,
|
id_url: IdUrl,
|
||||||
},
|
},
|
||||||
/// Add an action subkey to an existing identity description.
|
/// Add an action subkey to an existing identity description.
|
||||||
AddActKey {
|
AddActKey {
|
||||||
|
|
|
@ -3,9 +3,9 @@ use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use anyhow::{anyhow, ensure, Context};
|
use anyhow::{anyhow, ensure, Context};
|
||||||
use axum::http::{HeaderMap, HeaderName, StatusCode};
|
use axum::http::{HeaderMap, HeaderName, StatusCode};
|
||||||
|
use blah_types::identity::{IdUrl, UserIdentityDesc};
|
||||||
use blah_types::{
|
use blah_types::{
|
||||||
get_timestamp, PubKey, Signed, UserIdentityDesc, UserRegisterPayload, X_BLAH_DIFFICULTY,
|
get_timestamp, PubKey, Signed, UserRegisterPayload, X_BLAH_DIFFICULTY, X_BLAH_NONCE,
|
||||||
X_BLAH_NONCE,
|
|
||||||
};
|
};
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -14,17 +14,11 @@ use rand::RngCore;
|
||||||
use rusqlite::{named_params, params, OptionalExtension};
|
use rusqlite::{named_params, params, OptionalExtension};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use url::{Host, Url};
|
|
||||||
|
|
||||||
use crate::{ApiError, AppState};
|
use crate::{ApiError, AppState};
|
||||||
|
|
||||||
const USER_AGENT: &str = concat!("blahd/", env!("CARGO_PKG_VERSION"));
|
const USER_AGENT: &str = concat!("blahd/", env!("CARGO_PKG_VERSION"));
|
||||||
|
|
||||||
/// Max domain length is limited by TLS certificate CommonName `ub-common-name`,
|
|
||||||
/// which is 64. Adding the schema and port, it should still be below 80.
|
|
||||||
/// Ref: https://www.rfc-editor.org/rfc/rfc3280
|
|
||||||
const MAX_ID_URL_LEN: usize = 80;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -38,6 +32,7 @@ pub struct Config {
|
||||||
|
|
||||||
pub unsafe_allow_id_url_http: bool,
|
pub unsafe_allow_id_url_http: bool,
|
||||||
pub unsafe_allow_id_url_custom_port: bool,
|
pub unsafe_allow_id_url_custom_port: bool,
|
||||||
|
pub unsafe_allow_id_url_single_label: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
@ -53,10 +48,32 @@ impl Default for Config {
|
||||||
|
|
||||||
unsafe_allow_id_url_http: false,
|
unsafe_allow_id_url_http: false,
|
||||||
unsafe_allow_id_url_custom_port: false,
|
unsafe_allow_id_url_custom_port: false,
|
||||||
|
unsafe_allow_id_url_single_label: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
/// Check if the Identity URL is valid under the config.
|
||||||
|
/// This only does additional checking besides rules of [`IdUrl`].
|
||||||
|
fn validate_id_url(&self, url: &IdUrl) -> Result<(), &'static str> {
|
||||||
|
if !self.unsafe_allow_id_url_http && url.scheme() == "http" {
|
||||||
|
return Err("http id_url is not permitted for this server");
|
||||||
|
}
|
||||||
|
if !self.unsafe_allow_id_url_custom_port && url.port().is_some() {
|
||||||
|
return Err("id_url with custom port is not permitted for this server");
|
||||||
|
}
|
||||||
|
let host = url.host_str().expect("checked by IdUrl");
|
||||||
|
if host.starts_with('.') || host.ends_with('.') {
|
||||||
|
return Err("unpermitted id_url with starting or trailing dot");
|
||||||
|
}
|
||||||
|
if !self.unsafe_allow_id_url_single_label && !host.contains('.') {
|
||||||
|
return Err("single-label id_url is not permitted for this server");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
nonces: Mutex<Nonces>,
|
nonces: Mutex<Nonces>,
|
||||||
|
@ -131,31 +148,6 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the Identity URL is valid under the config.
|
|
||||||
///
|
|
||||||
/// We only accept simple HTTPS (and HTTP, if configured) domains. It must not be an IP host and
|
|
||||||
/// must not have other parts like username, query, and etc.
|
|
||||||
///
|
|
||||||
/// Ref: https://docs.rs/url/2.5.2/url/enum.Position.html
|
|
||||||
/// ```text
|
|
||||||
/// url =
|
|
||||||
/// scheme ":"
|
|
||||||
/// [ "//" [ username [ ":" password ]? "@" ]? host [ ":" port ]? ]?
|
|
||||||
/// path [ "?" query ]? [ "#" fragment ]?
|
|
||||||
/// ```
|
|
||||||
fn is_id_url_valid(config: &Config, url: &Url) -> bool {
|
|
||||||
use url::Position;
|
|
||||||
|
|
||||||
url.as_str().len() <= MAX_ID_URL_LEN
|
|
||||||
&& (url.scheme() == "https" || config.unsafe_allow_id_url_http && url.scheme() == "http")
|
|
||||||
&& &url[Position::AfterScheme..Position::BeforeHost] == "://"
|
|
||||||
&& url
|
|
||||||
.host()
|
|
||||||
.is_some_and(|host| matches!(host, Host::Domain(_)))
|
|
||||||
&& (config.unsafe_allow_id_url_custom_port || url.port().is_none())
|
|
||||||
&& &url[Position::BeforePath..] == "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn user_register(
|
pub async fn user_register(
|
||||||
st: &AppState,
|
st: &AppState,
|
||||||
msg: Signed<UserRegisterPayload>,
|
msg: Signed<UserRegisterPayload>,
|
||||||
|
@ -178,11 +170,11 @@ pub async fn user_register(
|
||||||
"unexpected server url in payload",
|
"unexpected server url in payload",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !is_id_url_valid(&st.config.register, ®.id_url) {
|
if let Err(err) = st.config.register.validate_id_url(®.id_url) {
|
||||||
return Err(error_response!(
|
return Err(error_response!(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
"invalid_id_url",
|
"invalid_id_url",
|
||||||
"invalid identity URL",
|
"{err}",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !st.register.nonce().contains(®.challenge_nonce) {
|
if !st.register.nonce().contains(®.challenge_nonce) {
|
||||||
|
@ -247,7 +239,7 @@ pub async fn user_register(
|
||||||
return Err(error_response!(
|
return Err(error_response!(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
"fetch_id_description",
|
"fetch_id_description",
|
||||||
"failed to fetch identity description from domain {}: {}",
|
"failed to fetch identity description from {}: {}",
|
||||||
reg.id_url,
|
reg.id_url,
|
||||||
err,
|
err,
|
||||||
))
|
))
|
||||||
|
@ -323,7 +315,7 @@ pub async fn user_register(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_id_desc(
|
fn validate_id_desc(
|
||||||
id_url: &Url,
|
id_url: &IdUrl,
|
||||||
id_key: &PubKey,
|
id_key: &PubKey,
|
||||||
id_desc: &UserIdentityDesc,
|
id_desc: &UserIdentityDesc,
|
||||||
now: u64,
|
now: u64,
|
||||||
|
@ -368,3 +360,30 @@ fn validate_id_desc(
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reject_unpermitted_id_url() {
|
||||||
|
let mut conf = Config::default();
|
||||||
|
let http_url = "http://example.com".parse().unwrap();
|
||||||
|
conf.validate_id_url(&http_url).unwrap_err();
|
||||||
|
conf.unsafe_allow_id_url_http = true;
|
||||||
|
conf.validate_id_url(&http_url).unwrap();
|
||||||
|
|
||||||
|
let custom_port = "https://example.com:8080".parse().unwrap();
|
||||||
|
conf.validate_id_url(&custom_port).unwrap_err();
|
||||||
|
conf.unsafe_allow_id_url_custom_port = true;
|
||||||
|
conf.validate_id_url(&custom_port).unwrap();
|
||||||
|
|
||||||
|
let single_label = "https://localhost".parse().unwrap();
|
||||||
|
conf.validate_id_url(&single_label).unwrap_err();
|
||||||
|
conf.unsafe_allow_id_url_single_label = true;
|
||||||
|
conf.validate_id_url(&single_label).unwrap();
|
||||||
|
|
||||||
|
conf.validate_id_url(&"https://.".parse().unwrap())
|
||||||
|
.unwrap_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -9,11 +9,12 @@ use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use axum::http::HeaderMap;
|
use axum::http::HeaderMap;
|
||||||
|
use blah_types::identity::{IdUrl, UserActKeyDesc, UserIdentityDesc, UserProfile};
|
||||||
use blah_types::{
|
use blah_types::{
|
||||||
get_timestamp, AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id,
|
get_timestamp, AuthPayload, ChatPayload, CreateGroup, CreatePeerChat, CreateRoomPayload, Id,
|
||||||
MemberPermission, PubKey, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata,
|
MemberPermission, PubKey, RichText, RoomAdminOp, RoomAdminPayload, RoomAttrs, RoomMetadata,
|
||||||
ServerPermission, Signed, SignedChatMsg, UserActKeyDesc, UserIdentityDesc, UserKey,
|
ServerPermission, Signed, SignedChatMsg, UserKey, UserRegisterPayload, WithMsgId,
|
||||||
UserProfile, UserRegisterPayload, WithMsgId, X_BLAH_DIFFICULTY, X_BLAH_NONCE,
|
X_BLAH_DIFFICULTY, X_BLAH_NONCE,
|
||||||
};
|
};
|
||||||
use blahd::{ApiError, AppState, Database, RoomList, RoomMsgs};
|
use blahd::{ApiError, AppState, Database, RoomList, RoomMsgs};
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
@ -29,7 +30,6 @@ use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
// Register API requires a non-IP hostname.
|
// Register API requires a non-IP hostname.
|
||||||
const LOCALHOST: &str = "localhost";
|
const LOCALHOST: &str = "localhost";
|
||||||
|
@ -48,6 +48,7 @@ difficulty = {REGISTER_DIFFICULTY}
|
||||||
request_timeout_secs = 1
|
request_timeout_secs = 1
|
||||||
unsafe_allow_id_url_http = true
|
unsafe_allow_id_url_http = true
|
||||||
unsafe_allow_id_url_custom_port = true
|
unsafe_allow_id_url_custom_port = true
|
||||||
|
unsafe_allow_id_url_single_label = true
|
||||||
"#
|
"#
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
@ -856,9 +857,9 @@ async fn register(server: Server) {
|
||||||
|
|
||||||
let mut req = UserRegisterPayload {
|
let mut req = UserRegisterPayload {
|
||||||
id_key: CAROL.pubkeys.id_key.clone(),
|
id_key: CAROL.pubkeys.id_key.clone(),
|
||||||
// Fake values.
|
// Invalid values.
|
||||||
server_url: "http://invalid.example.com".parse().unwrap(),
|
server_url: "http://localhost".parse().unwrap(),
|
||||||
id_url: "file:///etc/passwd".parse().unwrap(),
|
id_url: "http://.".parse().unwrap(),
|
||||||
challenge_nonce: challenge_nonce - 1,
|
challenge_nonce: challenge_nonce - 1,
|
||||||
};
|
};
|
||||||
let register = |req: Signed<UserRegisterPayload>| {
|
let register = |req: Signed<UserRegisterPayload>| {
|
||||||
|
@ -897,7 +898,9 @@ async fn register(server: Server) {
|
||||||
|
|
||||||
let listener = TcpListener::bind(format!("{LOCALHOST}:0")).await.unwrap();
|
let listener = TcpListener::bind(format!("{LOCALHOST}:0")).await.unwrap();
|
||||||
let port = listener.local_addr().unwrap().port();
|
let port = listener.local_addr().unwrap().port();
|
||||||
req.id_url = Url::parse(&format!("http://{LOCALHOST}:{port}")).unwrap();
|
req.id_url = format!("http://{LOCALHOST}:{port}")
|
||||||
|
.parse::<IdUrl>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let router = axum::Router::new()
|
let router = axum::Router::new()
|
||||||
.route(
|
.route(
|
||||||
|
@ -962,7 +965,7 @@ async fn register(server: Server) {
|
||||||
(StatusCode::OK, desc.clone())
|
(StatusCode::OK, desc.clone())
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
let sign_profile = |url: Url| {
|
let sign_profile = |url: IdUrl| {
|
||||||
server.sign(
|
server.sign(
|
||||||
&CAROL,
|
&CAROL,
|
||||||
UserProfile {
|
UserProfile {
|
||||||
|
@ -985,7 +988,7 @@ async fn register(server: Server) {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let profile = sign_profile(req.id_url.join("/mismatch").unwrap());
|
let profile = sign_profile("https://localhost".parse().unwrap());
|
||||||
UserIdentityDesc {
|
UserIdentityDesc {
|
||||||
id_key: CAROL.pubkeys.id_key.clone(),
|
id_key: CAROL.pubkeys.id_key.clone(),
|
||||||
act_keys: vec![act_key],
|
act_keys: vec![act_key],
|
||||||
|
|
Loading…
Add table
Reference in a new issue