Skip to content

Commit

Permalink
fix: using relay public key from environment variable (#241)
Browse files Browse the repository at this point in the history
  • Loading branch information
geekbrother committed Oct 11, 2023
1 parent 591effe commit 0934fd2
Show file tree
Hide file tree
Showing 20 changed files with 132 additions and 159 deletions.
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

0 comments on commit 0934fd2

Please sign in to comment.