Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: using relay public key from environment variable #241

Merged
merged 5 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ PUBLIC_URL=https://echo.walletconnect.com
DATABASE_URL=postgres://user:pass@host:port/database
DISABLE_HEADER=false

# Public key can be obtained from https://relay.walletconnect.com/public-key
RELAY_PUBLIC_KEY=

# Should Echo Server validate messages it recieves are from the Relay when attempting to send a push notification
VALIDATE_SIGNATURES=true

Expand Down
3 changes: 3 additions & 0 deletions .env.multi-tenant-example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ PUBLIC_URL=http://localhost:3000
DATABASE_URL=postgres://user:pass@host:port/database
LOG_LEVEL=debug,echo-server=debug

# Public key can be obtained from https://relay.walletconnect.com/public-key
RELAY_PUBLIC_KEY=

# Don't validate signatures - allows for users to send push notifications from
# HTTP clients e.g. curl, insomnia, postman, etc
VALIDATE_SIGNATURES=false
Expand Down
3 changes: 3 additions & 0 deletions .env.single-tenant-example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ PUBLIC_URL=http://localhost:3000
DATABASE_URL=postgres://user:pass@host:port/database
LOG_LEVEL=debug,echo-server=debug

# Public key can be obtained from https://relay.walletconnect.com/public-key
RELAY_PUBLIC_KEY=

# Don't validate signatures - allows for users to send push notifications from
# HTTP clients e.g. curl, insomnia, postman, etc
VALIDATE_SIGNATURES=false
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ jobs:
TF_VAR_cloud_api_key: ${{ secrets.CLOUD_API_KEY }}
TF_VAR_jwt_secret: ${{ secrets.JWT_SECRET }}
TF_VAR_image_version: ${{ inputs.image_tag }}
TF_VAR_relay_public_key: ${{ secrets.RELAY_PUBLIC_KEY }}
with:
environment: "staging"

Expand Down Expand Up @@ -156,6 +157,7 @@ jobs:
TF_VAR_cloud_api_key: ${{ secrets.CLOUD_API_KEY }}
TF_VAR_jwt_secret: ${{ secrets.JWT_SECRET }}
TF_VAR_image_version: ${{ inputs.image_tag }}
TF_VAR_relay_public_key: ${{ secrets.RELAY_PUBLIC_KEY }}
with:
environment: "prod"

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ jobs:
RUSTC_WRAPPER: sccache
SCCACHE_CACHE_SIZE: 1G
SCCACHE_DIR: ${{ matrix.sccache-path }}
# Unit test environment variables dependencies
DATABASE_URL: postgres://postgres:root@localhost:5432/postgres
TENANT_DATABASE_URL: postgres://postgres:root@localhost:5433/postgres
RELAY_PUBLIC_KEY: ${{ secrets.RELAY_PUBLIC_KEY }}
steps:
# Checkout code
- name: "Git checkout"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci_terraform.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ jobs:
TF_VAR_grafana_endpoint: ${{ steps.grafana-get-details.outputs.endpoint }}
TF_VAR_cloud_api_key: ${{ secrets.CLOUD_API_KEY }}
TF_VAR_jwt_secret: ${{ secrets.JWT_SECRET }}
TF_VAR_relay_public_key: ${{ secrets.RELAY_PUBLIC_KEY }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
environment: staging
Expand Down
17 changes: 8 additions & 9 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ pub struct Config {
pub log_level_otel: String,
#[serde(default = "default_disable_header")]
pub disable_header: bool,
#[serde(default = "default_relay_url")]
pub relay_url: String,
pub relay_public_key: String,
#[serde(default = "default_validate_signatures")]
pub validate_signatures: bool,
pub database_url: String,
Expand Down Expand Up @@ -64,7 +63,6 @@ pub struct Config {
pub fcm_api_key: Option<String>,

// Multi-tenancy
#[cfg(feature = "multitenant")]
pub tenant_database_url: String,
#[cfg(feature = "multitenant")]
pub jwt_secret: String,
Expand Down Expand Up @@ -111,6 +109,13 @@ impl Config {
Err(e) => Err(e),
}?;

// Empty Relay public key is not allowed
if self.relay_public_key.is_empty() {
return Err(InvalidConfiguration(
"`RELAY_PUBLIC_KEY` cannot be empty".to_string(),
));
}

Ok(())
}

Expand Down Expand Up @@ -191,12 +196,6 @@ fn default_validate_signatures() -> bool {
true
}

pub const RELAY_URL: &str = "https://relay.walletconnect.com";

fn default_relay_url() -> String {
RELAY_URL.to_string()
}

fn default_is_test() -> bool {
false
}
Expand Down
8 changes: 1 addition & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use {
request_id::{PropagateRequestIdLayer, SetRequestIdLayer},
trace::{DefaultMakeSpan, DefaultOnRequest, DefaultOnResponse, TraceLayer},
},
tracing::{info, log::LevelFilter, warn, Level},
tracing::{info, log::LevelFilter, Level},
};

#[cfg(not(feature = "multitenant"))]
Expand Down Expand Up @@ -155,12 +155,6 @@ pub async fn bootstap(mut shutdown: broadcast::Receiver<()>, config: Config) ->
.collect::<Vec<&str>>()
.join(", ");

// Fetch public key so it's cached for the first 6hrs
let public_key = state.relay_client.public_key().await;
if public_key.is_err() {
warn!("Failed initial fetch of Relay's Public Key, this may prevent webhook validation.")
}

if state.config.telemetry_prometheus_port.is_some() {
state.set_metrics(metrics::Metrics::new(Resource::new(vec![
KeyValue::new("service_name", "echo-server"),
Expand Down
5 changes: 3 additions & 2 deletions src/middleware/validate_signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ where
let s = span!(tracing::Level::DEBUG, "validate_signature");
let _ = s.enter();

let public_key = state.relay_client().public_key().await?;
let state_binding = state.relay_client();
let public_key = state_binding.get_verifying_key();

let (parts, body_raw) = req.into_parts();
let bytes = hyper::body::to_bytes(body_raw)
Expand All @@ -64,7 +65,7 @@ where

match (signature_header, timestamp_header) {
(Some(signature), Some(timestamp))
if signature_is_valid(signature, timestamp, &body, &public_key).await? =>
if signature_is_valid(signature, timestamp, &body, public_key).await? =>
{
let req = Request::<B>::from_parts(parts, bytes.into());
Ok(T::from_request(req, state)
Expand Down
64 changes: 14 additions & 50 deletions src/relay/mod.rs
Original file line number Diff line number Diff line change
@@ -1,62 +1,26 @@
use {
chrono::{DateTime, Duration, Utc},
ed25519_dalek::VerifyingKey,
std::ops::Add,
};

const PUBLIC_KEY_TTL_HOURS: i64 = 6;
use ed25519_dalek::VerifyingKey;

#[derive(Clone)]
pub struct RelayClient {
http_client: reqwest::Client,
base_url: String,
public_key: Option<VerifyingKey>,
public_key_last_fetched: DateTime<Utc>,
public_key: VerifyingKey,
}

impl RelayClient {
pub fn new(base_url: String) -> RelayClient {
RelayClient {
http_client: reqwest::Client::new(),
base_url,
public_key: None,
public_key_last_fetched: DateTime::<Utc>::MIN_UTC,
}
}

/// Fetches the public key with a TTL
pub async fn public_key(&mut self) -> crate::error::Result<VerifyingKey> {
if let Some(public_key) = self.public_key {
// TTL Not exceeded
if self
.public_key_last_fetched
.add(Duration::hours(PUBLIC_KEY_TTL_HOURS))
< Utc::now()
{
return Ok(public_key);
}
}

let public_key = self.fetch_public_key().await?;
self.public_key = Some(public_key);
self.public_key_last_fetched = Utc::now();
Ok(public_key)
pub fn new(string_public_key: String) -> crate::error::Result<RelayClient> {
let verifying_key = Self::string_to_verifying_key(&string_public_key)?;
Ok(RelayClient {
public_key: verifying_key,
})
}

async fn fetch_public_key(&self) -> crate::error::Result<VerifyingKey> {
let response = self
.http_client
.get(self.get_url("public-key"))
.send()
.await?;
let body = response.text().await?;
let key_bytes = hex::decode(body)?;
let public_key =
VerifyingKey::from_bytes(<&[u8; 32]>::try_from(key_bytes.as_slice()).unwrap())?;
Ok(public_key)
pub fn get_verifying_key(&self) -> &VerifyingKey {
&self.public_key
}

fn get_url(&self, path: &str) -> String {
format!("{}/{}", self.base_url, path)
fn string_to_verifying_key(string_key: &str) -> crate::error::Result<VerifyingKey> {
let key_bytes = hex::decode(string_key)?;
Ok(VerifyingKey::from_bytes(
<&[u8; 32]>::try_from(key_bytes.as_slice()).unwrap(),
)?)
}
}
6 changes: 2 additions & 4 deletions src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,6 @@ pub fn new_state(
#[cfg(not(feature = "multitenant"))]
let is_multitenant = false;

let relay_url = config.relay_url.to_string();

#[cfg(feature = "cloud")]
let (cloud_url, cloud_api_key) = (config.cloud_api_url.clone(), config.cloud_api_key.clone());

Expand All @@ -86,15 +84,15 @@ pub fn new_state(
};

Ok(AppState {
config,
config: config.clone(),
build_info: build_info.clone(),
metrics: None,
#[cfg(feature = "analytics")]
analytics: None,
client_store,
notification_store,
tenant_store,
relay_client: RelayClient::new(relay_url),
relay_client: RelayClient::new(config.relay_public_key)?,
#[cfg(feature = "cloud")]
registry_client: RegistryHttpClient::new(cloud_url, cloud_api_key.as_str())?,
#[cfg(feature = "multitenant")]
Expand Down
3 changes: 2 additions & 1 deletion terraform/ecs/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ resource "aws_ecs_task_definition" "app_task_definition" {
{ name = "CLOUD_API_KEY", value = var.cloud_api_key },
{ name = "CLOUD_API_URL", value = var.cloud_api_url },

{ name = "JWT_SECRET", value = var.jwt_secret }
{ name = "JWT_SECRET", value = var.jwt_secret },
{ name = "RELAY_PUBLIC_KEY", value = var.relay_public_key }
],
dependsOn = [
{ containerName = "aws-otel-collector", condition = "START" }
Expand Down
5 changes: 5 additions & 0 deletions terraform/ecs/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,8 @@ variable "jwt_secret" {
type = string
sensitive = true
}

variable "relay_public_key" {
type = string
sensitive = true
}
1 change: 1 addition & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ module "ecs" {
cloud_api_url = "https://registry.walletconnect.com/"

jwt_secret = var.jwt_secret
relay_public_key = var.relay_public_key

autoscaling_max_capacity = local.environment == "prod" ? 4 : 1
autoscaling_min_capacity = local.environment == "prod" ? 2 : 1
Expand Down
5 changes: 5 additions & 0 deletions terraform/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@ variable "jwt_secret" {
type = string
sensitive = true
}

variable "relay_public_key" {
type = string
sensitive = true
}
78 changes: 67 additions & 11 deletions tests/context/mod.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
use {
self::server::EchoServer,
async_trait::async_trait,
echo_server::state::{ClientStoreArc, NotificationStoreArc, TenantStoreArc},
echo_server::{
config::Config,
state::{ClientStoreArc, NotificationStoreArc, TenantStoreArc},
},
sqlx::{Pool, Postgres},
std::sync::Arc,
test_context::AsyncTestContext,
std::{env, sync::Arc},
test_context::{AsyncTestContext, TestContext},
};

mod server;
mod stores;

pub const DATABASE_URL: &str = "postgres://postgres:root@localhost:5432/postgres";
pub const TENANT_DATABASE_URL: &str = "postgres://postgres:root@localhost:5433/postgres";
pub struct ConfigContext {
pub config: Config,
}

pub struct EchoServerContext {
pub server: EchoServer,
Expand All @@ -26,22 +30,74 @@ pub struct StoreContext {
pub tenants: TenantStoreArc,
}

impl TestContext for ConfigContext {
fn setup() -> Self {
let public_port = self::server::get_random_port();
let config = Config {
port: public_port,
public_url: format!("http://127.0.0.1:{public_port}"),
log_level: "info,echo-server=info".into(),
log_level_otel: "info,echo-server=trace".into(),
disable_header: true,
validate_signatures: false,
relay_public_key: env::var("RELAY_PUBLIC_KEY").unwrap_or("none".to_string()),
database_url: env::var("DATABASE_URL").unwrap(),
tenant_database_url: env::var("TENANT_DATABASE_URL").unwrap(),
#[cfg(feature = "multitenant")]
jwt_secret: "n/a".to_string(),
otel_exporter_otlp_endpoint: None,
telemetry_prometheus_port: Some(self::server::get_random_port()),
#[cfg(not(feature = "multitenant"))]
apns_type: None,
#[cfg(not(feature = "multitenant"))]
apns_certificate: None,
#[cfg(not(feature = "multitenant"))]
apns_certificate_password: None,
#[cfg(not(feature = "multitenant"))]
apns_pkcs8_pem: None,
#[cfg(not(feature = "multitenant"))]
apns_team_id: None,
#[cfg(not(feature = "multitenant"))]
apns_key_id: None,
#[cfg(not(feature = "multitenant"))]
apns_topic: None,
#[cfg(not(feature = "multitenant"))]
fcm_api_key: None,
#[cfg(any(feature = "analytics", feature = "geoblock"))]
s3_endpoint: None,
#[cfg(any(feature = "analytics", feature = "geoblock"))]
geoip_db_bucket: None,
#[cfg(any(feature = "analytics", feature = "geoblock"))]
geoip_db_key: None,
#[cfg(feature = "analytics")]
analytics_export_bucket: "example-bucket".to_string(),
is_test: true,
cors_allowed_origins: vec!["*".to_string()],
#[cfg(feature = "cloud")]
cloud_api_url: "https://example.com".to_string(),
#[cfg(feature = "cloud")]
cloud_api_key: "n/a".to_string(),
#[cfg(feature = "geoblock")]
blocked_countries: vec![],
};
Self { config }
}
}

#[async_trait]
impl AsyncTestContext for EchoServerContext {
async fn setup() -> Self {
let server = EchoServer::start().await;
let server = EchoServer::start(ConfigContext::setup().config).await;
Self { server }
}

async fn teardown(mut self) {
self.server.shutdown().await;
}
}

#[async_trait]
impl AsyncTestContext for StoreContext {
async fn setup() -> Self {
let (db, tenant_db) = stores::open_pg_connections().await;
let config = ConfigContext::setup().config;
let (db, tenant_db) =
stores::open_pg_connections(&config.database_url, &config.tenant_database_url).await;

let db_arc = Arc::new(db);
let tenant_db_arc = Arc::new(tenant_db);
Expand Down
Loading
Loading