From 632c8af7115abe83b3c987b375c54c9421a9f74a Mon Sep 17 00:00:00 2001 From: jeffgrunewald Date: Wed, 29 Mar 2023 20:56:01 -0400 Subject: [PATCH 01/15] iot config helius integration --- file_store/src/traits/msg_verify.rs | 59 +++++++------- iot_config/Cargo.toml | 2 +- iot_config/src/admin_service.rs | 42 ++++++---- iot_config/src/gateway_info.rs | 114 ++++++++++++++++++++++++++++ iot_config/src/gateway_service.rs | 15 +++- iot_config/src/lib.rs | 1 + iot_config/src/main.rs | 4 +- 7 files changed, 184 insertions(+), 53 deletions(-) create mode 100644 iot_config/src/gateway_info.rs diff --git a/file_store/src/traits/msg_verify.rs b/file_store/src/traits/msg_verify.rs index a5eeafbf0..88adad877 100644 --- a/file_store/src/traits/msg_verify.rs +++ b/file_store/src/traits/msg_verify.rs @@ -1,15 +1,7 @@ use crate::{Error, Result}; use helium_crypto::{PublicKey, Verify}; use helium_proto::services::{ - iot_config::{ - AdminAddKeyReqV1, AdminLoadRegionReqV1, AdminRemoveKeyReqV1, GatewayLocationReqV1, - GatewayRegionParamsReqV1, OrgCreateHeliumReqV1, OrgCreateRoamerReqV1, OrgDisableReqV1, - OrgEnableReqV1, RouteCreateReqV1, RouteDeleteReqV1, RouteGetDevaddrRangesReqV1, - RouteGetEuisReqV1, RouteGetReqV1, RouteListReqV1, RouteStreamReqV1, - RouteUpdateDevaddrRangesReqV1, RouteUpdateEuisReqV1, RouteUpdateReqV1, - SessionKeyFilterGetReqV1, SessionKeyFilterListReqV1, SessionKeyFilterStreamReqV1, - SessionKeyFilterUpdateReqV1, - }, + iot_config, poc_lora::{LoraBeaconReportReqV1, LoraWitnessReportReqV1}, }; use helium_proto::{ @@ -40,29 +32,32 @@ impl_msg_verify!(SpeedtestReqV1, signature); impl_msg_verify!(LoraBeaconReportReqV1, signature); impl_msg_verify!(LoraWitnessReportReqV1, signature); impl_msg_verify!(DataTransferSessionReqV1, signature); -impl_msg_verify!(OrgCreateHeliumReqV1, signature); -impl_msg_verify!(OrgCreateRoamerReqV1, signature); -impl_msg_verify!(OrgDisableReqV1, signature); -impl_msg_verify!(OrgEnableReqV1, signature); -impl_msg_verify!(RouteStreamReqV1, signature); -impl_msg_verify!(RouteListReqV1, signature); -impl_msg_verify!(RouteGetReqV1, signature); -impl_msg_verify!(RouteCreateReqV1, signature); -impl_msg_verify!(RouteUpdateReqV1, signature); -impl_msg_verify!(RouteDeleteReqV1, signature); -impl_msg_verify!(RouteGetEuisReqV1, signature); -impl_msg_verify!(RouteUpdateEuisReqV1, signature); -impl_msg_verify!(RouteGetDevaddrRangesReqV1, signature); -impl_msg_verify!(RouteUpdateDevaddrRangesReqV1, signature); -impl_msg_verify!(GatewayLocationReqV1, signature); -impl_msg_verify!(GatewayRegionParamsReqV1, signature); -impl_msg_verify!(AdminAddKeyReqV1, signature); -impl_msg_verify!(AdminLoadRegionReqV1, signature); -impl_msg_verify!(AdminRemoveKeyReqV1, signature); -impl_msg_verify!(SessionKeyFilterGetReqV1, signature); -impl_msg_verify!(SessionKeyFilterListReqV1, signature); -impl_msg_verify!(SessionKeyFilterStreamReqV1, signature); -impl_msg_verify!(SessionKeyFilterUpdateReqV1, signature); +impl_msg_verify!(iot_config::OrgCreateHeliumReqV1, signature); +impl_msg_verify!(iot_config::OrgCreateRoamerReqV1, signature); +impl_msg_verify!(iot_config::OrgDisableReqV1, signature); +impl_msg_verify!(iot_config::OrgEnableReqV1, signature); +impl_msg_verify!(iot_config::RouteStreamReqV1, signature); +impl_msg_verify!(iot_config::RouteListReqV1, signature); +impl_msg_verify!(iot_config::RouteGetReqV1, signature); +impl_msg_verify!(iot_config::RouteCreateReqV1, signature); +impl_msg_verify!(iot_config::RouteUpdateReqV1, signature); +impl_msg_verify!(iot_config::RouteDeleteReqV1, signature); +impl_msg_verify!(iot_config::RouteGetEuisReqV1, signature); +impl_msg_verify!(iot_config::RouteUpdateEuisReqV1, signature); +impl_msg_verify!(iot_config::RouteGetDevaddrRangesReqV1, signature); +impl_msg_verify!(iot_config::RouteUpdateDevaddrRangesReqV1, signature); +impl_msg_verify!(iot_config::GatewayLocationReqV1, signature); +impl_msg_verify!(iot_config::GatewayRegionParamsReqV1, signature); +impl_msg_verify!(iot_config::AdminAddKeyReqV1, signature); +impl_msg_verify!(iot_config::AdminLoadRegionReqV1, signature); +impl_msg_verify!(iot_config::AdminRemoveKeyReqV1, signature); +impl_msg_verify!(iot_config::SessionKeyFilterGetReqV1, signature); +impl_msg_verify!(iot_config::SessionKeyFilterListReqV1, signature); +impl_msg_verify!(iot_config::SessionKeyFilterStreamReqV1, signature); +impl_msg_verify!(iot_config::SessionKeyFilterUpdateReqV1, signature); +impl_msg_verify!(iot_config::GatewayInfoReqV1, signature); +impl_msg_verify!(iot_config::GatewayInfoStreamReqV1, signature); +impl_msg_verify!(iot_config::RegionParamsReqV1, signature); #[cfg(test)] mod test { diff --git a/iot_config/Cargo.toml b/iot_config/Cargo.toml index eca753f5c..de2e7b1b2 100644 --- a/iot_config/Cargo.toml +++ b/iot_config/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true anyhow = {workspace = true} async-trait = {workspace = true} base64 = {workspace = true} +chrono = {workspace = true} clap = {workspace = true} config = {workspace = true} db-store = {path = "../db_store"} @@ -35,4 +36,3 @@ tonic = {workspace = true} tracing = {workspace = true} tracing-subscriber = {workspace = true} triggered = {workspace = true} -chrono = { workspace = true } diff --git a/iot_config/src/admin_service.rs b/iot_config/src/admin_service.rs index 714e881f7..a454bd0e5 100644 --- a/iot_config/src/admin_service.rs +++ b/iot_config/src/admin_service.rs @@ -1,18 +1,18 @@ use crate::{ admin::{self, AuthCache, KeyType}, region_map::{self, RegionMap}, - GrpcResult, + GrpcResult, Settings, }; use anyhow::Result; use file_store::traits::MsgVerify; use futures::future::TryFutureExt; -use helium_crypto::{Network, PublicKey, PublicKeyBinary}; +use helium_crypto::{Keypair, Network, PublicKey, PublicKeyBinary, Sign}; use helium_proto::{ services::iot_config::{ self, AdminAddKeyReqV1, AdminKeyResV1, AdminLoadRegionReqV1, AdminLoadRegionResV1, AdminRemoveKeyReqV1, RegionParamsReqV1, RegionParamsResV1, }, - Region, + Message, }; use sqlx::{Pool, Postgres}; use tonic::{Request, Response, Status}; @@ -22,21 +22,23 @@ pub struct AdminService { pool: Pool, region_map: RegionMap, required_network: Network, + signing_key: Keypair, } impl AdminService { pub fn new( + settings: &Settings, auth_cache: AuthCache, pool: Pool, region_map: RegionMap, - required_network: Network, - ) -> Self { - Self { + ) -> Result { + Ok(Self { auth_cache, pool, region_map, - required_network, - } + required_network: settings.network, + signing_key: settings.signing_keypair()?, + }) } async fn verify_request_signature(&self, request: &R) -> Result<(), Status> @@ -168,15 +170,27 @@ impl iot_config::Admin for AdminService { Ok(Response::new(AdminLoadRegionResV1 {})) } - // placeholder implementation async fn region_params( &self, - _request: Request, + request: Request, ) -> GrpcResult { - Ok(Response::new(RegionParamsResV1 { - params: None, + let request = request.into_inner(); + self.verify_request_signature(&request).await?; + + let region = request.region(); + + let params = self.region_map.get_params(®ion).await; + + let mut resp = RegionParamsResV1 { + region: request.region, + params, signature: vec![], - region: Region::Us915.into(), - })) + }; + resp.signature = self + .signing_key + .sign(&resp.encode_to_vec()) + .map_err(|_| Status::internal("resp signing error"))?; + tracing::debug!(region = region.to_string(), "returning region params"); + Ok(Response::new(resp)) } } diff --git a/iot_config/src/gateway_info.rs b/iot_config/src/gateway_info.rs new file mode 100644 index 000000000..d0c2f7b18 --- /dev/null +++ b/iot_config/src/gateway_info.rs @@ -0,0 +1,114 @@ +use futures::stream::BoxStream; +use helium_crypto::PublicKeyBinary; +use helium_proto::{services::iot_config::GatewayInfo as GatewayInfoProto, Region}; + +pub type GatewayInfoStream = BoxStream<'static, GatewayInfo>; + +#[derive(Clone, Debug)] +pub struct GatewayInfo { + pub address: PublicKeyBinary, + pub location: Option, + pub elevation: Option, + pub gain: i32, + pub is_full_hotspot: bool, + pub region: Region, +} + +#[async_trait::async_trait] +pub trait GatewayInfoResolver { + type Error; + + async fn resolve_gateway_info( + &mut self, + address: &PublicKeyBinary, + ) -> Result, Self::Error>; + + async fn stream_gateway_info(&mut self) -> Result; +} + +impl From for GatewayInfo { + fn from(info: GatewayInfoProto) -> Self { + let elevation = if info.elevation >= 0 { Some(info.elevation) } else { None }; + let region = info.region(); + Self { + address: info.address.into(), + location: u64::from_str_radix(&info.location, 16).ok(), + elevation, + gain: info.gain, + is_full_hotspot: info.is_full_hotspot, + region, + } + } +} + +impl TryFrom for GatewayInfoProto { + type Error = hextree::Error; + + fn try_from(info: GatewayInfo) -> Result { + let location = info.location.map_or(Ok(String::new()), |location| { + Ok(hextree::Cell::from_raw(location)?.to_string()) + })?; + Ok(Self { + address: info.address.into(), + location, + elevation: info.elevation.unwrap_or(-1), + gain: info.gain, + is_full_hotspot: info.is_full_hotspot, + region: info.region.into(), + }) + } +} + +pub(crate) mod db { + use futures::stream::{Stream, StreamExt}; + use helium_crypto::PublicKeyBinary; + use sqlx::{PgExecutor, Row}; + + pub struct GatewayMetadata { + pub address: PublicKeyBinary, + pub location: Option, + pub elevation: Option, + pub gain: i32, + pub is_full_hotspot: bool, + } + + pub async fn get_info( + db: impl PgExecutor<'_>, + address: &PublicKeyBinary, + ) -> anyhow::Result> { + Ok(sqlx::query_as::<_, GatewayMetadata>( + r#" + select (hotspot_key, location, elevation, gain, is_full_hotspot) from iot_metadata + where hotspot_key = $1 + "#, + ) + .bind(address) + .fetch_optional(db) + .await?) + } + + pub fn all_info_stream<'a>( + db: impl PgExecutor<'a> + 'a, + ) -> impl Stream + 'a { + sqlx::query_as::<_, GatewayMetadata>( + r#" + select (hotspot_key, location, elevation, gain, is_full_hotspot) from iot_metadata + "#, + ) + .fetch(db) + .filter_map(|metadata| async move { metadata.ok() }) + .boxed() + } + + impl sqlx::FromRow<'_, sqlx::postgres::PgRow> for GatewayMetadata { + fn from_row(row: &sqlx::postgres::PgRow) -> sqlx::Result { + Ok(Self { + address: row.get::("hotspot_key"), + location: row.get::, &str>("location").map(|v| v as u64), + elevation: row.get::, &str>("elevation"), + gain: row.get("gain"), + is_full_hotspot: row.get("is_full_hotspot"), + }) + } + } +} diff --git a/iot_config/src/gateway_service.rs b/iot_config/src/gateway_service.rs index e2dff30e9..f5f2ac7c8 100644 --- a/iot_config/src/gateway_service.rs +++ b/iot_config/src/gateway_service.rs @@ -1,7 +1,7 @@ use crate::{region_map::RegionMap, GrpcResult, GrpcStreamResult, Settings}; use anyhow::Result; use chrono::Utc; -use file_store::traits::MsgVerify; +use file_store::traits::{MsgVerify, TimestampEncode}; use helium_crypto::{Keypair, PublicKey, PublicKeyBinary, Sign}; use helium_proto::{ services::iot_config::{ @@ -32,6 +32,15 @@ impl GatewayService { signing_key: settings.signing_keypair()?, }) } + + fn sign_response(&self, response: &R) -> Result, Status> + where + R: Message, + { + self.signing_key + .sign(&response.encode_to_vec()) + .map_err(|_| Status::internal("response signing error")) + } } #[tonic::async_trait] @@ -159,16 +168,14 @@ impl iot_config::Gateway for GatewayService { Ok(Response::new(resp)) } - // placeholder implementation async fn info(&self, _request: Request) -> GrpcResult { Ok(Response::new(GatewayInfoResV1 { - timestamp: Utc::now().timestamp() as u64, + timestamp: Utc::now().encode_timestamp(), info: None, signature: vec![], })) } - // placeholder implementation type info_streamStream = GrpcStreamResult; async fn info_stream( &self, diff --git a/iot_config/src/lib.rs b/iot_config/src/lib.rs index 4ef37810b..f2c02cb11 100644 --- a/iot_config/src/lib.rs +++ b/iot_config/src/lib.rs @@ -1,5 +1,6 @@ pub mod admin; pub mod admin_service; +pub mod gateway_info; pub mod gateway_service; pub mod lora_field; pub mod org; diff --git a/iot_config/src/main.rs b/iot_config/src/main.rs index dc8d95628..0564d6325 100644 --- a/iot_config/src/main.rs +++ b/iot_config/src/main.rs @@ -90,11 +90,11 @@ impl Daemon { route_svc.clone_update_channel(), ); let admin_svc = AdminService::new( + settings, auth_cache.clone(), pool.clone(), region_map.clone(), - settings.network, - ); + )?; let session_key_filter_svc = SessionKeyFilterService::new( auth_cache.clone(), pool.clone(), From 47e3f685071d9e5bdfc723f826df030b9f9669d5 Mon Sep 17 00:00:00 2001 From: jeffgrunewald Date: Fri, 31 Mar 2023 16:01:19 -0400 Subject: [PATCH 02/15] iot config service to use helius db --- Cargo.lock | 1 - Cargo.toml | 6 +- iot_config/Cargo.toml | 1 - iot_config/migrations/7_oracle_key_type.sql | 1 + iot_config/pkg/settings-template.toml | 14 - iot_config/src/admin.rs | 248 +++++++------- iot_config/src/admin_service.rs | 141 ++++++-- iot_config/src/gateway_info.rs | 108 ++++-- iot_config/src/gateway_service.rs | 258 ++++++++++----- iot_config/src/main.rs | 23 +- iot_config/src/org_service.rs | 158 ++++++--- iot_config/src/region_map.rs | 76 +++-- iot_config/src/route.rs | 160 ++++++--- iot_config/src/route_service.rs | 350 ++++++++++++++------ iot_config/src/session_key.rs | 42 ++- iot_config/src/session_key_service.rs | 146 +++++--- iot_config/src/settings.rs | 2 - 17 files changed, 1151 insertions(+), 584 deletions(-) create mode 100644 iot_config/migrations/7_oracle_key_type.sql diff --git a/Cargo.lock b/Cargo.lock index 13bda8eb6..45f17f506 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3281,7 +3281,6 @@ dependencies = [ "libflate", "metrics", "metrics-exporter-prometheus", - "node-follower", "poc-metrics", "prost", "serde", diff --git a/Cargo.toml b/Cargo.toml index 8f43c221a..755001c07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,13 +55,13 @@ sqlx = {version = "0", features = [ "runtime-tokio-rustls" ]} -helium-crypto = {version = "0.6.3", features=["sqlx-postgres", "multisig"]} -helium-proto = {git = "https://github.com/helium/proto", branch = "master", features = ["services"]} +helium-crypto = {version = "0.6.8", features=["sqlx-postgres", "multisig"]} +helium-proto = {git = "https://github.com/helium/proto", branch = "jg/oracle-admin-keys", features = ["services"]} hextree = "*" solana-client = "1.14" solana-sdk = "1.14" reqwest = {version = "0", default-features=false, features = ["gzip", "json", "rustls-tls"]} -beacon = {git = "https://github.com/helium/gateway-rs.git", branch = "main"} +beacon = {git = "https://github.com/helium/gateway-rs.git", branch = "jg/temp-proto-upgrade"} humantime = "2" metrics = "0" metrics-exporter-prometheus = "0" diff --git a/iot_config/Cargo.toml b/iot_config/Cargo.toml index de2e7b1b2..5decd8321 100644 --- a/iot_config/Cargo.toml +++ b/iot_config/Cargo.toml @@ -23,7 +23,6 @@ hextree = {workspace = true} libflate = "1" metrics = {workspace = true} metrics-exporter-prometheus = {workspace = true} -node-follower = {path = "../node_follower"} poc-metrics = {path = "../metrics"} prost = {workspace = true} serde = {workspace = true} diff --git a/iot_config/migrations/7_oracle_key_type.sql b/iot_config/migrations/7_oracle_key_type.sql new file mode 100644 index 000000000..1917ded58 --- /dev/null +++ b/iot_config/migrations/7_oracle_key_type.sql @@ -0,0 +1 @@ +alter type key_type add value 'oracle'; \ No newline at end of file diff --git a/iot_config/pkg/settings-template.toml b/iot_config/pkg/settings-template.toml index 6bb76f9fb..7861c7779 100644 --- a/iot_config/pkg/settings-template.toml +++ b/iot_config/pkg/settings-template.toml @@ -40,17 +40,3 @@ max_connections = 20 # Endpoint for metrics. Default below # # endpoint = "127.0.0.1:19000" - -[follower] - -# Local grpc url to node follower for gateway location lookups -#[serde(with = "http_serde::uri", default = "default_url")] -# url = http://127.0.0.1:8080 -# Start block to begin streaming followed transactions -block = 0 -# Connect timeout for follower in seconds. Default 5 -# connect = 5 -# RPC timeout for follower in seconds. Default 5 -# rpc = 5 -# Batch size for gateway stream results. Default 100 -# batch = 100 diff --git a/iot_config/src/admin.rs b/iot_config/src/admin.rs index c79584415..71addbdb4 100644 --- a/iot_config/src/admin.rs +++ b/iot_config/src/admin.rs @@ -1,182 +1,105 @@ use crate::settings::Settings; +use anyhow::anyhow; use file_store::traits::MsgVerify; use helium_crypto::{PublicKey, PublicKeyBinary}; use helium_proto::services::iot_config::admin_add_key_req_v1::KeyTypeV1 as ProtoKeyType; use serde::Serialize; use sqlx::Row; -use std::{collections::HashSet, sync::Arc}; -use tokio::sync::RwLock; +use std::collections::HashMap; +use tokio::sync::watch; -pub async fn get_admin_keys( - key_type: &KeyType, - db: impl sqlx::PgExecutor<'_>, -) -> Result, AdminAuthError> { - Ok( - sqlx::query_scalar::<_, PublicKey>( - r#" select pubkey from admin_keys where key_type = $1 "#, - ) - .bind(key_type) - .fetch_all(db) - .await? - .into_iter() - .map(PublicKey::try_from) - .filter_map(|key| key.ok()) - .collect(), - ) -} - -pub async fn insert_key( - pubkey: PublicKeyBinary, - key_type: KeyType, - db: impl sqlx::PgExecutor<'_>, -) -> Result<(), sqlx::Error> { - sqlx::query(r#" insert into admin_keys (pubkey, key_type) values ($1, $2) "#) - .bind(pubkey) - .bind(key_type) - .execute(db) - .await - .map(|_| ()) -} - -pub async fn remove_key( - pubkey: PublicKeyBinary, - db: impl sqlx::PgExecutor<'_>, -) -> Result, sqlx::Error> { - let result = sqlx::query( - r#" - delete from admin_keys - where pubkey = $1 - returning (pubkey, key_type) - "#, - ) - .bind(pubkey) - .fetch_optional(db) - .await? - .map(|row| { - ( - row.get::("pubkey"), - row.get::("key_type"), - ) - }); - - Ok(result) -} +pub type CacheKeys = HashMap; #[derive(Clone, Debug)] pub struct AuthCache { - admin_keys: Arc>>, - router_keys: Arc>>, + cache_receiver: watch::Receiver, } impl AuthCache { pub async fn new( settings: &Settings, db: impl sqlx::PgExecutor<'_> + Copy, - ) -> Result { + ) -> anyhow::Result<(watch::Sender, Self)> { let config_admin = settings.admin_pubkey()?; - let mut admin_keys = get_admin_keys(&KeyType::Administrator, db) + let mut stored_keys = fetch_stored_keys(db) .await? .into_iter() - .collect::>(); - _ = admin_keys.insert(config_admin); + .collect::(); + stored_keys.insert(config_admin, KeyType::Administrator); - let router_keys = get_admin_keys(&KeyType::PacketRouter, db) - .await? - .into_iter() - .collect::>(); + let (cache_sender, cache_receiver) = watch::channel(stored_keys); - Ok(Self { - admin_keys: Arc::new(RwLock::new(admin_keys)), - router_keys: Arc::new(RwLock::new(router_keys)), - }) + Ok((cache_sender, Self { cache_receiver })) } - pub async fn verify_signature( - &self, - key_type: KeyType, - request: &R, - ) -> Result<(), AdminAuthError> + pub fn verify_signature(&self, signer: &PublicKey, request: &R) -> anyhow::Result<()> where R: MsgVerify, { - match key_type { - KeyType::Administrator => { - for key in self.admin_keys.read().await.iter() { - if request.verify(key).is_ok() { - tracing::debug!("request authorized by admin"); - return Ok(()); - } - } - } - KeyType::PacketRouter => { - for key in self.router_keys.read().await.iter() { - if request.verify(key).is_ok() { - tracing::debug!("request authorized by packet router {key}"); - return Ok(()); - } - } - } + if self.cache_receiver.borrow().contains_key(signer) && request.verify(signer).is_ok() { + tracing::debug!(pubkey = signer.to_string(), "request authorized"); + Ok(()) + } else { + Err(anyhow!("unauthorized request")) } - Err(AdminAuthError::UnauthorizedRequest) } - pub async fn insert_key(&self, key_type: KeyType, key: PublicKey) { - match key_type { - KeyType::Administrator => self.admin_keys.write(), - KeyType::PacketRouter => self.router_keys.write(), + pub fn verify_signature_with_type( + &self, + key_type: KeyType, + signer: &PublicKey, + request: &R, + ) -> anyhow::Result<()> + where + R: MsgVerify, + { + if self.cache_receiver.borrow().get_key_value(signer) == Some((signer, &key_type)) + && request.verify(signer).is_ok() + { + tracing::debug!(pubkey = signer.to_string(), "request authorized"); + Ok(()) + } else { + Err(anyhow!("unauthorized request")) } - .await - .insert(key); } - pub async fn remove_key(&self, key_type: KeyType, key: &PublicKey) { - match key_type { - KeyType::Administrator => self.admin_keys.write(), - KeyType::PacketRouter => self.router_keys.write(), - } - .await - .remove(key); + pub fn get_keys(&self) -> Vec<(PublicKey, KeyType)> { + self.cache_receiver + .borrow() + .iter() + .map(|(k, t)| (k.clone(), *t)) + .collect() } - pub async fn get_keys(&self, key_type: KeyType) -> Vec { - match key_type { - KeyType::Administrator => self.admin_keys.read(), - KeyType::PacketRouter => self.router_keys.read(), - } - .await - .iter() - .cloned() - .collect::>() + pub fn get_keys_by_type(&self, key_type: KeyType) -> Vec { + self.cache_receiver + .borrow() + .iter() + .filter_map(|(k, t)| { + if t == &key_type { + Some(k.clone()) + } else { + None + } + }) + .collect() } } -#[derive(Clone, Copy, Debug, Serialize, sqlx::Type)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, sqlx::Type)] #[sqlx(type_name = "key_type", rename_all = "snake_case")] pub enum KeyType { Administrator, PacketRouter, + Oracle, } -#[derive(thiserror::Error, Debug)] -pub enum AdminAuthError { - #[error("unauthorized admin request signature")] - UnauthorizedRequest, - #[error("error deserializing pubkey: {0}")] - DecodeKey(#[from] helium_crypto::Error), - #[error("error retrieving saved admin keys: {0}")] - DbStore(#[from] sqlx::Error), -} - -#[derive(thiserror::Error, Debug)] -#[error("unsupported key type {0}")] -pub struct UnsupportedKeyTypeError(i32); - impl KeyType { - pub fn from_i32(v: i32) -> Result { + pub fn from_i32(v: i32) -> anyhow::Result { ProtoKeyType::from_i32(v) .map(|kt| kt.into()) - .ok_or(UnsupportedKeyTypeError(v)) + .ok_or(anyhow!("unsupported key type {}", v)) } } @@ -191,6 +114,7 @@ impl From<&KeyType> for ProtoKeyType { match skt { KeyType::Administrator => ProtoKeyType::Administrator, KeyType::PacketRouter => ProtoKeyType::PacketRouter, + KeyType::Oracle => ProtoKeyType::Oracle, } } } @@ -200,6 +124,66 @@ impl From for KeyType { match kt { ProtoKeyType::Administrator => KeyType::Administrator, ProtoKeyType::PacketRouter => KeyType::PacketRouter, + ProtoKeyType::Oracle => KeyType::Oracle, } } } + +impl std::fmt::Display for KeyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::Administrator => "administrator", + Self::PacketRouter => "packet_router", + Self::Oracle => "oracle", + }; + f.write_str(s) + } +} + +pub async fn fetch_stored_keys( + db: impl sqlx::PgExecutor<'_>, +) -> anyhow::Result> { + Ok(sqlx::query(r#" select pubkey, key_type from admin_keys "#) + .fetch_all(db) + .await? + .into_iter() + .map(|row| (row.get::("pubkey"), row.get("key_type"))) + .collect()) +} + +pub async fn insert_key( + pubkey: PublicKeyBinary, + key_type: KeyType, + db: impl sqlx::PgExecutor<'_>, +) -> anyhow::Result<()> { + Ok( + sqlx::query(r#" insert into admin_keys (pubkey, key_type) values ($1, $2) "#) + .bind(pubkey) + .bind(key_type) + .execute(db) + .await + .map(|_| ())?, + ) +} + +pub async fn remove_key( + pubkey: PublicKeyBinary, + db: impl sqlx::PgExecutor<'_>, +) -> anyhow::Result> { + Ok(sqlx::query( + r#" + delete from admin_keys + where pubkey = $1 + returning (pubkey, key_type) + "#, + ) + .bind(pubkey) + .fetch_optional(db) + .await? + .map(|row| { + ( + row.get::("pubkey"), + row.get::("key_type"), + ) + })) +} diff --git a/iot_config/src/admin_service.rs b/iot_config/src/admin_service.rs index a454bd0e5..00194cf69 100644 --- a/iot_config/src/admin_service.rs +++ b/iot_config/src/admin_service.rs @@ -1,10 +1,11 @@ use crate::{ - admin::{self, AuthCache, KeyType}, - region_map::{self, RegionMap}, + admin::{self, AuthCache, CacheKeys, KeyType}, + region_map::{self, RegionMap, RegionMapReader}, GrpcResult, Settings, }; -use anyhow::Result; -use file_store::traits::MsgVerify; +use anyhow::{anyhow, Result}; +use chrono::Utc; +use file_store::traits::{MsgVerify, TimestampEncode}; use futures::future::TryFutureExt; use helium_crypto::{Keypair, Network, PublicKey, PublicKeyBinary, Sign}; use helium_proto::{ @@ -15,12 +16,15 @@ use helium_proto::{ Message, }; use sqlx::{Pool, Postgres}; +use tokio::sync::watch; use tonic::{Request, Response, Status}; pub struct AdminService { auth_cache: AuthCache, + auth_updater: watch::Sender, pool: Pool, - region_map: RegionMap, + region_map: RegionMapReader, + region_updater: watch::Sender, required_network: Network, signing_key: Keypair, } @@ -29,25 +33,28 @@ impl AdminService { pub fn new( settings: &Settings, auth_cache: AuthCache, + auth_updater: watch::Sender, pool: Pool, - region_map: RegionMap, + region_map: RegionMapReader, + region_updater: watch::Sender, ) -> Result { Ok(Self { auth_cache, + auth_updater, pool, region_map, + region_updater, required_network: settings.network, signing_key: settings.signing_keypair()?, }) } - async fn verify_request_signature(&self, request: &R) -> Result<(), Status> + fn verify_request_signature(&self, signer: &PublicKey, request: &R) -> Result<(), Status> where R: MsgVerify, { self.auth_cache - .verify_signature(KeyType::Administrator, request) - .await + .verify_signature_with_type(KeyType::Administrator, signer, request) .map_err(|_| Status::permission_denied("invalid admin signature"))?; Ok(()) } @@ -67,6 +74,15 @@ impl AdminService { PublicKey::try_from(bytes) .map_err(|_| Status::invalid_argument(format!("invalid public key: {bytes:?}"))) } + + fn sign_response(&self, response: &R) -> Result, Status> + where + R: Message, + { + self.signing_key + .sign(&response.encode_to_vec()) + .map_err(|_| Status::internal("response signing error")) + } } #[tonic::async_trait] @@ -74,7 +90,8 @@ impl iot_config::Admin for AdminService { async fn add_key(&self, request: Request) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request).await?; + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request)?; let key_type = request.key_type().into(); let pubkey = self @@ -84,29 +101,51 @@ impl iot_config::Admin for AdminService { admin::insert_key(request.pubkey.clone().into(), key_type, &self.pool) .and_then(|_| async move { - self.auth_cache.insert_key(key_type, pubkey).await; - Ok(()) + if self.auth_updater.send_if_modified(|cache| { + if let std::collections::hash_map::Entry::Vacant(key) = cache.entry(pubkey) { + key.insert(key_type); + true + } else { + false + } + }) { + Ok(()) + } else { + Err(anyhow!("key already registered")) + } }) - .map_err(|_| { + .map_err(|err| { let pubkey: PublicKeyBinary = request.pubkey.into(); tracing::error!(pubkey = pubkey.to_string(), "pubkey add failed"); - Status::internal(format!("error saving requested key: {pubkey}")) + Status::internal(format!("error saving requested key: {pubkey}, {err:?}")) }) .await?; - Ok(Response::new(AdminKeyResV1 {})) + let timestamp = Utc::now().encode_timestamp(); + let signer = self.signing_key.public_key().into(); + let mut resp = AdminKeyResV1 { + timestamp, + signer, + signature: vec![], + }; + resp.signature = self.sign_response(&resp.encode_to_vec())?; + + Ok(Response::new(resp)) } async fn remove_key(&self, request: Request) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request).await?; + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request)?; admin::remove_key(request.pubkey.clone().into(), &self.pool) .and_then(|deleted| async move { match deleted { - Some((pubkey, key_type)) => { - self.auth_cache.remove_key(key_type, &pubkey).await; + Some((pubkey, _key_type)) => { + self.auth_updater.send_modify(|cache| { + cache.remove(&pubkey); + }); Ok(()) } None => Ok(()), @@ -119,7 +158,16 @@ impl iot_config::Admin for AdminService { }) .await?; - Ok(Response::new(AdminKeyResV1 {})) + let timestamp = Utc::now().encode_timestamp(); + let signer = self.signing_key.public_key().into(); + let mut resp = AdminKeyResV1 { + timestamp, + signer, + signature: vec![], + }; + resp.signature = self.sign_response(&resp.encode_to_vec())?; + + Ok(Response::new(resp)) } async fn load_region( @@ -127,7 +175,9 @@ impl iot_config::Admin for AdminService { request: Request, ) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request).await?; + + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request)?; let region = Region::from_i32(request.region).ok_or(Status::invalid_argument(format!( "invalid lora region {}", @@ -151,23 +201,41 @@ impl iot_config::Admin for AdminService { None }; - let updated_region = region_map::update_region(region, ¶ms, idz, &self.pool) - .await + region_map::update_region(region, ¶ms.clone(), idz, &self.pool) + .and_then(|updated_region| async move { + self.region_updater.send_modify(|region_map| { + region_map.insert_params(region, params); + }); + match updated_region { + Some(region_tree) => { + tracing::debug!(region_cells = region_tree.len(), "new compacted region map"); + self.region_updater.send_modify(|region_map| { + region_map.replace_tree(region_tree) + }); + } + None => () + }; + Ok(()) + }) .map_err(|err| { tracing::error!( region = region.to_string(), "failed to update region: {err:?}" ); Status::internal("region update failed") - })?; + }) + .await?; - self.region_map.insert_params(region, params).await; - if let Some(region_tree) = updated_region { - tracing::debug!(region_cells = region_tree.len(), "new compacted region map"); - self.region_map.replace_tree(region_tree).await; - } + let timestamp = Utc::now().encode_timestamp(); + let signer = self.signing_key.public_key().into(); + let mut resp = AdminLoadRegionResV1 { + timestamp, + signer, + signature: vec![], + }; + resp.signature = self.sign_response(&resp.encode_to_vec())?; - Ok(Response::new(AdminLoadRegionResV1 {})) + Ok(Response::new(resp)) } async fn region_params( @@ -175,21 +243,24 @@ impl iot_config::Admin for AdminService { request: Request, ) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request).await?; + + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request)?; let region = request.region(); - let params = self.region_map.get_params(®ion).await; + let params = self.region_map.get_params(®ion); + let timestamp = Utc::now().encode_timestamp(); + let signer = self.signing_key.public_key().into(); let mut resp = RegionParamsResV1 { region: request.region, params, + signer, signature: vec![], + timestamp, }; - resp.signature = self - .signing_key - .sign(&resp.encode_to_vec()) - .map_err(|_| Status::internal("resp signing error"))?; + resp.signature = self.sign_response(&resp.encode_to_vec())?; tracing::debug!(region = region.to_string(), "returning region params"); Ok(Response::new(resp)) } diff --git a/iot_config/src/gateway_info.rs b/iot_config/src/gateway_info.rs index d0c2f7b18..00cc63814 100644 --- a/iot_config/src/gateway_info.rs +++ b/iot_config/src/gateway_info.rs @@ -1,17 +1,62 @@ +use crate::region_map; +use anyhow::anyhow; use futures::stream::BoxStream; use helium_crypto::PublicKeyBinary; -use helium_proto::{services::iot_config::GatewayInfo as GatewayInfoProto, Region}; +use helium_proto::{ + services::iot_config::{ + GatewayInfo as GatewayInfoProto, GatewayMetadata as GatewayMetadataProto, + }, + Region, +}; pub type GatewayInfoStream = BoxStream<'static, GatewayInfo>; +#[derive(Clone, Debug)] +pub struct GatewayMetadata { + pub location: u64, + pub elevation: i32, + pub gain: i32, + pub region: Region, +} + #[derive(Clone, Debug)] pub struct GatewayInfo { pub address: PublicKeyBinary, - pub location: Option, - pub elevation: Option, - pub gain: i32, + pub metadata: Option, pub is_full_hotspot: bool, - pub region: Region, +} + +impl GatewayInfo { + pub fn chain_metadata_to_info(meta: db::IotMetadata, region_map: ®ion_map::RegionMapReader) -> Self { + let metadata = if let (Some(location), Some(elevation), Some(gain)) = (meta.location, meta.elevation, meta.gain) { + if let Ok(region) = h3index_to_region(location, region_map) { + Some(GatewayMetadata { + location, + elevation, + gain, + region, + }) + } else { + None + } + } else { + None + }; + + Self { + address: meta.address, + is_full_hotspot: meta.is_full_hotspot, + metadata, + } + } +} + +fn h3index_to_region(location: u64, region_map: ®ion_map::RegionMapReader) -> anyhow::Result { + hextree::Cell::from_raw(location) + .map(|cell| { + region_map.get_region(cell) + })? + .ok_or(anyhow!("invalid region")) } #[async_trait::async_trait] @@ -28,15 +73,22 @@ pub trait GatewayInfoResolver { impl From for GatewayInfo { fn from(info: GatewayInfoProto) -> Self { - let elevation = if info.elevation >= 0 { Some(info.elevation) } else { None }; - let region = info.region(); + let metadata = if let Some(metadata) = info.metadata { + u64::from_str_radix(&metadata.location, 16) + .map(|location| GatewayMetadata { + location, + elevation: metadata.elevation, + gain: metadata.gain, + region: metadata.region(), + }) + .ok() + } else { + None + }; Self { address: info.address.into(), - location: u64::from_str_radix(&info.location, 16).ok(), - elevation, - gain: info.gain, is_full_hotspot: info.is_full_hotspot, - region, + metadata, } } } @@ -45,16 +97,20 @@ impl TryFrom for GatewayInfoProto { type Error = hextree::Error; fn try_from(info: GatewayInfo) -> Result { - let location = info.location.map_or(Ok(String::new()), |location| { - Ok(hextree::Cell::from_raw(location)?.to_string()) - })?; + let metadata = if let Some(metadata) = info.metadata { + Some(GatewayMetadataProto { + location: hextree::Cell::from_raw(metadata.location)?.to_string(), + elevation: metadata.elevation, + gain: metadata.gain, + region: metadata.region.into(), + }) + } else { + None + }; Ok(Self { address: info.address.into(), - location, - elevation: info.elevation.unwrap_or(-1), - gain: info.gain, is_full_hotspot: info.is_full_hotspot, - region: info.region.into(), + metadata, }) } } @@ -64,19 +120,19 @@ pub(crate) mod db { use helium_crypto::PublicKeyBinary; use sqlx::{PgExecutor, Row}; - pub struct GatewayMetadata { + pub struct IotMetadata { pub address: PublicKeyBinary, pub location: Option, pub elevation: Option, - pub gain: i32, + pub gain: Option, pub is_full_hotspot: bool, } pub async fn get_info( db: impl PgExecutor<'_>, address: &PublicKeyBinary, - ) -> anyhow::Result> { - Ok(sqlx::query_as::<_, GatewayMetadata>( + ) -> anyhow::Result> { + Ok(sqlx::query_as::<_, IotMetadata>( r#" select (hotspot_key, location, elevation, gain, is_full_hotspot) from iot_metadata where hotspot_key = $1 @@ -89,8 +145,8 @@ pub(crate) mod db { pub fn all_info_stream<'a>( db: impl PgExecutor<'a> + 'a, - ) -> impl Stream + 'a { - sqlx::query_as::<_, GatewayMetadata>( + ) -> impl Stream + 'a { + sqlx::query_as::<_, IotMetadata>( r#" select (hotspot_key, location, elevation, gain, is_full_hotspot) from iot_metadata "#, @@ -100,13 +156,13 @@ pub(crate) mod db { .boxed() } - impl sqlx::FromRow<'_, sqlx::postgres::PgRow> for GatewayMetadata { + impl sqlx::FromRow<'_, sqlx::postgres::PgRow> for IotMetadata { fn from_row(row: &sqlx::postgres::PgRow) -> sqlx::Result { Ok(Self { address: row.get::("hotspot_key"), location: row.get::, &str>("location").map(|v| v as u64), elevation: row.get::, &str>("elevation"), - gain: row.get("gain"), + gain: row.get::, &str>("gain"), is_full_hotspot: row.get("is_full_hotspot"), }) } diff --git a/iot_config/src/gateway_service.rs b/iot_config/src/gateway_service.rs index f5f2ac7c8..a7ce97d4a 100644 --- a/iot_config/src/gateway_service.rs +++ b/iot_config/src/gateway_service.rs @@ -1,7 +1,14 @@ -use crate::{region_map::RegionMap, GrpcResult, GrpcStreamResult, Settings}; +use crate::{ + admin::AuthCache, gateway_info::{self, GatewayInfo, db::IotMetadata}, + region_map::RegionMapReader, GrpcResult, GrpcStreamResult, Settings +}; use anyhow::Result; use chrono::Utc; use file_store::traits::{MsgVerify, TimestampEncode}; +use futures::{ + future::TryFutureExt, + stream::{StreamExt, TryStreamExt}, +}; use helium_crypto::{Keypair, PublicKey, PublicKeyBinary, Sign}; use helium_proto::{ services::iot_config::{ @@ -12,27 +19,32 @@ use helium_proto::{ Message, Region, }; use hextree::Cell; -use node_follower::{ - follower_service::FollowerService, - gateway_resp::{GatewayInfo, GatewayInfoResolver}, -}; +use sqlx::{Pool, Postgres}; +use std::sync::Arc; use tonic::{Request, Response, Status}; pub struct GatewayService { - follower_service: FollowerService, - region_map: RegionMap, - signing_key: Keypair, + auth_cache: AuthCache, + pool: Pool, + region_map: RegionMapReader, + signing_key: Arc, } impl GatewayService { - pub fn new(settings: &Settings, region_map: RegionMap) -> Result { + pub fn new(settings: &Settings, pool: Pool, region_map: RegionMapReader, auth_cache: AuthCache) -> Result { Ok(Self { - follower_service: FollowerService::from_settings(&settings.follower), + auth_cache, + pool, region_map, - signing_key: settings.signing_keypair()?, + signing_key: Arc::new(settings.signing_keypair()?), }) } + fn verify_public_key(&self, bytes: &[u8]) -> Result { + PublicKey::try_from(bytes) + .map_err(|_| Status::invalid_argument(format!("invalid public key: {bytes:?}"))) + } + fn sign_response(&self, response: &R) -> Result, Status> where R: Message, @@ -41,6 +53,16 @@ impl GatewayService { .sign(&response.encode_to_vec()) .map_err(|_| Status::internal("response signing error")) } + + fn verify_request_signature(&self, signer: &PublicKey, request: &R) -> Result<(), Status> + where + R: MsgVerify, + { + self.auth_cache + .verify_signature(signer, request) + .map_err(|_| Status::permission_denied("invalid admin signature"))?; + Ok(()) + } } #[tonic::async_trait] @@ -49,33 +71,44 @@ impl iot_config::Gateway for GatewayService { &self, request: Request, ) -> GrpcResult { - // Should this rpc be admin-authorized only or should a requesting pubkey - // field be added to the request to do basic signature verification, allowing - // open access but discourage endpoint abuse? let request = request.into_inner(); - let gateway_address: &PublicKeyBinary = &request.gateway.into(); + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request)?; - let location = self - .follower_service - .clone() - .resolve_gateway_info(gateway_address) + let address: &PublicKeyBinary = &request.gateway.into(); + + let location = gateway_info::db::get_info(&self.pool, address) .await - .map_err(|_| Status::internal(format!("error retrieving gateway {gateway_address}"))) - .and_then(|info| { - info.location - .ok_or_else(|| Status::not_found(format!("{gateway_address} not asserted"))) - })?; + .map_err(|_| Status::internal("error fetching gateway info"))? + .map_or_else( + || Err(Status::not_found(format!("gateway not found: pubkey = {address:}"))), + |info| { + if let Some(location) = info.location { + Ok(location) + } else { + Err(Status::not_found(format!("gateway unasserted: pubkey = {address:}"))) + } + } + )?; let location = Cell::from_raw(location) .map_err(|_| { Status::internal(format!( - "invalid h3 index location {location} for {gateway_address}" + "invalid h3 index location {location} for {address}" )) })? .to_string(); - Ok(Response::new(GatewayLocationResV1 { location })) + let mut resp = GatewayLocationResV1 { + location, + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; + + Ok(Response::new(resp)) } async fn region_params( @@ -84,105 +117,166 @@ impl iot_config::Gateway for GatewayService { ) -> GrpcResult { let request = request.into_inner(); - let pubkey = PublicKey::try_from(request.address.clone()) - .map_err(|_| Status::invalid_argument("invalid gateway address"))?; + let pubkey = self.verify_public_key(&request.address)?; request .verify(&pubkey) .map_err(|_| Status::permission_denied("invalid request signature"))?; - let pubkey: &PublicKeyBinary = &pubkey.into(); - tracing::debug!(pubkey = pubkey.to_string(), "fetching region params"); + let address: &PublicKeyBinary = &pubkey.into(); + tracing::debug!(pubkey = address.to_string(), "fetching region params"); let default_region = Region::from_i32(request.region).ok_or(Status::invalid_argument( format!("invalid lora region {}", request.region), ))?; - let (region, gain) = match self - .follower_service - .clone() - .resolve_gateway_info(pubkey) + let (region, gain) = if let Some(info) = gateway_info::db::get_info(&self.pool, address) .await - { - Err(_) => { - tracing::debug!( - pubkey = pubkey.to_string(), - default_region = default_region.to_string(), - "error retrieving gateway from chain" - ); - (default_region, 0) - } - Ok(GatewayInfo { location, gain, .. }) => { - let region = match location { - None => { - tracing::debug!( - pubkey = pubkey.to_string(), - default_region = default_region.to_string(), - "no asserted location" - ); - default_region - } - Some(location) => match Cell::from_raw(location) { + .map_err(|_| Status::internal("error fetching gateway info"))? { + if let (Some(location), Some(gain)) = (info.location, info.gain) { + let region = match hextree::Cell::from_raw(location) { Ok(h3_location) => self .region_map .get_region(h3_location) - .await .unwrap_or_else(|| { - tracing::debug!( - pubkey = pubkey.to_string(), - location = location, - "gateway region lookup failed for assert location" - ); + tracing::debug!(pubkey = address.to_string(), location = location, "gateway region lookup failed for asserted location"); default_region }), Err(_) => { - tracing::debug!( - pubkey = pubkey.to_string(), - location = location, - "gateway asserted location is invalid h3 index" - ); + tracing::debug!(pubkey = address.to_string(), location = location, "gateway asserted location is invalid h3 index"); default_region } - }, - }; - (region, gain) - } - }; + }; + (region, gain) + } else { + tracing::debug!(pubkey = address.to_string(), default_region = default_region.to_string(), "gateway not asserted"); + (default_region, 0) + } + } else { + tracing::debug!(pubkey = address.to_string(), default_region = default_region.to_string(), "error retrieving gateway from chain"); + (default_region, 0) + }; - let params = self.region_map.get_params(®ion).await; + let params = self.region_map.get_params(®ion); let mut resp = GatewayRegionParamsResV1 { region: region.into(), params, gain: gain as u64, + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), signature: vec![], }; - resp.signature = self - .signing_key - .sign(&resp.encode_to_vec()) - .map_err(|_| Status::internal("resp signing error"))?; + resp.signature = self.sign_response(&resp)?; tracing::debug!( - pubkey = pubkey.to_string(), + pubkey = address.to_string(), region = region.to_string(), "returning region params" ); Ok(Response::new(resp)) } - async fn info(&self, _request: Request) -> GrpcResult { - Ok(Response::new(GatewayInfoResV1 { + async fn info(&self, request: Request) -> GrpcResult { + let request = request.into_inner(); + + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request)?; + + let address = &request.address.into(); + let metadata_info = gateway_info::db::get_info(&self.pool, address) + .await + .map_err(|_| Status::internal("error fetching gateway info"))? + .ok_or(Status::not_found(format!("gateway not found: pubkey = {address:}")))?; + + let gateway_info = GatewayInfo::chain_metadata_to_info(metadata_info, &self.region_map); + let mut resp = GatewayInfoResV1 { + info: Some(gateway_info.try_into().map_err(|_| Status::internal("unexpected error converting gateway info to protobuf"))?), timestamp: Utc::now().encode_timestamp(), - info: None, + signer: self.signing_key.public_key().into(), signature: vec![], - })) + }; + resp.signature = self.sign_response(&resp.encode_to_vec())?; + + Ok(Response::new(resp)) } type info_streamStream = GrpcStreamResult; async fn info_stream( &self, - _request: Request, + request: Request, ) -> GrpcResult { - let (_tx, rx) = tokio::sync::mpsc::channel(20); + let request = request.into_inner(); + + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request)?; + + tracing::debug!("fetching all gateways' info"); + + let pool = self.pool.clone(); + let signing_key = self.signing_key.clone(); + let batch_size = request.batch_size; + let region_map = self.region_map.clone(); + + let (tx, rx) = tokio::sync::mpsc::channel(20); + + tokio::spawn(async move { + stream_all_gateways_info(&pool, tx.clone(), &signing_key, region_map.clone(), batch_size).await + }); Ok(Response::new(GrpcStreamResult::new(rx))) } } + +async fn stream_all_gateways_info( + pool: &Pool, + tx: tokio::sync::mpsc::Sender>, + signing_key: &Keypair, + region_map: RegionMapReader, + batch_size: u32, +) -> anyhow::Result<()> { + let timestamp = Utc::now().encode_timestamp(); + let signer: Vec = signing_key.public_key().into(); + let region_map = ®ion_map; + let tx = &tx; + Ok(gateway_info::db::all_info_stream(pool) + .map(Ok::) + .try_filter_map(|info| async move { + let result: Option = match GatewayInfo::chain_metadata_to_info(info, region_map).try_into() { + Ok(info_proto) => Some(info_proto), + Err(_) => None, + }; + Ok(result) + }) + .try_chunks(batch_size as usize) + .map_ok(move |batch| { + ( + GatewayInfoStreamResV1 { + gateways: batch, + timestamp, + signer: signer.clone(), + signature: vec![], + }, + signing_key.clone() + ) + }) + .try_filter_map(|(res, keypair)| async move { + let result = match keypair.sign(&res.encode_to_vec()) { + Ok(signature) => Some(GatewayInfoStreamResV1 { + gateways: res.gateways, + timestamp: res.timestamp, + signer: res.signer, + signature, + }), + Err(_) => None, + }; + Ok(result) + }) + .map_err(|err| Status::internal(format!("info batch failed with reason: {err:?}"))) + .try_for_each(|res| { + tx.send(Ok(res)) + .map_err(|err| Status::internal(format!("info batch send failed with reason {err:?}"))) + }) + .or_else(|err| { + tx.send(Err(Status::internal(format!("info batch failed with reason: {err:?}")))) + }) + .await?) +} diff --git a/iot_config/src/main.rs b/iot_config/src/main.rs index 0564d6325..f91873597 100644 --- a/iot_config/src/main.rs +++ b/iot_config/src/main.rs @@ -77,29 +77,36 @@ impl Daemon { let listen_addr = settings.listen_addr()?; - let auth_cache = AuthCache::new(settings, &pool).await?; - let region_map = RegionMap::new(&pool).await?; + let (auth_updater, auth_cache) = AuthCache::new(settings, &pool).await?; + let (region_updater, region_map) = RegionMapReader::new(&pool).await?; - let gateway_svc = GatewayService::new(settings, region_map.clone())?; - let route_svc = - RouteService::new(auth_cache.clone(), pool.clone(), shutdown_listener.clone()); + let gateway_svc = GatewayService::new(settings, pool.clone(), region_map.clone(), auth_cache.clone())?; + let route_svc = RouteService::new( + settings, + auth_cache.clone(), + pool.clone(), + shutdown_listener.clone(), + )?; let org_svc = OrgService::new( + settings, auth_cache.clone(), pool.clone(), - settings.network, route_svc.clone_update_channel(), - ); + )?; let admin_svc = AdminService::new( settings, auth_cache.clone(), + auth_updater, pool.clone(), region_map.clone(), + region_updater, )?; let session_key_filter_svc = SessionKeyFilterService::new( + settings, auth_cache.clone(), pool.clone(), shutdown_listener.clone(), - ); + )?; let server = transport::Server::builder() .http2_keepalive_interval(Some(Duration::from_secs(250))) diff --git a/iot_config/src/org_service.rs b/iot_config/src/org_service.rs index a36d4569a..64ff478f7 100644 --- a/iot_config/src/org_service.rs +++ b/iot_config/src/org_service.rs @@ -2,15 +2,19 @@ use crate::{ admin::{AuthCache, KeyType}, lora_field, org, route::list_routes, - GrpcResult, HELIUM_NET_ID, + GrpcResult, Settings, HELIUM_NET_ID, }; use anyhow::Result; -use file_store::traits::MsgVerify; -use helium_crypto::{Network, PublicKey}; -use helium_proto::services::iot_config::{ - self, route_stream_res_v1, ActionV1, OrgCreateHeliumReqV1, OrgCreateRoamerReqV1, - OrgDisableReqV1, OrgDisableResV1, OrgEnableReqV1, OrgEnableResV1, OrgGetReqV1, OrgListReqV1, - OrgListResV1, OrgResV1, OrgV1, RouteStreamResV1, +use chrono::Utc; +use file_store::traits::{MsgVerify, TimestampEncode}; +use helium_crypto::{Keypair, Network, PublicKey, Sign}; +use helium_proto::{ + services::iot_config::{ + self, route_stream_res_v1, ActionV1, OrgCreateHeliumReqV1, OrgCreateRoamerReqV1, + OrgDisableReqV1, OrgDisableResV1, OrgEnableReqV1, OrgEnableResV1, OrgGetReqV1, + OrgListReqV1, OrgListResV1, OrgResV1, OrgV1, RouteStreamResV1, + }, + Message, }; use sqlx::{Pool, Postgres}; use tokio::sync::broadcast::Sender; @@ -21,21 +25,23 @@ pub struct OrgService { pool: Pool, required_network: Network, route_update_tx: Sender, + signing_key: Keypair, } impl OrgService { pub fn new( + settings: &Settings, auth_cache: AuthCache, pool: Pool, - required_network: Network, route_update_tx: Sender, - ) -> Self { - Self { + ) -> Result { + Ok(Self { auth_cache, pool, - required_network, + required_network: settings.network, route_update_tx, - } + signing_key: settings.signing_keypair()?, + }) } fn verify_network(&self, public_key: PublicKey) -> Result { @@ -54,16 +60,28 @@ impl OrgService { .map_err(|_| Status::invalid_argument(format!("invalid public key: {bytes:?}"))) } - async fn verify_request_signature(&self, request: &R) -> Result<(), Status> + async fn verify_request_signature( + &self, + signer: &PublicKey, + request: &R, + ) -> Result<(), Status> where R: MsgVerify, { self.auth_cache - .verify_signature(KeyType::Administrator, request) - .await + .verify_signature_with_type(KeyType::Administrator, signer, request) .map_err(|_| Status::permission_denied("invalid admin signature"))?; Ok(()) } + + fn sign_response(&self, response: &R) -> Result, Status> + where + R: Message, + { + self.signing_key + .sign(&response.encode_to_vec()) + .map_err(|_| Status::internal("response signing error")) + } } #[tonic::async_trait] @@ -76,7 +94,15 @@ impl iot_config::Org for OrgService { .map(|org| org.into()) .collect(); - Ok(Response::new(OrgListResV1 { orgs: proto_orgs })) + let mut resp = OrgListResV1 { + orgs: proto_orgs, + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; + + Ok(Response::new(resp)) } async fn get(&self, request: Request) -> GrpcResult { @@ -100,7 +126,7 @@ impl iot_config::Org for OrgService { }) .map_err(|err| Status::internal(format!("net id error: {err}")))?; - Ok(Response::new(OrgResV1 { + let mut resp = OrgResV1 { org: Some(org.org.into()), net_id: net_id.into(), devaddr_constraints: org @@ -108,13 +134,20 @@ impl iot_config::Org for OrgService { .into_iter() .map(|constraint| constraint.into()) .collect(), - })) + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; + + Ok(Response::new(resp)) } async fn create_helium(&self, request: Request) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request).await?; + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request).await?; let mut verify_keys: Vec<&[u8]> = vec![request.owner.as_ref(), request.payer.as_ref()]; let mut verify_delegates: Vec<&[u8]> = request @@ -169,17 +202,24 @@ impl iot_config::Org for OrgService { Status::internal("org constraints save failed") })?; - Ok(Response::new(OrgResV1 { + let mut resp = OrgResV1 { org: Some(org.into()), net_id: HELIUM_NET_ID.into(), devaddr_constraints: vec![devaddr_constraint.into()], - })) + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; + + Ok(Response::new(resp)) } async fn create_roamer(&self, request: Request) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request).await?; + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request).await?; let mut verify_keys: Vec<&[u8]> = vec![request.owner.as_ref(), request.payer.as_ref()]; let mut verify_delegates: Vec<&[u8]> = request @@ -229,17 +269,24 @@ impl iot_config::Org for OrgService { Status::internal("org constraints save failed") })?; - Ok(Response::new(OrgResV1 { + let mut resp = OrgResV1 { org: Some(org.into()), net_id: net_id.into(), devaddr_constraints: vec![devaddr_range.into()], - })) + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; + + Ok(Response::new(resp)) } async fn disable(&self, request: Request) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request).await?; + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request).await?; if !org::is_locked(request.oui, &self.pool) .await @@ -266,16 +313,19 @@ impl iot_config::Org for OrgService { )) })?; + let timestamp = Utc::now().encode_timestamp(); + let signer: Vec = self.signing_key.public_key().into(); for route in org_routes { let route_id = route.id.clone(); - if self - .route_update_tx - .send(RouteStreamResV1 { - action: ActionV1::Add.into(), - data: Some(route_stream_res_v1::Data::Route(route.into())), - }) - .is_err() - { + let mut update = RouteStreamResV1 { + action: ActionV1::Add.into(), + data: Some(route_stream_res_v1::Data::Route(route.into())), + timestamp, + signer: signer.clone(), + signature: vec![], + }; + update.signature = self.sign_response(&update)?; + if self.route_update_tx.send(update).is_err() { tracing::info!( route_id = route_id, "all subscribers disconnected; route disable incomplete" @@ -286,13 +336,22 @@ impl iot_config::Org for OrgService { } } - Ok(Response::new(OrgDisableResV1 { oui: request.oui })) + let mut resp = OrgDisableResV1 { + oui: request.oui, + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; + + Ok(Response::new(resp)) } async fn enable(&self, request: Request) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request).await?; + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request).await?; if org::is_locked(request.oui, &self.pool) .await @@ -319,16 +378,19 @@ impl iot_config::Org for OrgService { )) })?; + let timestamp = Utc::now().encode_timestamp(); + let signer: Vec = self.signing_key.public_key().into(); for route in org_routes { let route_id = route.id.clone(); - if self - .route_update_tx - .send(RouteStreamResV1 { - action: ActionV1::Add.into(), - data: Some(route_stream_res_v1::Data::Route(route.into())), - }) - .is_err() - { + let mut update = RouteStreamResV1 { + action: ActionV1::Add.into(), + data: Some(route_stream_res_v1::Data::Route(route.into())), + timestamp, + signer: signer.clone(), + signature: vec![], + }; + update.signature = self.sign_response(&update)?; + if self.route_update_tx.send(update).is_err() { tracing::info!( route_id = route_id, "all subscribers disconnected; route enable incomplete" @@ -339,6 +401,14 @@ impl iot_config::Org for OrgService { } } - Ok(Response::new(OrgEnableResV1 { oui: request.oui })) + let mut resp = OrgEnableResV1 { + oui: request.oui, + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; + + Ok(Response::new(resp)) } } diff --git a/iot_config/src/region_map.rs b/iot_config/src/region_map.rs index a299e7ccb..dcf8a3abb 100644 --- a/iot_config/src/region_map.rs +++ b/iot_config/src/region_map.rs @@ -1,57 +1,65 @@ +use anyhow::anyhow; use futures::stream::TryStreamExt; use helium_proto::{BlockchainRegionParamsV1, Message, Region}; use hextree::{compaction::EqCompactor, Cell, HexTreeMap}; use libflate::gzip::Decoder; -use std::{collections::HashMap, io::Read, str::FromStr, sync::Arc}; -use tokio::sync::RwLock; +use std::{collections::HashMap, io::Read, str::FromStr}; +use tokio::sync::watch; #[derive(Clone, Debug)] pub struct RegionMap { - region_hextree: Arc>>, - params_hashmap: Arc>>, + region_hextree: HexTreeMap, + params_map: HashMap, +} + +#[derive(Clone, Debug)] +pub struct RegionMapReader { + map_receiver: watch::Receiver, +} + +impl RegionMapReader { + pub async fn new(db: impl sqlx::PgExecutor<'_> + Copy) -> anyhow::Result<(watch::Sender, Self)> { + let region_map = RegionMap::new(db).await?; + let (map_sender, map_receiver) = watch::channel(region_map); + Ok((map_sender, Self { map_receiver })) + } + + pub fn get_region(&self, location: Cell) -> Option { + self.map_receiver.borrow().get_region(location) + } + + pub fn get_params(&self, region: &Region) -> Option { + self.map_receiver.borrow().get_params(region) + } } impl RegionMap { - pub async fn new(db: impl sqlx::PgExecutor<'_> + Copy) -> Result { + pub async fn new(db: impl sqlx::PgExecutor<'_> + Copy) -> anyhow::Result { let region_hextree = build_region_tree(db).await?; let params_map = build_params_map(db).await?; Ok(Self { - region_hextree: Arc::new(RwLock::new(region_hextree)), - params_hashmap: Arc::new(RwLock::new(params_map)), + region_hextree, + params_map, }) } - pub async fn get_region(&self, location: Cell) -> Option { - self.region_hextree.read().await.get(location).cloned() + pub fn get_region(&self, location: Cell) -> Option { + self.region_hextree.get(location).cloned() } - pub async fn get_params(&self, region: &Region) -> Option { - self.params_hashmap.read().await.get(region).cloned() + pub fn get_params(&self, region: &Region) -> Option { + self.params_map.get(region).cloned() } - pub async fn insert_params(&self, region: Region, params: BlockchainRegionParamsV1) { - _ = self.params_hashmap.write().await.insert(region, params) + pub fn insert_params(&mut self, region: Region, params: BlockchainRegionParamsV1) { + _ = self.params_map.insert(region, params) } - pub async fn replace_tree(&self, new_map: HexTreeMap) { - *self.region_hextree.write().await = new_map + pub fn replace_tree(&mut self, new_map: HexTreeMap) { + self.region_hextree = new_map } } -#[derive(thiserror::Error, Debug)] -pub enum RegionMapError { - #[error("region map database error")] - DbFetch(#[from] sqlx::Error), - #[error("params decode error")] - ParamsDecode(#[from] helium_proto::DecodeError), - #[error("indices gunzip error")] - IdxGunzip(#[from] std::io::Error), - #[error("malformed region h3 index list")] - MalformedH3Indexes, - #[error("unsupported region error: {0}")] - UnsupportedRegion(i32), -} - #[derive(sqlx::FromRow)] pub struct HexRegion { pub region: String, @@ -61,7 +69,7 @@ pub struct HexRegion { pub async fn build_region_tree( db: impl sqlx::PgExecutor<'_>, -) -> Result, RegionMapError> { +) -> anyhow::Result> { let mut region_tree = HexTreeMap::with_compactor(EqCompactor); let mut regions = sqlx::query_as::<_, HexRegion>("select * from regions").fetch(db); @@ -75,7 +83,7 @@ pub async fn build_region_tree( if raw_h3_indices.len() % std::mem::size_of::() != 0 { tracing::error!("h3 index list malformed; indices are not an index-byte-size multiple; region: {region}"); - return Err(RegionMapError::MalformedH3Indexes); + return Err(anyhow!("malformed h3 indices")); } let mut h3_idx_buf = [0_u8; 8]; @@ -88,7 +96,7 @@ pub async fn build_region_tree( tracing::error!( "h3 index list malformed; region, chunk, bits: {region}, {chunk_num}, {h3_idx:x}" ); - return Err(RegionMapError::MalformedH3Indexes); + return Err(anyhow!("malformed h3 indices")); } } } @@ -100,7 +108,7 @@ pub async fn build_region_tree( pub async fn build_params_map( db: impl sqlx::PgExecutor<'_>, -) -> Result, RegionMapError> { +) -> anyhow::Result> { let mut params_map: HashMap = HashMap::new(); let mut regions = sqlx::query_as::<_, HexRegion>("select * from regions").fetch(db); @@ -126,7 +134,7 @@ pub async fn update_region( params: &BlockchainRegionParamsV1, indexes: Option<&[u8]>, db: impl sqlx::PgExecutor<'_> + sqlx::Acquire<'_, Database = sqlx::Postgres> + Copy, -) -> Result>, RegionMapError> { +) -> anyhow::Result>> { let mut transaction = db.begin().await?; sqlx::query( diff --git a/iot_config/src/route.rs b/iot_config/src/route.rs index fd58bbcbd..2b73ac9ce 100644 --- a/iot_config/src/route.rs +++ b/iot_config/src/route.rs @@ -2,12 +2,20 @@ use crate::{ broadcast_update, lora_field::{DevAddrRange, EuiPair, NetIdField}, }; -use futures::stream::{self, Stream, StreamExt, TryStreamExt}; +use anyhow::anyhow; +use chrono::Utc; +use file_store::traits::TimestampEncode; +use futures::{ + future::TryFutureExt, + stream::{self, Stream, StreamExt, TryStreamExt}, +}; +use helium_crypto::{Keypair, Sign}; +use helium_proto::Message; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::{types::Uuid, Row}; -use std::collections::BTreeMap; -use tokio::sync::broadcast::{error::SendError, Sender}; +use std::{collections::BTreeMap, sync::Arc}; +use tokio::sync::broadcast::Sender; pub mod proto { pub use helium_proto::{ @@ -16,7 +24,7 @@ pub mod proto { ActionV1, ProtocolGwmpMappingV1, ProtocolGwmpV1, ProtocolHttpRoamingV1, ProtocolPacketRouterV1, RouteStreamResV1, RouteV1, ServerV1, }, - Region, + Message, Region, }; } @@ -73,22 +81,21 @@ pub struct StorageRoute { #[derive(thiserror::Error, Debug)] pub enum RouteStorageError { #[error("db persist failed: {0}")] - PersistError(#[from] sqlx::Error), + StorageError(#[from] sqlx::Error), #[error("uuid parse error: {0}")] UuidParse(#[from] sqlx::types::uuid::Error), #[error("protocol serialize error: {0}")] ProtocolSerde(#[from] serde_json::Error), #[error("protocol error: {0}")] ServerProtocol(String), - #[error("stream update error: {0}")] - StreamUpdate(#[from] Box>), } pub async fn create_route( route: Route, db: impl sqlx::PgExecutor<'_> + sqlx::Acquire<'_, Database = sqlx::Postgres> + Copy, + signing_key: &Keypair, update_tx: Sender, -) -> Result { +) -> anyhow::Result { let net_id: i32 = route.net_id.into(); let protocol_opts = route .server @@ -123,12 +130,26 @@ pub async fn create_route( transaction.commit().await?; if new_route.active && !new_route.locked { - _ = update_tx.send(proto::RouteStreamResV1 { + let timestamp = Utc::now().encode_timestamp(); + let signer = signing_key.public_key().into(); + let mut update = proto::RouteStreamResV1 { action: proto::ActionV1::Add.into(), data: Some(proto::route_stream_res_v1::Data::Route( new_route.clone().into(), )), - }); + timestamp, + signer, + signature: vec![], + }; + signing_key + .sign(&update.encode_to_vec()) + .map_err(|err| anyhow!(format!("error signing route stream response: {err:?}"))) + .and_then(|signature| { + update.signature = signature; + update_tx.send(update).map_err(|err| { + anyhow!(format!("error broadcasting route stream response: {err:?}")) + }) + })?; }; Ok(new_route) @@ -137,8 +158,9 @@ pub async fn create_route( pub async fn update_route( route: Route, db: impl sqlx::PgExecutor<'_> + sqlx::Acquire<'_, Database = sqlx::Postgres> + Copy, + signing_key: &Keypair, update_tx: Sender, -) -> Result { +) -> anyhow::Result { let protocol_opts = route .server .protocol @@ -170,12 +192,27 @@ pub async fn update_route( transaction.commit().await?; - _ = update_tx.send(proto::RouteStreamResV1 { + let timestamp = Utc::now().encode_timestamp(); + let signer = signing_key.public_key().into(); + let mut update_res = proto::RouteStreamResV1 { action: proto::ActionV1::Add.into(), data: Some(proto::route_stream_res_v1::Data::Route( updated_route.clone().into(), )), - }); + timestamp, + signer, + signature: vec![], + }; + + _ = signing_key + .sign(&update_res.encode_to_vec()) + .map_err(|err| anyhow!(format!("error signing route stream response: {err:?}"))) + .and_then(|signature| { + update_res.signature = signature; + update_tx.send(update_res).map_err(|err| { + anyhow!(format!("error broadcasting route stream response: {err:?}")) + }) + }); Ok(updated_route) } @@ -183,7 +220,7 @@ pub async fn update_route( async fn insert_euis( euis: &[EuiPair], db: impl sqlx::PgExecutor<'_>, -) -> Result, RouteStorageError> { +) -> anyhow::Result> { if euis.is_empty() { return Ok(vec![]); } @@ -213,7 +250,7 @@ async fn insert_euis( async fn remove_euis( euis: &[EuiPair], db: impl sqlx::PgExecutor<'_>, -) -> Result, RouteStorageError> { +) -> anyhow::Result> { if euis.is_empty() { return Ok(vec![]); } @@ -244,8 +281,9 @@ pub async fn update_euis( to_add: &[EuiPair], to_remove: &[EuiPair], db: impl sqlx::PgExecutor<'_> + sqlx::Acquire<'_, Database = sqlx::Postgres> + Copy, + signing_key: Arc, update_tx: Sender, -) -> Result<(), RouteStorageError> { +) -> anyhow::Result<()> { let mut transaction = db.begin().await?; let added_euis: Vec<(EuiPair, proto::ActionV1)> = insert_euis(to_add, &mut transaction) @@ -263,16 +301,25 @@ pub async fn update_euis( transaction.commit().await?; tokio::spawn(async move { + let timestamp = Utc::now().encode_timestamp(); + let signer: Vec = signing_key.public_key().into(); stream::iter([added_euis, removed_euis].concat()) .map(Ok) .try_for_each(|(update, action)| { - broadcast_update::( - proto::RouteStreamResV1 { - action: i32::from(action), - data: Some(proto::route_stream_res_v1::Data::EuiPair(update.into())), - }, - update_tx.clone(), - ) + let mut update_res = proto::RouteStreamResV1 { + action: i32::from(action), + data: Some(proto::route_stream_res_v1::Data::EuiPair(update.into())), + timestamp, + signer: signer.clone(), + signature: vec![], + }; + futures::future::ready(signing_key.sign(&update_res.encode_to_vec())) + .map_err(|_| anyhow!("failed signing eui pair update")) + .and_then(|signature| { + update_res.signature = signature; + broadcast_update::(update_res, update_tx.clone()) + .map_err(|_| anyhow!("failed broadcasting eui pair update")) + }) }) .await }); @@ -283,7 +330,7 @@ pub async fn update_euis( async fn insert_devaddr_ranges( ranges: &[DevAddrRange], db: impl sqlx::PgExecutor<'_>, -) -> Result, RouteStorageError> { +) -> anyhow::Result> { if ranges.is_empty() { return Ok(vec![]); } @@ -314,7 +361,7 @@ async fn insert_devaddr_ranges( async fn remove_devaddr_ranges( ranges: &[DevAddrRange], db: impl sqlx::PgExecutor<'_>, -) -> Result, RouteStorageError> { +) -> anyhow::Result> { if ranges.is_empty() { return Ok(vec![]); } @@ -346,8 +393,9 @@ pub async fn update_devaddr_ranges( to_add: &[DevAddrRange], to_remove: &[DevAddrRange], db: impl sqlx::PgExecutor<'_> + sqlx::Acquire<'_, Database = sqlx::Postgres> + Copy, + signing_key: Arc, update_tx: Sender, -) -> Result<(), RouteStorageError> { +) -> anyhow::Result<()> { let mut transaction = db.begin().await?; let added_devaddrs: Vec<(DevAddrRange, proto::ActionV1)> = @@ -367,18 +415,28 @@ pub async fn update_devaddr_ranges( transaction.commit().await?; tokio::spawn(async move { + let timestamp = Utc::now().encode_timestamp(); + let signer: Vec = signing_key.public_key().into(); stream::iter([added_devaddrs, removed_devaddrs].concat()) .map(Ok) .try_for_each(|(update, action)| { - broadcast_update::( - proto::RouteStreamResV1 { - action: i32::from(action), - data: Some(proto::route_stream_res_v1::Data::DevaddrRange( - update.into(), - )), - }, - update_tx.clone(), - ) + let mut devaddr_res = proto::RouteStreamResV1 { + action: i32::from(action), + data: Some(proto::route_stream_res_v1::Data::DevaddrRange( + update.into(), + )), + timestamp, + signer: signer.clone(), + signature: vec![], + }; + + futures::future::ready(signing_key.sign(&devaddr_res.encode_to_vec())) + .map_err(|_| anyhow!("failed to sign devaddr range update")) + .and_then(|signature| { + devaddr_res.signature = signature; + broadcast_update::(devaddr_res, update_tx.clone()) + .map_err(|_| anyhow!("failed to broadcast devaddr range update")) + }) }) .await }); @@ -386,10 +444,7 @@ pub async fn update_devaddr_ranges( Ok(()) } -pub async fn list_routes( - oui: u64, - db: impl sqlx::PgExecutor<'_>, -) -> Result, RouteStorageError> { +pub async fn list_routes(oui: u64, db: impl sqlx::PgExecutor<'_>) -> anyhow::Result> { Ok(sqlx::query_as::<_, StorageRoute>( r#" select r.id, r.oui, r.net_id, r.max_copies, r.server_host, r.server_port, r.server_protocol_opts, r.active, o.locked @@ -507,10 +562,7 @@ pub fn devaddr_range_stream<'a>( .boxed() } -pub async fn get_route( - id: &str, - db: impl sqlx::PgExecutor<'_>, -) -> Result { +pub async fn get_route(id: &str, db: impl sqlx::PgExecutor<'_>) -> anyhow::Result { let uuid = Uuid::try_parse(id)?; let route_row = sqlx::query_as::<_, StorageRoute>( r#" @@ -548,8 +600,9 @@ pub async fn get_route( pub async fn delete_route( id: &str, db: impl sqlx::PgExecutor<'_> + sqlx::Acquire<'_, Database = sqlx::Postgres> + Copy, + signing_key: &Keypair, update_tx: Sender, -) -> Result<(), RouteStorageError> { +) -> anyhow::Result<()> { let uuid = Uuid::try_parse(id)?; let mut transaction = db.begin().await?; @@ -567,12 +620,27 @@ pub async fn delete_route( transaction.commit().await?; - _ = update_tx.send(proto::RouteStreamResV1 { + let timestamp = Utc::now().encode_timestamp(); + let signer = signing_key.public_key().into(); + let mut delete_res = proto::RouteStreamResV1 { action: proto::ActionV1::Remove.into(), data: Some(proto::route_stream_res_v1::Data::Route( route.clone().into(), )), - }); + timestamp, + signer, + signature: vec![], + }; + + _ = signing_key + .sign(&delete_res.encode_to_vec()) + .map_err(|_| anyhow!("failed to sign route delete update")) + .and_then(|signature| { + delete_res.signature = signature; + update_tx + .send(delete_res) + .map_err(|_| anyhow!("failed to broadcast route delete update")) + }); Ok(()) } diff --git a/iot_config/src/route_service.rs b/iot_config/src/route_service.rs index 25bfc2003..db21b6966 100644 --- a/iot_config/src/route_service.rs +++ b/iot_config/src/route_service.rs @@ -3,23 +3,28 @@ use crate::{ lora_field::{DevAddrConstraint, DevAddrRange, EuiPair}, org::{self, DbOrgError}, route::{self, Route, RouteStorageError}, - update_channel, GrpcResult, GrpcStreamRequest, GrpcStreamResult, + update_channel, GrpcResult, GrpcStreamRequest, GrpcStreamResult, Settings, }; use anyhow::{anyhow, Result}; -use file_store::traits::MsgVerify; +use chrono::Utc; +use file_store::traits::{MsgVerify, TimestampEncode}; use futures::{ future::TryFutureExt, stream::{StreamExt, TryStreamExt}, }; -use helium_crypto::PublicKey; -use helium_proto::services::iot_config::{ - self, route_stream_res_v1, ActionV1, DevaddrRangeV1, EuiPairV1, RouteCreateReqV1, - RouteDeleteReqV1, RouteDevaddrRangesResV1, RouteEuisResV1, RouteGetDevaddrRangesReqV1, - RouteGetEuisReqV1, RouteGetReqV1, RouteListReqV1, RouteListResV1, RouteStreamReqV1, - RouteStreamResV1, RouteUpdateDevaddrRangesReqV1, RouteUpdateEuisReqV1, RouteUpdateReqV1, - RouteV1, +use helium_crypto::{Keypair, PublicKey, Sign}; +use helium_proto::{ + services::iot_config::{ + self, route_stream_res_v1, ActionV1, DevaddrRangeV1, EuiPairV1, RouteCreateReqV1, + RouteDeleteReqV1, RouteDevaddrRangesResV1, RouteEuisResV1, RouteGetDevaddrRangesReqV1, + RouteGetEuisReqV1, RouteGetReqV1, RouteListReqV1, RouteListResV1, RouteResV1, + RouteStreamReqV1, RouteStreamResV1, RouteUpdateDevaddrRangesReqV1, RouteUpdateEuisReqV1, + RouteUpdateReqV1, RouteV1, + }, + Message, }; use sqlx::{Pool, Postgres}; +use std::sync::Arc; use tokio::sync::{broadcast, mpsc}; use tonic::{Request, Response, Status}; @@ -30,6 +35,7 @@ pub struct RouteService { pool: Pool, update_channel: broadcast::Sender, shutdown: triggered::Listener, + signing_key: Arc, } #[derive(Clone, Debug)] @@ -39,13 +45,19 @@ enum OrgId<'a> { } impl RouteService { - pub fn new(auth_cache: AuthCache, pool: Pool, shutdown: triggered::Listener) -> Self { - Self { + pub fn new( + settings: &Settings, + auth_cache: AuthCache, + pool: Pool, + shutdown: triggered::Listener, + ) -> Result { + Ok(Self { auth_cache, pool, update_channel: update_channel(), shutdown, - } + signing_key: Arc::new(settings.signing_keypair()?), + }) } fn subscribe_to_routes(&self) -> broadcast::Receiver { @@ -58,6 +70,7 @@ impl RouteService { async fn verify_request_signature<'a, R>( &self, + signer: &PublicKey, request: &R, id: OrgId<'a>, ) -> Result<(), Status> @@ -66,11 +79,10 @@ impl RouteService { { if self .auth_cache - .verify_signature(KeyType::Administrator, request) - .await + .verify_signature_with_type(KeyType::Administrator, signer, request) .is_ok() { - tracing::debug!("request authorized by admin"); + tracing::debug!(signer = signer.to_string(), "request authorized by admin"); return Ok(()); } @@ -80,49 +92,54 @@ impl RouteService { } .map_err(|_| Status::internal("auth verification error"))?; - for pubkey in org_keys.iter() { - if request.verify(pubkey).is_ok() { - tracing::debug!( - pubkey = pubkey.to_string(), - "request authorized by delegate" - ); - return Ok(()); - } + if org_keys.as_slice().contains(signer) && request.verify(signer).is_ok() { + tracing::debug!( + signer = signer.to_string(), + "request authorized by delegate" + ); + return Ok(()); } + Err(Status::permission_denied("unauthorized request signature")) } - async fn verify_stream_request_signature(&self, request: &R) -> Result<(), Status> + fn verify_stream_request_signature( + &self, + signer: &PublicKey, + request: &R, + ) -> Result<(), Status> where R: MsgVerify, { - if self - .auth_cache - .verify_signature(KeyType::PacketRouter, request) - .await - .is_ok() - { - tracing::debug!("request authorized for registered packet router"); - Ok(()) - } else if self - .auth_cache - .verify_signature(KeyType::Administrator, request) - .await - .is_ok() - { - tracing::debug!("request authorized by admin"); + if self.auth_cache.verify_signature(signer, request).is_ok() { + tracing::debug!(signer = signer.to_string(), "request authorized"); Ok(()) } else { Err(Status::permission_denied("unauthorized request signature")) } } + fn verify_public_key(&self, bytes: &[u8]) -> Result { + PublicKey::try_from(bytes) + .map_err(|_| Status::invalid_argument(format!("invalid public key: {bytes:?}"))) + } + + fn sign_response(&self, response: &R) -> Result, Status> + where + R: Message, + { + self.signing_key + .sign(&response.encode_to_vec()) + .map_err(|_| Status::internal("response signing error")) + } + async fn update_validator( &self, route_id: &str, check_constraints: bool, ) -> Result { - let admin_keys = self.auth_cache.get_keys(KeyType::Administrator).await; + // let admin_keys = self.auth_cache.get_keys().into_iter().filter(|(_, keytype)| keytype == &KeyType::Administrator).map(|(pubkey, _)| pubkey).collect(); + let admin_keys = self.auth_cache.get_keys_by_type(KeyType::Administrator); DevAddrEuiValidator::new(route_id, admin_keys, &self.pool, check_constraints).await } @@ -133,7 +150,8 @@ impl iot_config::Route for RouteService { async fn list(&self, request: Request) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request, OrgId::Oui(request.oui)) + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request, OrgId::Oui(request.oui)) .await?; tracing::debug!(org = request.oui, "list routes"); @@ -145,15 +163,22 @@ impl iot_config::Route for RouteService { .map(|route| route.into()) .collect(); - Ok(Response::new(RouteListResV1 { + let mut resp = RouteListResV1 { routes: proto_routes, - })) + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; + + Ok(Response::new(resp)) } - async fn get(&self, request: Request) -> GrpcResult { + async fn get(&self, request: Request) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request, OrgId::RouteId(&request.id)) + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request, OrgId::RouteId(&request.id)) .await?; tracing::debug!(route_id = request.id, "get route"); @@ -165,13 +190,22 @@ impl iot_config::Route for RouteService { Status::internal("fetch route failed") })?; - Ok(Response::new(route.into())) + let mut resp = RouteResV1 { + route: Some(route.into()), + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; + + Ok(Response::new(resp)) } - async fn create(&self, request: Request) -> GrpcResult { + async fn create(&self, request: Request) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request, OrgId::Oui(request.oui)) + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request, OrgId::Oui(request.oui)) .await?; let route: Route = request @@ -192,17 +226,30 @@ impl iot_config::Route for RouteService { )); } - let new_route: Route = route::create_route(route, &self.pool, self.clone_update_channel()) - .await - .map_err(|err| { - tracing::error!("route create failed {err:?}"); - Status::internal("route create failed") - })?; + let new_route: Route = route::create_route( + route, + &self.pool, + &self.signing_key, + self.clone_update_channel(), + ) + .await + .map_err(|err| { + tracing::error!("route create failed {err:?}"); + Status::internal("route create failed") + })?; + + let mut resp = RouteResV1 { + route: Some(new_route.into()), + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; - Ok(Response::new(new_route.into())) + Ok(Response::new(resp)) } - async fn update(&self, request: Request) -> GrpcResult { + async fn update(&self, request: Request) -> GrpcResult { let request = request.into_inner(); let route: Route = request @@ -217,23 +264,38 @@ impl iot_config::Route for RouteService { "route update {route:?}" ); - self.verify_request_signature(&request, OrgId::Oui(route.oui)) + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request, OrgId::Oui(route.oui)) .await?; - let updated_route = route::update_route(route, &self.pool, self.clone_update_channel()) - .await - .map_err(|err| { - tracing::error!("route update failed {err:?}"); - Status::internal("update route failed") - })?; + let updated_route = route::update_route( + route, + &self.pool, + &self.signing_key, + self.clone_update_channel(), + ) + .await + .map_err(|err| { + tracing::error!("route update failed {err:?}"); + Status::internal("update route failed") + })?; + + let mut resp = RouteResV1 { + route: Some(updated_route.into()), + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; - Ok(Response::new(updated_route.into())) + Ok(Response::new(resp)) } - async fn delete(&self, request: Request) -> GrpcResult { + async fn delete(&self, request: Request) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request, OrgId::RouteId(&request.id)) + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request, OrgId::RouteId(&request.id)) .await?; tracing::debug!(route_id = request.id, "route delete"); @@ -242,33 +304,48 @@ impl iot_config::Route for RouteService { .await .map_err(|_| Status::internal("fetch route failed"))?; - route::delete_route(&request.id, &self.pool, self.clone_update_channel()) - .await - .map_err(|err| { - tracing::error!("route delete failed {err:?}"); - Status::internal("delete route failed") - })?; + route::delete_route( + &request.id, + &self.pool, + &self.signing_key, + self.clone_update_channel(), + ) + .await + .map_err(|err| { + tracing::error!("route delete failed {err:?}"); + Status::internal("delete route failed") + })?; + + let mut resp = RouteResV1 { + route: Some(route.into()), + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; - Ok(Response::new(route.into())) + Ok(Response::new(resp)) } type streamStream = GrpcStreamResult; async fn stream(&self, request: Request) -> GrpcResult { let request = request.into_inner(); - self.verify_stream_request_signature(&request).await?; + let signer = self.verify_public_key(&request.signer)?; + self.verify_stream_request_signature(&signer, &request)?; tracing::info!("client subscribed to route stream"); let pool = self.pool.clone(); let shutdown_listener = self.shutdown.clone(); let (tx, rx) = tokio::sync::mpsc::channel(20); + let signing_key = self.signing_key.clone(); let mut route_updates = self.subscribe_to_routes(); tokio::spawn(async move { - if stream_existing_routes(&pool, tx.clone()) - .and_then(|_| stream_existing_euis(&pool, tx.clone())) - .and_then(|_| stream_existing_devaddrs(&pool, tx.clone())) + if stream_existing_routes(&pool, &signing_key, tx.clone()) + .and_then(|_| stream_existing_euis(&pool, &signing_key, tx.clone())) + .and_then(|_| stream_existing_devaddrs(&pool, &signing_key, tx.clone())) .await .is_err() { @@ -300,7 +377,8 @@ impl iot_config::Route for RouteService { ) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request, OrgId::RouteId(&request.route_id)) + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request, OrgId::RouteId(&request.route_id)) .await?; let pool = self.pool.clone(); @@ -387,12 +465,18 @@ impl iot_config::Route for RouteService { removing = to_remove.len(), "updating eui pairs", ); - route::update_euis(&to_add, &to_remove, &self.pool, self.update_channel.clone()) - .await - .map_err(|err| { - tracing::error!("eui pair update failed: {err:?}"); - Status::internal("eui pair update failed") - })?; + route::update_euis( + &to_add, + &to_remove, + &self.pool, + self.signing_key.clone(), + self.update_channel.clone(), + ) + .await + .map_err(|err| { + tracing::error!("eui pair update failed: {err:?}"); + Status::internal("eui pair update failed") + })?; to_add = vec![]; to_remove = vec![]; pending_updates = 0; @@ -406,14 +490,27 @@ impl iot_config::Route for RouteService { "updating euis", ); - route::update_euis(&to_add, &to_remove, &self.pool, self.clone_update_channel()) - .await - .map_err(|err| { - tracing::error!("eui update failed: {err:?}"); - Status::internal("eui update failed") - })?; + route::update_euis( + &to_add, + &to_remove, + &self.pool, + self.signing_key.clone(), + self.clone_update_channel(), + ) + .await + .map_err(|err| { + tracing::error!("eui update failed: {err:?}"); + Status::internal("eui update failed") + })?; } - Ok(Response::new(RouteEuisResV1 {})) + let mut resp = RouteEuisResV1 { + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; + + Ok(Response::new(resp)) } type get_devaddr_rangesStream = GrpcStreamResult; @@ -423,7 +520,8 @@ impl iot_config::Route for RouteService { ) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request, OrgId::RouteId(&request.route_id)) + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request, OrgId::RouteId(&request.route_id)) .await?; let (tx, rx) = tokio::sync::mpsc::channel(20); @@ -515,6 +613,7 @@ impl iot_config::Route for RouteService { &to_add, &to_remove, &self.pool, + self.signing_key.clone(), self.update_channel.clone(), ) .await @@ -539,6 +638,7 @@ impl iot_config::Route for RouteService { &to_add, &to_remove, &self.pool, + self.signing_key.clone(), self.update_channel.clone(), ) .await @@ -547,7 +647,14 @@ impl iot_config::Route for RouteService { Status::internal("devaddr range update failed") })?; } - Ok(Response::new(RouteDevaddrRangesResV1 {})) + let mut resp = RouteDevaddrRangesResV1 { + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp)?; + + Ok(Response::new(resp)) } } @@ -709,14 +816,27 @@ where async fn stream_existing_routes( pool: &Pool, + signing_key: &Keypair, tx: mpsc::Sender>, ) -> Result<()> { + let timestamp = Utc::now().encode_timestamp(); + let signer: Vec = signing_key.public_key().into(); + let tx = &tx; route::active_route_stream(pool) - .then(|route| { - tx.send(Ok(RouteStreamResV1 { + .then(move |route| { + let mut route_res = RouteStreamResV1 { action: ActionV1::Add.into(), data: Some(route_stream_res_v1::Data::Route(route.into())), - })) + timestamp, + signer: signer.clone(), + signature: vec![], + }; + if let Ok(signature) = signing_key.sign(&route_res.encode_to_vec()) { + route_res.signature = signature; + tx.send(Ok(route_res)) + } else { + tx.send(Err(Status::internal("failed to sign route"))) + } }) .map_err(|err| anyhow!(err)) .try_fold((), |acc, _| async move { Ok(acc) }) @@ -725,14 +845,27 @@ async fn stream_existing_routes( async fn stream_existing_euis( pool: &Pool, + signing_key: &Keypair, tx: mpsc::Sender>, ) -> Result<()> { + let timestamp = Utc::now().encode_timestamp(); + let signer: Vec = signing_key.public_key().into(); + let tx = &tx; route::eui_stream(pool) - .then(|eui_pair| { - tx.send(Ok(RouteStreamResV1 { + .then(move |eui_pair| { + let mut eui_pair_res = RouteStreamResV1 { action: ActionV1::Add.into(), data: Some(route_stream_res_v1::Data::EuiPair(eui_pair.into())), - })) + timestamp, + signer: signer.clone(), + signature: vec![], + }; + if let Ok(signature) = signing_key.sign(&eui_pair_res.encode_to_vec()) { + eui_pair_res.signature = signature; + tx.send(Ok(eui_pair_res)) + } else { + tx.send(Err(Status::internal("failed to sign eui pair"))) + } }) .map_err(|err| anyhow!(err)) .try_fold((), |acc, _| async move { Ok(acc) }) @@ -741,16 +874,29 @@ async fn stream_existing_euis( async fn stream_existing_devaddrs( pool: &Pool, + signing_key: &Keypair, tx: mpsc::Sender>, ) -> Result<()> { + let timestamp = Utc::now().encode_timestamp(); + let signer: Vec = signing_key.public_key().into(); + let tx = &tx; route::devaddr_range_stream(pool) - .then(|devaddr_range| { - tx.send(Ok(RouteStreamResV1 { + .then(move |devaddr_range| { + let mut devaddr_range_res = RouteStreamResV1 { action: ActionV1::Add.into(), data: Some(route_stream_res_v1::Data::DevaddrRange( devaddr_range.into(), )), - })) + timestamp, + signer: signer.clone(), + signature: vec![], + }; + if let Ok(signature) = signing_key.sign(&devaddr_range_res.encode_to_vec()) { + devaddr_range_res.signature = signature; + tx.send(Ok(devaddr_range_res)) + } else { + tx.send(Err(Status::internal("failed to sign devaddr range"))) + } }) .map_err(|err| anyhow!(err)) .try_fold((), |acc, _| async move { Ok(acc) }) diff --git a/iot_config/src/session_key.rs b/iot_config/src/session_key.rs index 96885c5d6..c01d685e5 100644 --- a/iot_config/src/session_key.rs +++ b/iot_config/src/session_key.rs @@ -1,9 +1,18 @@ use crate::{broadcast_update, lora_field::DevAddrField}; -use futures::stream::{self, Stream, StreamExt, TryStreamExt}; -use helium_proto::services::iot_config::{ - ActionV1, SessionKeyFilterStreamResV1, SessionKeyFilterV1, +use anyhow::anyhow; +use chrono::Utc; +use file_store::traits::TimestampEncode; +use futures::{ + future::TryFutureExt, + stream::{self, Stream, StreamExt, TryStreamExt}, +}; +use helium_crypto::{Keypair, Sign}; +use helium_proto::{ + services::iot_config::{ActionV1, SessionKeyFilterStreamResV1, SessionKeyFilterV1}, + Message, }; use sqlx::{postgres::PgRow, FromRow, Row}; +use std::sync::Arc; use tokio::sync::broadcast::Sender; #[derive(Clone, Debug)] @@ -68,6 +77,7 @@ pub async fn update_session_keys( to_add: &[SessionKeyFilter], to_remove: &[SessionKeyFilter], db: impl sqlx::PgExecutor<'_> + sqlx::Acquire<'_, Database = sqlx::Postgres> + Copy, + signing_key: Arc, update_tx: Sender, ) -> Result<(), sqlx::Error> { let mut transaction = db.begin().await?; @@ -89,16 +99,28 @@ pub async fn update_session_keys( transaction.commit().await?; tokio::spawn(async move { + let timestamp = Utc::now().encode_timestamp(); + let signer: Vec = signing_key.public_key().into(); stream::iter([added_updates, removed_updates].concat()) .map(Ok) .try_for_each(|(update, action)| { - broadcast_update::( - SessionKeyFilterStreamResV1 { - action: i32::from(action), - filter: Some(update.into()), - }, - update_tx.clone(), - ) + let mut skf_update = SessionKeyFilterStreamResV1 { + action: i32::from(action), + filter: Some(update.into()), + timestamp, + signer: signer.clone(), + signature: vec![], + }; + futures::future::ready(signing_key.sign(&skf_update.encode_to_vec())) + .map_err(|_| anyhow!("failed to sign session key filter update")) + .and_then(|signature| { + skf_update.signature = signature; + broadcast_update::( + skf_update, + update_tx.clone(), + ) + .map_err(|_| anyhow!("failed to broadcast session key filter update")) + }) }) .await }); diff --git a/iot_config/src/session_key_service.rs b/iot_config/src/session_key_service.rs index 219cf8b8b..005980fef 100644 --- a/iot_config/src/session_key_service.rs +++ b/iot_config/src/session_key_service.rs @@ -3,18 +3,26 @@ use crate::{ lora_field::DevAddrConstraint, org::{self, DbOrgError}, session_key::{self, SessionKeyFilter}, - update_channel, GrpcResult, GrpcStreamRequest, GrpcStreamResult, + update_channel, GrpcResult, GrpcStreamRequest, GrpcStreamResult, Settings, }; use anyhow::{anyhow, Result}; -use file_store::traits::MsgVerify; -use futures::stream::{StreamExt, TryStreamExt}; -use helium_crypto::PublicKey; -use helium_proto::services::iot_config::{ - self, ActionV1, SessionKeyFilterGetReqV1, SessionKeyFilterListReqV1, - SessionKeyFilterStreamReqV1, SessionKeyFilterStreamResV1, SessionKeyFilterUpdateReqV1, - SessionKeyFilterUpdateResV1, SessionKeyFilterV1, +use chrono::Utc; +use file_store::traits::{MsgVerify, TimestampEncode}; +use futures::{ + future::TryFutureExt, + stream::{StreamExt, TryStreamExt}, +}; +use helium_crypto::{Keypair, PublicKey, Sign}; +use helium_proto::{ + services::iot_config::{ + self, ActionV1, SessionKeyFilterGetReqV1, SessionKeyFilterListReqV1, + SessionKeyFilterStreamReqV1, SessionKeyFilterStreamResV1, SessionKeyFilterUpdateReqV1, + SessionKeyFilterUpdateResV1, SessionKeyFilterV1, + }, + Message, }; use sqlx::{Pool, Postgres}; +use std::sync::Arc; use tokio::sync::{broadcast, mpsc}; use tonic::{Request, Response, Status}; @@ -25,16 +33,23 @@ pub struct SessionKeyFilterService { pool: Pool, update_channel: broadcast::Sender, shutdown: triggered::Listener, + signing_key: Arc, } impl SessionKeyFilterService { - pub fn new(auth_cache: AuthCache, pool: Pool, shutdown: triggered::Listener) -> Self { - Self { + pub fn new( + settings: &Settings, + auth_cache: AuthCache, + pool: Pool, + shutdown: triggered::Listener, + ) -> Result { + Ok(Self { auth_cache, pool, update_channel: update_channel(), shutdown, - } + signing_key: Arc::new(settings.signing_keypair()?), + }) } fn subscribe_to_session_keys(&self) -> broadcast::Receiver { @@ -45,17 +60,21 @@ impl SessionKeyFilterService { self.update_channel.clone() } - async fn verify_request_signature<'a, R>(&self, request: &R, id: u64) -> Result<(), Status> + async fn verify_request_signature<'a, R>( + &self, + signer: &PublicKey, + request: &R, + id: u64, + ) -> Result<(), Status> where R: MsgVerify, { if self .auth_cache - .verify_signature(KeyType::Administrator, request) - .await + .verify_signature_with_type(KeyType::Administrator, signer, request) .is_ok() { - tracing::debug!("request authorized by admin"); + tracing::debug!(signer = signer.to_string(), "request authorized by admin"); return Ok(()); } @@ -63,42 +82,48 @@ impl SessionKeyFilterService { .await .map_err(|_| Status::internal("auth verification error"))?; - for pubkey in org_keys.iter() { - if request.verify(pubkey).is_ok() { - tracing::debug!("request authorized by delegate {pubkey}"); - return Ok(()); - } + if org_keys.as_slice().contains(signer) && request.verify(signer).is_ok() { + tracing::debug!( + signer = signer.to_string(), + "request authorized by delegate" + ); + return Ok(()); } Err(Status::permission_denied("unauthorized request signature")) } - async fn verify_stream_request_signature(&self, request: &R) -> Result<(), Status> + fn verify_stream_request_signature( + &self, + signer: &PublicKey, + request: &R, + ) -> Result<(), Status> where R: MsgVerify, { - if self - .auth_cache - .verify_signature(KeyType::PacketRouter, request) - .await - .is_ok() - { - tracing::debug!("request authorized for registered packet router"); - Ok(()) - } else if self - .auth_cache - .verify_signature(KeyType::Administrator, request) - .await - .is_ok() - { - tracing::debug!("request authorized by admin"); + if self.auth_cache.verify_signature(signer, request).is_ok() { + tracing::debug!(signer = signer.to_string(), "request authorized"); Ok(()) } else { Err(Status::permission_denied("unauthorized request signature")) } } + fn verify_public_key(&self, bytes: &[u8]) -> Result { + PublicKey::try_from(bytes) + .map_err(|_| Status::invalid_argument(format!("invalid public key: {bytes:?}"))) + } + + fn sign_response(&self, response: &R) -> Result, Status> + where + R: Message, + { + self.signing_key + .sign(&response.encode_to_vec()) + .map_err(|_| Status::internal("response signing error")) + } + async fn update_validator(&self, oui: u64) -> Result { - let admin_keys = self.auth_cache.get_keys(KeyType::Administrator).await; + let admin_keys = self.auth_cache.get_keys_by_type(KeyType::Administrator); SkfValidator::new(oui, admin_keys, &self.pool).await } @@ -113,7 +138,9 @@ impl iot_config::SessionKeyFilter for SessionKeyFilterService { ) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request, request.oui).await?; + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request, request.oui) + .await?; let pool = self.pool.clone(); let (tx, rx) = tokio::sync::mpsc::channel(20); @@ -141,7 +168,9 @@ impl iot_config::SessionKeyFilter for SessionKeyFilterService { async fn get(&self, request: Request) -> GrpcResult { let request = request.into_inner(); - self.verify_request_signature(&request, request.oui).await?; + let signer = self.verify_public_key(&request.signer)?; + self.verify_request_signature(&signer, &request, request.oui) + .await?; let (tx, rx) = tokio::sync::mpsc::channel(20); let pool = self.pool.clone(); @@ -216,6 +245,7 @@ impl iot_config::SessionKeyFilter for SessionKeyFilterService { &to_add, &to_remove, &self.pool, + self.signing_key.clone(), self.clone_update_channel(), ) .await @@ -240,6 +270,7 @@ impl iot_config::SessionKeyFilter for SessionKeyFilterService { &to_add, &to_remove, &self.pool, + self.signing_key.clone(), self.clone_update_channel(), ) .await @@ -248,7 +279,15 @@ impl iot_config::SessionKeyFilter for SessionKeyFilterService { Status::internal("session key update failed") })?; } - Ok(Response::new(SessionKeyFilterUpdateResV1 {})) + + let mut resp = SessionKeyFilterUpdateResV1 { + timestamp: Utc::now().encode_timestamp(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + resp.signature = self.sign_response(&resp.encode_to_vec())?; + + Ok(Response::new(resp)) } type streamStream = GrpcStreamResult; @@ -258,18 +297,23 @@ impl iot_config::SessionKeyFilter for SessionKeyFilterService { ) -> GrpcResult { let request = request.into_inner(); - self.verify_stream_request_signature(&request).await?; + let signer = self.verify_public_key(&request.signer)?; + self.verify_stream_request_signature(&signer, &request)?; tracing::info!("client subscribed to session key stream"); let pool = self.pool.clone(); let shutdown_listener = self.shutdown.clone(); let (tx, rx) = tokio::sync::mpsc::channel(20); + let signing_key = self.signing_key.clone(); let mut session_key_updates = self.subscribe_to_session_keys(); tokio::spawn(async move { - if stream_existing_skfs(&pool, tx.clone()).await.is_err() { + if stream_existing_skfs(&pool, signing_key, tx.clone()) + .await + .is_err() + { return; } @@ -294,14 +338,28 @@ impl iot_config::SessionKeyFilter for SessionKeyFilterService { async fn stream_existing_skfs( pool: &Pool, + signing_key: Arc, tx: mpsc::Sender>, ) -> Result<()> { + let timestamp = Utc::now().encode_timestamp(); + let signer: Vec = signing_key.public_key().into(); session_key::list_stream(pool) .then(|session_key_filter| { - tx.send(Ok(SessionKeyFilterStreamResV1 { + let mut skf_resp = SessionKeyFilterStreamResV1 { action: ActionV1::Add.into(), filter: Some(session_key_filter.into()), - })) + timestamp, + signer: signer.clone(), + signature: vec![], + }; + + futures::future::ready(signing_key.sign(&skf_resp.encode_to_vec())) + .map_err(|_| anyhow!("failed signing session key filter")) + .and_then(|signature| { + skf_resp.signature = signature; + tx.send(Ok(skf_resp)) + .map_err(|_| anyhow!("failed sending session key filter")) + }) }) .map_err(|err| anyhow!(err)) .try_fold((), |acc, _| async move { Ok(acc) }) diff --git a/iot_config/src/settings.rs b/iot_config/src/settings.rs index 541c57a3f..dd8a6aa32 100644 --- a/iot_config/src/settings.rs +++ b/iot_config/src/settings.rs @@ -24,8 +24,6 @@ pub struct Settings { pub network: Network, pub database: db_store::Settings, pub metrics: poc_metrics::Settings, - /// Helium blockchain node client for gateway location lookups - pub follower: node_follower::Settings, } pub fn default_log() -> String { From 0a9a1872a494aa301a6bdc7c7f70b811b22125cc Mon Sep 17 00:00:00 2001 From: Brian Balser Date: Sat, 1 Apr 2023 19:52:47 -0400 Subject: [PATCH 03/15] Update gateway_service::stream_all_gateways_info to not use stream combinators --- iot_config/src/gateway_service.rs | 178 +++++++++++++++++------------- iot_config/src/main.rs | 9 +- 2 files changed, 108 insertions(+), 79 deletions(-) diff --git a/iot_config/src/gateway_service.rs b/iot_config/src/gateway_service.rs index a7ce97d4a..6960bd62e 100644 --- a/iot_config/src/gateway_service.rs +++ b/iot_config/src/gateway_service.rs @@ -1,14 +1,13 @@ use crate::{ - admin::AuthCache, gateway_info::{self, GatewayInfo, db::IotMetadata}, - region_map::RegionMapReader, GrpcResult, GrpcStreamResult, Settings + admin::AuthCache, + gateway_info::{self, GatewayInfo}, + region_map::RegionMapReader, + GrpcResult, GrpcStreamResult, Settings, }; use anyhow::Result; use chrono::Utc; use file_store::traits::{MsgVerify, TimestampEncode}; -use futures::{ - future::TryFutureExt, - stream::{StreamExt, TryStreamExt}, -}; +use futures::stream::StreamExt; use helium_crypto::{Keypair, PublicKey, PublicKeyBinary, Sign}; use helium_proto::{ services::iot_config::{ @@ -31,7 +30,12 @@ pub struct GatewayService { } impl GatewayService { - pub fn new(settings: &Settings, pool: Pool, region_map: RegionMapReader, auth_cache: AuthCache) -> Result { + pub fn new( + settings: &Settings, + pool: Pool, + region_map: RegionMapReader, + auth_cache: AuthCache, + ) -> Result { Ok(Self { auth_cache, pool, @@ -82,14 +86,20 @@ impl iot_config::Gateway for GatewayService { .await .map_err(|_| Status::internal("error fetching gateway info"))? .map_or_else( - || Err(Status::not_found(format!("gateway not found: pubkey = {address:}"))), + || { + Err(Status::not_found(format!( + "gateway not found: pubkey = {address:}" + ))) + }, |info| { if let Some(location) = info.location { Ok(location) } else { - Err(Status::not_found(format!("gateway unasserted: pubkey = {address:}"))) + Err(Status::not_found(format!( + "gateway unasserted: pubkey = {address:}" + ))) } - } + }, )?; let location = Cell::from_raw(location) @@ -131,30 +141,46 @@ impl iot_config::Gateway for GatewayService { let (region, gain) = if let Some(info) = gateway_info::db::get_info(&self.pool, address) .await - .map_err(|_| Status::internal("error fetching gateway info"))? { - if let (Some(location), Some(gain)) = (info.location, info.gain) { - let region = match hextree::Cell::from_raw(location) { - Ok(h3_location) => self - .region_map - .get_region(h3_location) - .unwrap_or_else(|| { - tracing::debug!(pubkey = address.to_string(), location = location, "gateway region lookup failed for asserted location"); - default_region - }), - Err(_) => { - tracing::debug!(pubkey = address.to_string(), location = location, "gateway asserted location is invalid h3 index"); + .map_err(|_| Status::internal("error fetching gateway info"))? + { + if let (Some(location), Some(gain)) = (info.location, info.gain) { + let region = match hextree::Cell::from_raw(location) { + Ok(h3_location) => { + self.region_map.get_region(h3_location).unwrap_or_else(|| { + tracing::debug!( + pubkey = address.to_string(), + location = location, + "gateway region lookup failed for asserted location" + ); default_region - } - }; - (region, gain) - } else { - tracing::debug!(pubkey = address.to_string(), default_region = default_region.to_string(), "gateway not asserted"); - (default_region, 0) - } + }) + } + Err(_) => { + tracing::debug!( + pubkey = address.to_string(), + location = location, + "gateway asserted location is invalid h3 index" + ); + default_region + } + }; + (region, gain) } else { - tracing::debug!(pubkey = address.to_string(), default_region = default_region.to_string(), "error retrieving gateway from chain"); + tracing::debug!( + pubkey = address.to_string(), + default_region = default_region.to_string(), + "gateway not asserted" + ); (default_region, 0) - }; + } + } else { + tracing::debug!( + pubkey = address.to_string(), + default_region = default_region.to_string(), + "error retrieving gateway from chain" + ); + (default_region, 0) + }; let params = self.region_map.get_params(®ion); @@ -185,11 +211,15 @@ impl iot_config::Gateway for GatewayService { let metadata_info = gateway_info::db::get_info(&self.pool, address) .await .map_err(|_| Status::internal("error fetching gateway info"))? - .ok_or(Status::not_found(format!("gateway not found: pubkey = {address:}")))?; + .ok_or(Status::not_found(format!( + "gateway not found: pubkey = {address:}" + )))?; let gateway_info = GatewayInfo::chain_metadata_to_info(metadata_info, &self.region_map); let mut resp = GatewayInfoResV1 { - info: Some(gateway_info.try_into().map_err(|_| Status::internal("unexpected error converting gateway info to protobuf"))?), + info: Some(gateway_info.try_into().map_err(|_| { + Status::internal("unexpected error converting gateway info to protobuf") + })?), timestamp: Utc::now().encode_timestamp(), signer: self.signing_key.public_key().into(), signature: vec![], @@ -219,7 +249,14 @@ impl iot_config::Gateway for GatewayService { let (tx, rx) = tokio::sync::mpsc::channel(20); tokio::spawn(async move { - stream_all_gateways_info(&pool, tx.clone(), &signing_key, region_map.clone(), batch_size).await + stream_all_gateways_info( + &pool, + tx.clone(), + &signing_key, + region_map.clone(), + batch_size, + ) + .await }); Ok(Response::new(GrpcStreamResult::new(rx))) @@ -231,52 +268,39 @@ async fn stream_all_gateways_info( tx: tokio::sync::mpsc::Sender>, signing_key: &Keypair, region_map: RegionMapReader, - batch_size: u32, + _batch_size: u32, ) -> anyhow::Result<()> { let timestamp = Utc::now().encode_timestamp(); let signer: Vec = signing_key.public_key().into(); - let region_map = ®ion_map; let tx = &tx; - Ok(gateway_info::db::all_info_stream(pool) - .map(Ok::) - .try_filter_map(|info| async move { - let result: Option = match GatewayInfo::chain_metadata_to_info(info, region_map).try_into() { - Ok(info_proto) => Some(info_proto), - Err(_) => None, - }; - Ok(result) - }) - .try_chunks(batch_size as usize) - .map_ok(move |batch| { - ( - GatewayInfoStreamResV1 { - gateways: batch, - timestamp, - signer: signer.clone(), - signature: vec![], - }, - signing_key.clone() - ) - }) - .try_filter_map(|(res, keypair)| async move { - let result = match keypair.sign(&res.encode_to_vec()) { - Ok(signature) => Some(GatewayInfoStreamResV1 { - gateways: res.gateways, - timestamp: res.timestamp, - signer: res.signer, - signature, - }), - Err(_) => None, + let mut stream = gateway_info::db::all_info_stream(pool); + while let Some(info) = stream.next().await { + let gateway_info: iot_config::GatewayInfo = + match GatewayInfo::chain_metadata_to_info(info, ®ion_map).try_into() { + Ok(gi) => gi, + Err(_) => { + continue; + } }; - Ok(result) - }) - .map_err(|err| Status::internal(format!("info batch failed with reason: {err:?}"))) - .try_for_each(|res| { - tx.send(Ok(res)) - .map_err(|err| Status::internal(format!("info batch send failed with reason {err:?}"))) - }) - .or_else(|err| { - tx.send(Err(Status::internal(format!("info batch failed with reason: {err:?}")))) - }) - .await?) + + let mut response = GatewayInfoStreamResV1 { + gateways: vec![gateway_info], + timestamp, + signer: signer.clone(), + signature: vec![], + }; + + response = match signing_key.sign(&response.encode_to_vec()) { + Ok(signature) => GatewayInfoStreamResV1 { + signature, + ..response + }, + Err(_) => { + continue; + } + }; + + tx.send(Ok(response)).await?; + } + Ok(()) } diff --git a/iot_config/src/main.rs b/iot_config/src/main.rs index f91873597..6db2a69d2 100644 --- a/iot_config/src/main.rs +++ b/iot_config/src/main.rs @@ -6,7 +6,7 @@ use helium_proto::services::iot_config::{ }; use iot_config::{ admin::AuthCache, gateway_service::GatewayService, org_service::OrgService, - region_map::RegionMap, route_service::RouteService, + region_map::RegionMapReader, route_service::RouteService, session_key_service::SessionKeyFilterService, settings::Settings, AdminService, }; use std::{path::PathBuf, time::Duration}; @@ -80,7 +80,12 @@ impl Daemon { let (auth_updater, auth_cache) = AuthCache::new(settings, &pool).await?; let (region_updater, region_map) = RegionMapReader::new(&pool).await?; - let gateway_svc = GatewayService::new(settings, pool.clone(), region_map.clone(), auth_cache.clone())?; + let gateway_svc = GatewayService::new( + settings, + pool.clone(), + region_map.clone(), + auth_cache.clone(), + )?; let route_svc = RouteService::new( settings, auth_cache.clone(), From 0f0fc54df545f83bf8944806adff92ed99e84e6a Mon Sep 17 00:00:00 2001 From: Brian Balser Date: Sat, 1 Apr 2023 20:17:19 -0400 Subject: [PATCH 04/15] Adding batching back into gateway_service::stream_all_gateways_info --- iot_config/src/gateway_service.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/iot_config/src/gateway_service.rs b/iot_config/src/gateway_service.rs index 6960bd62e..8671f2b8e 100644 --- a/iot_config/src/gateway_service.rs +++ b/iot_config/src/gateway_service.rs @@ -268,23 +268,25 @@ async fn stream_all_gateways_info( tx: tokio::sync::mpsc::Sender>, signing_key: &Keypair, region_map: RegionMapReader, - _batch_size: u32, + batch_size: u32, ) -> anyhow::Result<()> { let timestamp = Utc::now().encode_timestamp(); let signer: Vec = signing_key.public_key().into(); let tx = &tx; - let mut stream = gateway_info::db::all_info_stream(pool); - while let Some(info) = stream.next().await { - let gateway_info: iot_config::GatewayInfo = - match GatewayInfo::chain_metadata_to_info(info, ®ion_map).try_into() { - Ok(gi) => gi, - Err(_) => { - continue; + let mut stream = gateway_info::db::all_info_stream(pool).chunks(batch_size as usize); + while let Some(infos) = stream.next().await { + let gateway_infos = infos + .into_iter() + .filter_map(|info| { + match GatewayInfo::chain_metadata_to_info(info, ®ion_map).try_into() { + Ok(gi) => Some(gi), + Err(_) => None, } - }; + }) + .collect(); let mut response = GatewayInfoStreamResV1 { - gateways: vec![gateway_info], + gateways: gateway_infos, timestamp, signer: signer.clone(), signature: vec![], From e6143a0d57d9f222a666c8dd1d38454af85d786d Mon Sep 17 00:00:00 2001 From: jeffgrunewald Date: Tue, 4 Apr 2023 12:14:28 -0400 Subject: [PATCH 05/15] order region query, cleanup some result/option matches --- iot_config/src/admin_service.rs | 12 ++++-------- iot_config/src/gateway_info.rs | 18 ++++++++++++------ iot_config/src/gateway_service.rs | 8 +++----- iot_config/src/region_map.rs | 7 +++++-- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/iot_config/src/admin_service.rs b/iot_config/src/admin_service.rs index 00194cf69..2a8eba493 100644 --- a/iot_config/src/admin_service.rs +++ b/iot_config/src/admin_service.rs @@ -206,14 +206,10 @@ impl iot_config::Admin for AdminService { self.region_updater.send_modify(|region_map| { region_map.insert_params(region, params); }); - match updated_region { - Some(region_tree) => { - tracing::debug!(region_cells = region_tree.len(), "new compacted region map"); - self.region_updater.send_modify(|region_map| { - region_map.replace_tree(region_tree) - }); - } - None => () + if let Some(region_tree) = updated_region { + tracing::debug!(region_cells = region_tree.len(), "new compacted region map"); + self.region_updater + .send_modify(|region_map| region_map.replace_tree(region_tree)); }; Ok(()) }) diff --git a/iot_config/src/gateway_info.rs b/iot_config/src/gateway_info.rs index 00cc63814..bec822fd9 100644 --- a/iot_config/src/gateway_info.rs +++ b/iot_config/src/gateway_info.rs @@ -27,8 +27,13 @@ pub struct GatewayInfo { } impl GatewayInfo { - pub fn chain_metadata_to_info(meta: db::IotMetadata, region_map: ®ion_map::RegionMapReader) -> Self { - let metadata = if let (Some(location), Some(elevation), Some(gain)) = (meta.location, meta.elevation, meta.gain) { + pub fn chain_metadata_to_info( + meta: db::IotMetadata, + region_map: ®ion_map::RegionMapReader, + ) -> Self { + let metadata = if let (Some(location), Some(elevation), Some(gain)) = + (meta.location, meta.elevation, meta.gain) + { if let Ok(region) = h3index_to_region(location, region_map) { Some(GatewayMetadata { location, @@ -51,11 +56,12 @@ impl GatewayInfo { } } -fn h3index_to_region(location: u64, region_map: ®ion_map::RegionMapReader) -> anyhow::Result { +fn h3index_to_region( + location: u64, + region_map: ®ion_map::RegionMapReader, +) -> anyhow::Result { hextree::Cell::from_raw(location) - .map(|cell| { - region_map.get_region(cell) - })? + .map(|cell| region_map.get_region(cell))? .ok_or(anyhow!("invalid region")) } diff --git a/iot_config/src/gateway_service.rs b/iot_config/src/gateway_service.rs index 8671f2b8e..e6a3f0123 100644 --- a/iot_config/src/gateway_service.rs +++ b/iot_config/src/gateway_service.rs @@ -272,16 +272,14 @@ async fn stream_all_gateways_info( ) -> anyhow::Result<()> { let timestamp = Utc::now().encode_timestamp(); let signer: Vec = signing_key.public_key().into(); - let tx = &tx; let mut stream = gateway_info::db::all_info_stream(pool).chunks(batch_size as usize); while let Some(infos) = stream.next().await { let gateway_infos = infos .into_iter() .filter_map(|info| { - match GatewayInfo::chain_metadata_to_info(info, ®ion_map).try_into() { - Ok(gi) => Some(gi), - Err(_) => None, - } + GatewayInfo::chain_metadata_to_info(info, ®ion_map) + .try_into() + .ok() }) .collect(); diff --git a/iot_config/src/region_map.rs b/iot_config/src/region_map.rs index dcf8a3abb..7c458ba4c 100644 --- a/iot_config/src/region_map.rs +++ b/iot_config/src/region_map.rs @@ -18,7 +18,9 @@ pub struct RegionMapReader { } impl RegionMapReader { - pub async fn new(db: impl sqlx::PgExecutor<'_> + Copy) -> anyhow::Result<(watch::Sender, Self)> { + pub async fn new( + db: impl sqlx::PgExecutor<'_> + Copy, + ) -> anyhow::Result<(watch::Sender, Self)> { let region_map = RegionMap::new(db).await?; let (map_sender, map_receiver) = watch::channel(region_map); Ok((map_sender, Self { map_receiver })) @@ -72,7 +74,8 @@ pub async fn build_region_tree( ) -> anyhow::Result> { let mut region_tree = HexTreeMap::with_compactor(EqCompactor); - let mut regions = sqlx::query_as::<_, HexRegion>("select * from regions").fetch(db); + let mut regions = + sqlx::query_as::<_, HexRegion>("select * from regions order by region desc").fetch(db); while let Some(region_row) = regions.try_next().await? { if let Some(indexes) = region_row.indexes { From 8c6b32627d5439cdf953ded05c087bd233b9ec64 Mon Sep 17 00:00:00 2001 From: jeffgrunewald Date: Tue, 4 Apr 2023 13:25:07 -0400 Subject: [PATCH 06/15] coupling iot config client under service crate --- Cargo.lock | 2 + file_store/src/traits/msg_verify.rs | 2 + iot_config/Cargo.toml | 2 + iot_config/src/client/mod.rs | 100 ++++++++++++++++++++++++++++ iot_config/src/client/settings.rs | 56 ++++++++++++++++ iot_config/src/gateway_info.rs | 2 +- iot_config/src/lib.rs | 2 + 7 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 iot_config/src/client/mod.rs create mode 100644 iot_config/src/client/settings.rs diff --git a/Cargo.lock b/Cargo.lock index 45f17f506..18508ae97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3278,6 +3278,8 @@ dependencies = [ "helium-crypto", "helium-proto", "hextree", + "http", + "http-serde", "libflate", "metrics", "metrics-exporter-prometheus", diff --git a/file_store/src/traits/msg_verify.rs b/file_store/src/traits/msg_verify.rs index 88adad877..893f773bc 100644 --- a/file_store/src/traits/msg_verify.rs +++ b/file_store/src/traits/msg_verify.rs @@ -58,6 +58,8 @@ impl_msg_verify!(iot_config::SessionKeyFilterUpdateReqV1, signature); impl_msg_verify!(iot_config::GatewayInfoReqV1, signature); impl_msg_verify!(iot_config::GatewayInfoStreamReqV1, signature); impl_msg_verify!(iot_config::RegionParamsReqV1, signature); +impl_msg_verify!(iot_config::GatewayInfoResV1, signature); +impl_msg_verify!(iot_config::GatewayInfoStreamResV1, signature); #[cfg(test)] mod test { diff --git a/iot_config/Cargo.toml b/iot_config/Cargo.toml index 5decd8321..caeeb5986 100644 --- a/iot_config/Cargo.toml +++ b/iot_config/Cargo.toml @@ -20,6 +20,8 @@ futures-util = {workspace = true} helium-crypto = {workspace = true} helium-proto = {workspace = true} hextree = {workspace = true} +http = {workspace = true} +http-serde = {workspace = true} libflate = "1" metrics = {workspace = true} metrics-exporter-prometheus = {workspace = true} diff --git a/iot_config/src/client/mod.rs b/iot_config/src/client/mod.rs new file mode 100644 index 000000000..d8b8038d1 --- /dev/null +++ b/iot_config/src/client/mod.rs @@ -0,0 +1,100 @@ +use crate::gateway_info; +use file_store::traits::MsgVerify; +use futures::stream::{self, StreamExt}; +use helium_crypto::{Keypair, PublicKey, PublicKeyBinary, Sign}; +use helium_proto::{ + services::{iot_config, Channel}, + Message, +}; +use std::sync::Arc; + +mod settings; +pub use settings::Settings; + +#[derive(thiserror::Error, Debug)] +pub enum ClientError { + #[error("error signing request: {0}")] + SigningError(#[from] helium_crypto::Error), + #[error("grpc error response: {0}")] + GrpcError(#[from] tonic::Status), + #[error("error verifying response signature: {0}")] + VerificationError(#[from] file_store::Error), +} + +#[derive(Clone, Debug)] +pub struct Client { + pub client: iot_config::GatewayClient, + signing_key: Arc, + config_pubkey: PublicKey, + batch_size: u32, +} + +impl Client { + pub fn from_settings(settings: &Settings) -> Result> { + Ok(Self { + client: settings.connect(), + signing_key: settings.signing_keypair()?, + config_pubkey: settings.config_pubkey()?, + batch_size: settings.batch_size, + }) + } +} + +#[async_trait::async_trait] +impl gateway_info::GatewayInfoResolver for Client { + type Error = ClientError; + + async fn resolve_gateway_info( + &mut self, + address: &PublicKeyBinary, + ) -> Result, Self::Error> { + let mut request = iot_config::GatewayInfoReqV1 { + address: address.clone().into(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + request.signature = self.signing_key.sign(&request.encode_to_vec())?; + tracing::debug!(pubkey = address.to_string(), "fetching gateway info"); + let response = match self.client.info(request).await { + Ok(info_resp) => { + let response = info_resp.into_inner(); + response.verify(&self.config_pubkey)?; + response.info.map(gateway_info::GatewayInfo::from) + } + Err(status) if status.code() == tonic::Code::NotFound => None, + Err(status) => Err(status)?, + }; + Ok(response) + } + + async fn stream_gateways_info( + &mut self, + ) -> Result { + let mut request = iot_config::GatewayInfoStreamReqV1 { + batch_size: self.batch_size, + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + request.signature = self.signing_key.sign(&request.encode_to_vec())?; + tracing::debug!("fetching gateway info stream"); + let pubkey = Arc::new(self.config_pubkey.clone()); + let response_stream = self + .client + .info_stream(request) + .await? + .into_inner() + .filter_map(|resp| async move { resp.ok() }) + .map(move |resp| (resp, pubkey.clone())) + .filter_map(|(resp, pubkey)| async move { + match resp.verify(&pubkey) { + Ok(()) => Some(resp), + Err(_) => None, + } + }) + .flat_map(|resp| stream::iter(resp.gateways.into_iter())) + .map(gateway_info::GatewayInfo::from) + .boxed(); + + Ok(response_stream) + } +} diff --git a/iot_config/src/client/settings.rs b/iot_config/src/client/settings.rs new file mode 100644 index 000000000..e71da2c89 --- /dev/null +++ b/iot_config/src/client/settings.rs @@ -0,0 +1,56 @@ +use helium_proto::services::{iot_config, Channel, Endpoint}; +use serde::Deserialize; +use std::{str::FromStr, sync::Arc, time::Duration}; + +#[derive(Clone, Debug, Deserialize)] +pub struct Settings { + /// grpc url to the iot config oracle server + #[serde(with = "http_serde::uri")] + pub url: http::Uri, + /// File from which to load keypair for signing config server requests + pub signing_keypair: String, + /// B58 encoded public key of the iot config server for verifying responses + pub config_pubkey: String, + /// Connect timeout for the iot config client in seconds. Default 5 + #[serde(default = "default_connect_timeout")] + pub connect_timeout: u64, + /// RPC timeout for iot config client in seconds. Default 5 + #[serde(default = "default_rpc_timeout")] + pub rpc_timeout: u64, + /// Batch size for gateway info stream results. Default 1000 + #[serde(default = "default_batch_size")] + pub batch_size: u32, +} + +pub fn default_connect_timeout() -> u64 { + 5 +} + +pub fn default_rpc_timeout() -> u64 { + 5 +} + +pub fn default_batch_size() -> u32 { + 1000 +} + +impl Settings { + pub fn connect(&self) -> iot_config::GatewayClient { + let channel = Endpoint::from(self.url.clone()) + .connect_timeout(Duration::from_secs(self.connect_timeout)) + .timeout(Duration::from_secs(self.rpc_timeout)) + .connect_lazy(); + iot_config::GatewayClient::new(channel) + } + + pub fn signing_keypair( + &self, + ) -> Result, Box> { + let data = std::fs::read(&self.signing_keypair).map_err(helium_crypto::Error::from)?; + Ok(Arc::new(helium_crypto::Keypair::try_from(&data[..])?)) + } + + pub fn config_pubkey(&self) -> Result { + helium_crypto::PublicKey::from_str(&self.config_pubkey) + } +} diff --git a/iot_config/src/gateway_info.rs b/iot_config/src/gateway_info.rs index bec822fd9..cd716fa8b 100644 --- a/iot_config/src/gateway_info.rs +++ b/iot_config/src/gateway_info.rs @@ -74,7 +74,7 @@ pub trait GatewayInfoResolver { address: &PublicKeyBinary, ) -> Result, Self::Error>; - async fn stream_gateway_info(&mut self) -> Result; + async fn stream_gateways_info(&mut self) -> Result; } impl From for GatewayInfo { diff --git a/iot_config/src/lib.rs b/iot_config/src/lib.rs index f2c02cb11..f19af4ec4 100644 --- a/iot_config/src/lib.rs +++ b/iot_config/src/lib.rs @@ -1,5 +1,6 @@ pub mod admin; pub mod admin_service; +pub mod client; pub mod gateway_info; pub mod gateway_service; pub mod lora_field; @@ -13,6 +14,7 @@ pub mod session_key_service; pub mod settings; pub use admin_service::AdminService; +pub use client::{Client, Settings as ClientSettings}; pub use gateway_service::GatewayService; use lora_field::{LoraField, NetIdField}; pub use org_service::OrgService; From 0af4857f583ec1fe86a68f779a3e4dc091ad0485 Mon Sep 17 00:00:00 2001 From: jeffgrunewald Date: Tue, 4 Apr 2023 16:21:40 -0400 Subject: [PATCH 07/15] add region params resolver to iot config client --- file_store/src/traits/msg_verify.rs | 1 + iot_config/src/client/mod.rs | 61 ++++++++++++++++++++++++----- iot_config/src/client/settings.rs | 11 +----- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/file_store/src/traits/msg_verify.rs b/file_store/src/traits/msg_verify.rs index 893f773bc..bc1a7e079 100644 --- a/file_store/src/traits/msg_verify.rs +++ b/file_store/src/traits/msg_verify.rs @@ -60,6 +60,7 @@ impl_msg_verify!(iot_config::GatewayInfoStreamReqV1, signature); impl_msg_verify!(iot_config::RegionParamsReqV1, signature); impl_msg_verify!(iot_config::GatewayInfoResV1, signature); impl_msg_verify!(iot_config::GatewayInfoStreamResV1, signature); +impl_msg_verify!(iot_config::RegionParamsResV1, signature); #[cfg(test)] mod test { diff --git a/iot_config/src/client/mod.rs b/iot_config/src/client/mod.rs index d8b8038d1..41c1d68e7 100644 --- a/iot_config/src/client/mod.rs +++ b/iot_config/src/client/mod.rs @@ -3,10 +3,10 @@ use file_store::traits::MsgVerify; use futures::stream::{self, StreamExt}; use helium_crypto::{Keypair, PublicKey, PublicKeyBinary, Sign}; use helium_proto::{ - services::{iot_config, Channel}, - Message, + services::{iot_config, Channel, Endpoint}, + BlockchainRegionParamV1, Message, Region, }; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; mod settings; pub use settings::Settings; @@ -14,16 +14,19 @@ pub use settings::Settings; #[derive(thiserror::Error, Debug)] pub enum ClientError { #[error("error signing request: {0}")] - SigningError(#[from] helium_crypto::Error), + Signing(#[from] helium_crypto::Error), #[error("grpc error response: {0}")] - GrpcError(#[from] tonic::Status), + Rpc(#[from] tonic::Status), #[error("error verifying response signature: {0}")] - VerificationError(#[from] file_store::Error), + Verification(#[from] file_store::Error), + #[error("error resolving region params: {0}")] + UndefinedRegionParams(String), } #[derive(Clone, Debug)] pub struct Client { - pub client: iot_config::GatewayClient, + pub gateway_client: iot_config::gateway_client::GatewayClient, + pub admin_client: iot_config::admin_client::AdminClient, signing_key: Arc, config_pubkey: PublicKey, batch_size: u32, @@ -31,13 +34,39 @@ pub struct Client { impl Client { pub fn from_settings(settings: &Settings) -> Result> { + let channel = Endpoint::from(settings.url.clone()) + .connect_timeout(Duration::from_secs(settings.connect_timeout)) + .timeout(Duration::from_secs(settings.rpc_timeout)) + .connect_lazy(); Ok(Self { - client: settings.connect(), + gateway_client: iot_config::gateway_client::GatewayClient::new(channel.clone()), + admin_client: iot_config::admin_client::AdminClient::new(channel), signing_key: settings.signing_keypair()?, config_pubkey: settings.config_pubkey()?, batch_size: settings.batch_size, }) } + + pub async fn resolve_region_params( + &mut self, + region: Region, + ) -> Result { + let mut request = iot_config::RegionParamsReqV1 { + region: region.into(), + signer: self.signing_key.public_key().into(), + signature: vec![], + }; + request.signature = self.signing_key.sign(&request.encode_to_vec())?; + let response = self.admin_client.region_params(request).await?.into_inner(); + response.verify(&self.config_pubkey)?; + Ok(RegionParamsInfo { + region: response.region(), + region_params: response + .params + .ok_or_else(|| ClientError::UndefinedRegionParams(format!("{region}")))? + .region_params, + }) + } } #[async_trait::async_trait] @@ -55,7 +84,7 @@ impl gateway_info::GatewayInfoResolver for Client { }; request.signature = self.signing_key.sign(&request.encode_to_vec())?; tracing::debug!(pubkey = address.to_string(), "fetching gateway info"); - let response = match self.client.info(request).await { + let response = match self.gateway_client.info(request).await { Ok(info_resp) => { let response = info_resp.into_inner(); response.verify(&self.config_pubkey)?; @@ -79,7 +108,7 @@ impl gateway_info::GatewayInfoResolver for Client { tracing::debug!("fetching gateway info stream"); let pubkey = Arc::new(self.config_pubkey.clone()); let response_stream = self - .client + .gateway_client .info_stream(request) .await? .into_inner() @@ -98,3 +127,15 @@ impl gateway_info::GatewayInfoResolver for Client { Ok(response_stream) } } + +#[derive(Clone, Debug)] +pub struct RegionParamsInfo { + pub region: Region, + pub region_params: Vec, +} + +#[derive(thiserror::Error, Debug)] +pub enum RegionParamsResolveError { + #[error("params undefined for region: {0}")] + Undefined(String), +} diff --git a/iot_config/src/client/settings.rs b/iot_config/src/client/settings.rs index e71da2c89..3c7d20550 100644 --- a/iot_config/src/client/settings.rs +++ b/iot_config/src/client/settings.rs @@ -1,6 +1,5 @@ -use helium_proto::services::{iot_config, Channel, Endpoint}; use serde::Deserialize; -use std::{str::FromStr, sync::Arc, time::Duration}; +use std::{str::FromStr, sync::Arc}; #[derive(Clone, Debug, Deserialize)] pub struct Settings { @@ -35,14 +34,6 @@ pub fn default_batch_size() -> u32 { } impl Settings { - pub fn connect(&self) -> iot_config::GatewayClient { - let channel = Endpoint::from(self.url.clone()) - .connect_timeout(Duration::from_secs(self.connect_timeout)) - .timeout(Duration::from_secs(self.rpc_timeout)) - .connect_lazy(); - iot_config::GatewayClient::new(channel) - } - pub fn signing_keypair( &self, ) -> Result, Box> { From 47ece4ebd0e1760d18842071ff9ed5e170ca1dae Mon Sep 17 00:00:00 2001 From: jeffgrunewald Date: Wed, 5 Apr 2023 02:35:11 -0400 Subject: [PATCH 08/15] swap iot config integrated client for standalone --- Cargo.lock | 21 +- Cargo.toml | 1 - iot_config_client/Cargo.toml | 22 -- iot_config_client/src/gateway_resolver.rs | 49 ---- iot_config_client/src/iot_config_client.rs | 116 --------- iot_config_client/src/lib.rs | 10 - .../src/region_params_resolver.rs | 43 ---- iot_config_client/src/settings.rs | 59 ----- iot_packet_verifier/src/verifier.rs | 2 + iot_verifier/Cargo.toml | 2 +- iot_verifier/src/gateway_cache.rs | 14 +- iot_verifier/src/main.rs | 2 +- iot_verifier/src/poc.rs | 235 +++++++++--------- iot_verifier/src/region_cache.rs | 5 +- iot_verifier/src/runner.rs | 3 +- iot_verifier/src/settings.rs | 2 +- iot_verifier/src/tx_scaler.rs | 11 +- 17 files changed, 137 insertions(+), 460 deletions(-) delete mode 100644 iot_config_client/Cargo.toml delete mode 100644 iot_config_client/src/gateway_resolver.rs delete mode 100644 iot_config_client/src/iot_config_client.rs delete mode 100644 iot_config_client/src/lib.rs delete mode 100644 iot_config_client/src/region_params_resolver.rs delete mode 100644 iot_config_client/src/settings.rs diff --git a/Cargo.lock b/Cargo.lock index 18508ae97..09f368a8e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3297,25 +3297,6 @@ dependencies = [ "triggered", ] -[[package]] -name = "iot-config-client" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "beacon", - "futures", - "helium-crypto", - "helium-proto", - "http", - "http-serde", - "rand 0.8.5", - "retainer", - "serde", - "thiserror", - "tonic", -] - [[package]] name = "iot-packet-verifier" version = "0.1.0" @@ -3379,7 +3360,7 @@ dependencies = [ "helium-proto", "http-serde", "humantime", - "iot-config-client", + "iot-config", "itertools", "lazy_static", "metrics", diff --git a/Cargo.toml b/Cargo.toml index 755001c07..345035650 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ members = [ "iot_packet_verifier", "iot_config", "price", - "iot_config_client" ] [workspace.package] diff --git a/iot_config_client/Cargo.toml b/iot_config_client/Cargo.toml deleted file mode 100644 index 5b784226c..000000000 --- a/iot_config_client/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "iot-config-client" -version = "0.1.0" -description = "Client for IOT Config" -edition.workspace = true -authors.workspace = true -license.workspace = true - -[dependencies] -anyhow = {workspace = true} -futures = {workspace = true} -serde = {workspace = true} -http-serde = {workspace = true} -thiserror = {workspace = true} -beacon = {workspace = true} -helium-proto = { workspace = true } -helium-crypto = { workspace = true } -http = {workspace = true} -tonic = {workspace = true} -retainer = {workspace = true} -async-trait = {workspace = true} -rand = {workspace = true} diff --git a/iot_config_client/src/gateway_resolver.rs b/iot_config_client/src/gateway_resolver.rs deleted file mode 100644 index 0f6bb1e73..000000000 --- a/iot_config_client/src/gateway_resolver.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::iot_config_client::IotConfigClientError; -use async_trait::async_trait; -use helium_crypto::PublicKeyBinary; -use helium_proto::{services::iot_config::GatewayInfo as GatewayInfoProto, Region}; - -#[async_trait] -pub trait GatewayInfoResolver { - async fn resolve_gateway_info( - &mut self, - address: &PublicKeyBinary, - ) -> Result; -} - -#[derive(Debug, Clone)] -pub struct GatewayInfo { - pub address: PublicKeyBinary, - pub location: Option, - pub elevation: Option, - pub gain: i32, - pub is_full_hotspot: bool, - pub region: Region, -} - -#[derive(Debug, thiserror::Error)] -pub enum GatewayResolverError { - #[error("unsupported region {0}")] - Region(String), -} - -impl TryFrom for GatewayInfo { - type Error = GatewayResolverError; - fn try_from(v: GatewayInfoProto) -> Result { - let region: Region = Region::from_i32(v.region) - .ok_or_else(|| GatewayResolverError::Region(format!("{:?}", v.region)))?; - let location = u64::from_str_radix(&v.location, 16).ok(); - let elevation = match location { - Some(_) => Some(v.elevation), - None => None, - }; - Ok(Self { - address: v.address.into(), - location, - elevation, - gain: v.gain, - is_full_hotspot: v.is_full_hotspot, - region, - }) - } -} diff --git a/iot_config_client/src/iot_config_client.rs b/iot_config_client/src/iot_config_client.rs deleted file mode 100644 index 7975f0cb4..000000000 --- a/iot_config_client/src/iot_config_client.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::{ - gateway_resolver::{self, GatewayInfo, GatewayInfoResolver}, - region_params_resolver::{self, RegionParamsInfo, RegionParamsResolver}, - GatewayInfoStream, Settings, -}; -use anyhow::Result; -use futures::stream::{self, StreamExt}; -use helium_crypto::{Keypair, PublicKeyBinary, Sign}; -use helium_proto::services::{ - iot_config::{self, GatewayInfoReqV1, GatewayInfoStreamReqV1, RegionParamsReqV1}, - Channel, -}; -use helium_proto::{Message, Region}; -use std::sync::Arc; - -type IotGatewayConfigClient = iot_config::GatewayClient; -type IotAdminConfigClient = iot_config::admin_client::AdminClient; - -#[derive(Debug, Clone)] -pub struct IotConfigClient { - keypair: Arc, - pub gateway_client: IotGatewayConfigClient, - pub region_client: IotAdminConfigClient, - batch_size: u32, -} - -#[derive(Debug, thiserror::Error)] -pub enum IotConfigClientError { - #[error("error resolving gateway info")] - GatewayResolverError(#[from] gateway_resolver::GatewayResolverError), - #[error("error resolving region params")] - RegionParamsResolverError(#[from] region_params_resolver::RegionParamsResolverError), - #[error("gateway not found: {0}")] - GatewayNotFound(PublicKeyBinary), - #[error("region params not found {0}")] - RegionParamsNotFound(String), - #[error("uri error")] - Uri(#[from] http::uri::InvalidUri), - #[error("grpc {}", .0.message())] - Grpc(#[from] tonic::Status), - #[error("keypair error: {0}")] - KeypairError(#[from] Box), - #[error("signing error: {0}")] - SignError(#[from] helium_crypto::Error), -} - -#[async_trait::async_trait] -impl GatewayInfoResolver for IotConfigClient { - async fn resolve_gateway_info( - &mut self, - address: &PublicKeyBinary, - ) -> Result { - let mut req = GatewayInfoReqV1 { - address: address.clone().into(), - signature: vec![], - }; - req.signature = self.keypair.sign(&req.encode_to_vec())?; - let res = self.gateway_client.info(req).await?.into_inner(); - match res.info { - Some(gateway_info) => Ok(gateway_info.try_into()?), - _ => Err(IotConfigClientError::GatewayNotFound(address.clone())), - } - } -} - -#[async_trait::async_trait] -impl RegionParamsResolver for IotConfigClient { - async fn resolve_region_params( - &mut self, - region: Region, - ) -> Result { - let mut req = RegionParamsReqV1 { - region: region.into(), - signature: vec![], - }; - req.signature = self.keypair.sign(&req.encode_to_vec())?; - match self.region_client.region_params(req).await { - Ok(region_params_info) => Ok(region_params_info.into_inner().try_into()?), - _ => Err(IotConfigClientError::RegionParamsNotFound(format!( - "{region:?}" - ))), - } - } -} - -impl IotConfigClient { - pub fn from_settings(settings: &Settings) -> Result { - let channel = settings.connect_endpoint(); - let keypair = settings.keypair()?; - Ok(Self { - keypair: Arc::new(keypair), - gateway_client: settings.connect_iot_gateway_config(channel.clone()), - region_client: settings.connect_iot_region_config(channel), - batch_size: settings.batch, - }) - } - - pub async fn gateway_stream(&mut self) -> Result { - let mut req = GatewayInfoStreamReqV1 { - batch_size: self.batch_size, - signature: vec![], - }; - req.signature = self.keypair.sign(&req.encode_to_vec())?; - let gw_stream = self - .gateway_client - .info_stream(req) - .await? - .into_inner() - .filter_map(|resp| async move { resp.ok() }) - .flat_map(|resp| stream::iter(resp.gateways.into_iter())) - .filter_map(|resp| async move { GatewayInfo::try_from(resp).ok() }) - .boxed(); - - Ok(gw_stream) - } -} diff --git a/iot_config_client/src/lib.rs b/iot_config_client/src/lib.rs deleted file mode 100644 index 20d9ae4f7..000000000 --- a/iot_config_client/src/lib.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod gateway_resolver; -pub mod iot_config_client; -pub mod region_params_resolver; -mod settings; - -use futures::stream::BoxStream; -pub use settings::Settings; - -pub type Stream = BoxStream<'static, T>; -pub type GatewayInfoStream = Stream; diff --git a/iot_config_client/src/region_params_resolver.rs b/iot_config_client/src/region_params_resolver.rs deleted file mode 100644 index 4f8857dbf..000000000 --- a/iot_config_client/src/region_params_resolver.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::iot_config_client::IotConfigClientError; -use async_trait::async_trait; -use helium_proto::{ - services::iot_config::RegionParamsResV1 as RegionParamsProto, BlockchainRegionParamV1, Region, -}; - -#[async_trait] -pub trait RegionParamsResolver { - async fn resolve_region_params( - &mut self, - region: Region, - ) -> Result; -} - -#[derive(Debug, Clone)] -pub struct RegionParamsInfo { - pub region: Region, - pub region_params: Vec, -} - -#[derive(Debug, thiserror::Error)] -pub enum RegionParamsResolverError { - #[error("unsupported region {0}")] - Region(String), -} - -impl TryFrom for RegionParamsInfo { - type Error = RegionParamsResolverError; - fn try_from(v: RegionParamsProto) -> Result { - let region: Region = Region::from_i32(v.region) - .ok_or_else(|| RegionParamsResolverError::Region(format!("{:?}", v.region)))?; - let region_params = v - .params - .ok_or_else(|| { - RegionParamsResolverError::Region("failed to get region params".to_string()) - })? - .region_params; - Ok(Self { - region, - region_params, - }) - } -} diff --git a/iot_config_client/src/settings.rs b/iot_config_client/src/settings.rs deleted file mode 100644 index 32ce51b25..000000000 --- a/iot_config_client/src/settings.rs +++ /dev/null @@ -1,59 +0,0 @@ -use helium_proto::services::{iot_config, Channel, Endpoint}; -use serde::Deserialize; -use std::time::Duration; - -#[derive(Debug, Deserialize, Clone)] -pub struct Settings { - /// signing key used to sign requests to iot config service - pub keypair: String, - /// uri to iot config service - #[serde(with = "http_serde::uri")] - pub url: http::Uri, - /// Connect timeout in seconds. Default 5 - #[serde(default = "default_connect_timeout")] - pub connect: u64, - /// RPC timeout in seconds. Default 5 - #[serde(default = "default_rpc_timeout")] - pub rpc: u64, - /// batch size for gateway stream results. Default 100 - #[serde(default = "default_batch_size")] - pub batch: u32, -} - -pub fn default_connect_timeout() -> u64 { - 5 -} - -pub fn default_rpc_timeout() -> u64 { - 5 -} - -pub fn default_batch_size() -> u32 { - 100 -} - -impl Settings { - pub fn connect_endpoint(&self) -> Channel { - Endpoint::from(self.url.clone()) - .connect_timeout(Duration::from_secs(self.connect)) - .timeout(Duration::from_secs(self.rpc)) - .connect_lazy() - } - - pub fn connect_iot_gateway_config( - &self, - channel: Channel, - ) -> iot_config::GatewayClient { - iot_config::GatewayClient::new(channel) - } - pub fn connect_iot_region_config( - &self, - channel: Channel, - ) -> iot_config::admin_client::AdminClient { - iot_config::admin_client::AdminClient::new(channel) - } - pub fn keypair(&self) -> Result> { - let data = std::fs::read(&self.keypair).map_err(helium_crypto::Error::from)?; - Ok(helium_crypto::Keypair::try_from(&data[..])?) - } -} diff --git a/iot_packet_verifier/src/verifier.rs b/iot_packet_verifier/src/verifier.rs index e2a85084b..45e01395a 100644 --- a/iot_packet_verifier/src/verifier.rs +++ b/iot_packet_verifier/src/verifier.rs @@ -201,6 +201,7 @@ impl ConfigServer for CachedOrgClient { let mut req = OrgEnableReqV1 { oui, timestamp: Utc::now().timestamp_millis() as u64, + signer: self.keypair.public_key().into(), signature: vec![], }; let signature = self.keypair.sign(&req.encode_to_vec())?; @@ -215,6 +216,7 @@ impl ConfigServer for CachedOrgClient { let mut req = OrgDisableReqV1 { oui, timestamp: Utc::now().timestamp_millis() as u64, + signer: self.keypair.public_key().into(), signature: vec![], }; let signature = self.keypair.sign(&req.encode_to_vec())?; diff --git a/iot_verifier/Cargo.toml b/iot_verifier/Cargo.toml index 009b68ad3..2b37dd09a 100644 --- a/iot_verifier/Cargo.toml +++ b/iot_verifier/Cargo.toml @@ -42,7 +42,7 @@ file-store = { path = "../file_store" } metrics = {workspace = true} retainer = {workspace = true} blake3 = {workspace = true} -iot-config-client = { path = "../iot_config_client" } +iot-config = { path = "../iot_config" } poc-metrics = { path = "../metrics" } db-store = {path = "../db_store"} denylist = {path = "../denylist"} diff --git a/iot_verifier/src/gateway_cache.rs b/iot_verifier/src/gateway_cache.rs index 4f42e990b..2a7eec33c 100644 --- a/iot_verifier/src/gateway_cache.rs +++ b/iot_verifier/src/gateway_cache.rs @@ -1,8 +1,8 @@ use futures::stream::StreamExt; use helium_crypto::PublicKeyBinary; -use iot_config_client::{ - gateway_resolver::{GatewayInfo, GatewayInfoResolver}, - iot_config_client::{IotConfigClient, IotConfigClientError}, +use iot_config::{ + client::{Client as IotConfigClient, ClientError as IotConfigClientError}, + gateway_info::{GatewayInfo, GatewayInfoResolver}, }; use retainer::Cache; use std::{sync::Arc, time::Duration}; @@ -43,7 +43,11 @@ impl GatewayCache { pub async fn prewarm(&self) -> anyhow::Result<()> { tracing::info!("starting prewarming gateway cache"); - let mut gw_stream = self.iot_config_client.clone().gateway_stream().await?; + let mut gw_stream = self + .iot_config_client + .clone() + .stream_gateways_info() + .await?; while let Some(gateway_info) = gw_stream.next().await { _ = self.insert(gateway_info).await; } @@ -67,7 +71,7 @@ impl GatewayCache { .resolve_gateway_info(address) .await { - Ok(res) => { + Ok(Some(res)) => { tracing::debug!("cache miss: {:?}", address); metrics::increment_counter!("oracles_iot_verifier_gateway_cache_miss"); _ = self.insert(res.clone()).await; diff --git a/iot_verifier/src/main.rs b/iot_verifier/src/main.rs index 7066ae2f0..ef45c3b6a 100644 --- a/iot_verifier/src/main.rs +++ b/iot_verifier/src/main.rs @@ -6,7 +6,7 @@ use file_store::{ file_upload, iot_packet::IotValidPacket, FileStore, FileType, }; use futures::TryFutureExt; -use iot_config_client::iot_config_client::IotConfigClient; +use iot_config::client::Client as IotConfigClient; use iot_verifier::{ entropy_loader, gateway_cache::GatewayCache, loader, metrics::Metrics, packet_loader, poc_report::Report, purger, region_cache::RegionCache, rewarder::Rewarder, runner, diff --git a/iot_verifier/src/poc.rs b/iot_verifier/src/poc.rs index c37dcf3ba..ce1b406ea 100644 --- a/iot_verifier/src/poc.rs +++ b/iot_verifier/src/poc.rs @@ -20,7 +20,7 @@ use helium_proto::{ services::poc_lora::{InvalidParticipantSide, InvalidReason, VerificationStatus}, BlockchainRegionParamV1, Region as ProtoRegion, }; -use iot_config_client::gateway_resolver::GatewayInfo; +use iot_config::gateway_info::{GatewayInfo, GatewayMetadata}; use lazy_static::lazy_static; use rust_decimal::Decimal; use sqlx::PgPool; @@ -111,20 +111,31 @@ impl Poc { // it not available then declare beacon invalid let beaconer_info = match gateway_cache.resolve_gateway_info(&beaconer_pub_key).await { Ok(res) => res, - Err(_e) => { + Err(_err) => { return Ok(VerifyBeaconResult::gateway_not_found()); } }; - let beaconer_region_info = - match region_cache.resolve_region_info(beaconer_info.region).await { - Ok(res) => res, - Err(_e) => { - return Ok(VerifyBeaconResult::invalid( - InvalidReason::InvalidRegion, - beaconer_info, - )); - } - }; + let beaconer_metadata = match beaconer_info.metadata { + Some(ref metadata) => metadata, + None => { + return Ok(VerifyBeaconResult::invalid( + InvalidReason::NotAsserted, + beaconer_info, + )) + } + }; + let beaconer_region_info = match region_cache + .resolve_region_info(beaconer_metadata.region) + .await + { + Ok(res) => res, + Err(_e) => { + return Ok(VerifyBeaconResult::invalid( + InvalidReason::InvalidRegion, + beaconer_info, + )); + } + }; // we have beaconer info, proceed to verifications let last_beacon = LastBeacon::get(pool, beaconer_pub_key.as_ref()).await?; match do_beacon_verifications( @@ -139,12 +150,8 @@ impl Poc { beacon_interval_tolerance, ) { Ok(()) => { - let beaconer_location = beaconer_info - .location - .ok_or(VerificationError::NotFound("invalid beaconer_location"))?; - let tx_scale = hex_density_map - .get(beaconer_location) + .get(beaconer_metadata.location) .await .unwrap_or(*DEFAULT_TX_SCALE); Ok(VerifyBeaconResult::valid(beaconer_info, tx_scale)) @@ -226,6 +233,29 @@ impl Poc { )) } }; + let witness_metadata = match witness_info.metadata { + Some(ref metadata) => metadata, + None => { + return Ok(IotVerifiedWitnessReport::invalid( + InvalidReason::NotAsserted, + &witness_report.report, + witness_report.received_timestamp, + None, + InvalidParticipantSide::Witness, + )) + } + }; + // to avoid assuming beaconer location is set and to avoid unwrap + // we explicity match location here again + let Some(ref beaconer_metadata) = beaconer_info.metadata else { + return Ok(IotVerifiedWitnessReport::invalid( + InvalidReason::NotAsserted, + &witness_report.report, + witness_report.received_timestamp, + None, + InvalidParticipantSide::Beaconer, + )) + }; // run the witness verifications match do_witness_verifications( self.entropy_start, @@ -233,29 +263,17 @@ impl Poc { witness_report, &witness_info, &self.beacon_report, - beaconer_info, + beaconer_metadata, ) { Ok(()) => { - // to avoid assuming beaconer location is set and to avoid unwrap - // we explicity match location here again - let Some(beaconer_location) = beaconer_info.location else { - return Ok(IotVerifiedWitnessReport::invalid( - InvalidReason::NotAsserted, - &witness_report.report, - witness_report.received_timestamp, - None, - InvalidParticipantSide::Beaconer, - )) - }; - let tx_scale = hex_density_map - .get(beaconer_location) + .get(beaconer_metadata.location) .await .unwrap_or(*DEFAULT_TX_SCALE); Ok(IotVerifiedWitnessReport::valid( &witness_report.report, witness_report.received_timestamp, - witness_info.location, + Some(witness_metadata.location), tx_scale, )) } @@ -287,8 +305,11 @@ pub fn do_beacon_verifications( beaconer_info.address.clone() ); let beacon_received_ts = beacon_report.received_timestamp; + let beaconer_metadata = match beaconer_info.metadata { + Some(ref metadata) => metadata, + None => return Err(InvalidReason::NotAsserted), + }; verify_entropy(entropy_start, entropy_end, beacon_received_ts)?; - verify_gw_location(beaconer_info.location)?; verify_gw_capability(beaconer_info.is_full_hotspot)?; verify_beacon_schedule( &last_beacon, @@ -298,9 +319,9 @@ pub fn do_beacon_verifications( )?; verify_beacon_payload( &beacon_report.report, - beaconer_info.region, + beaconer_metadata.region, beaconer_region_params, - beaconer_info.gain, + beaconer_metadata.gain, entropy_start, entropy_version as u32, )?; @@ -317,13 +338,17 @@ pub fn do_witness_verifications( witness_report: &IotWitnessIngestReport, witness_info: &GatewayInfo, beacon_report: &IotBeaconIngestReport, - beaconer_info: &GatewayInfo, + beaconer_metadata: &GatewayMetadata, ) -> GenericVerifyResult { tracing::debug!( "verifying witness from gateway: {:?}", witness_info.address.clone() ); let beacon_report = &beacon_report; + let witness_metadata = match witness_info.metadata { + Some(ref metadata) => metadata, + None => return Err(InvalidReason::NotAsserted), + }; verify_self_witness( &beacon_report.report.pub_key, &witness_report.report.pub_key, @@ -334,23 +359,22 @@ pub fn do_witness_verifications( witness_report.received_timestamp, )?; verify_witness_data(&beacon_report.report.data, &witness_report.report.data)?; - verify_gw_location(witness_info.location)?; verify_gw_capability(witness_info.is_full_hotspot)?; verify_witness_freq( beacon_report.report.frequency, witness_report.report.frequency, )?; - verify_witness_region(beaconer_info.region, witness_info.region)?; - verify_witness_cell_distance(beaconer_info.location, witness_info.location)?; - verify_witness_distance(beaconer_info.location, witness_info.location)?; + verify_witness_region(beaconer_metadata.region, witness_metadata.region)?; + verify_witness_cell_distance(beaconer_metadata.location, witness_metadata.location)?; + verify_witness_distance(beaconer_metadata.location, witness_metadata.location)?; verify_witness_rssi( witness_report.report.signal, witness_report.report.frequency, beacon_report.report.tx_power, - beaconer_info.gain, - witness_info.gain, - beaconer_info.location, - witness_info.location, + beaconer_metadata.gain, + witness_metadata.gain, + beaconer_metadata.location, + witness_metadata.location, )?; tracing::debug!( "valid witness from gateway: {:?}", @@ -472,21 +496,6 @@ fn verify_beacon_payload( Ok(()) } -/// verify gateway has an asserted location -fn verify_gw_location(gateway_loc: Option) -> GenericVerifyResult { - match gateway_loc { - Some(location) => location, - None => { - tracing::debug!( - "beacon verification failed, reason: {:?}", - InvalidReason::NotAsserted - ); - return Err(InvalidReason::NotAsserted); - } - }; - Ok(()) -} - /// verify gateway is permitted to participate in POC fn verify_gw_capability(is_full_hotspot: bool) -> GenericVerifyResult { if !is_full_hotspot { @@ -544,17 +553,8 @@ fn verify_witness_region( } /// verify witness does not exceed max distance from beaconer -fn verify_witness_distance( - beacon_loc: Option, - witness_loc: Option, -) -> GenericVerifyResult { - // other verifications handle location checks but dont assume - // we have a valid location passed in here - // if no location for either beaconer or witness then default - // this verification to a fail - let l1 = beacon_loc.ok_or(InvalidReason::MaxDistanceExceeded)?; - let l2 = witness_loc.ok_or(InvalidReason::MaxDistanceExceeded)?; - let witness_distance = match calc_distance(l1, l2) { +fn verify_witness_distance(beacon_loc: u64, witness_loc: u64) -> GenericVerifyResult { + let witness_distance = match calc_distance(beacon_loc, witness_loc) { Ok(d) => d, Err(_) => return Err(InvalidReason::MaxDistanceExceeded), }; @@ -569,17 +569,8 @@ fn verify_witness_distance( } /// verify min hex distance between beaconer and witness -fn verify_witness_cell_distance( - beacon_loc: Option, - witness_loc: Option, -) -> GenericVerifyResult { - // other verifications handle location checks but dont assume - // we have a valid location passed in here - // if no location for either beaconer or witness then default - // this verification to a fail - let l1 = beacon_loc.ok_or(InvalidReason::BelowMinDistance)?; - let l2 = witness_loc.ok_or(InvalidReason::BelowMinDistance)?; - let cell_distance = match calc_cell_distance(l1, l2) { +fn verify_witness_cell_distance(beacon_loc: u64, witness_loc: u64) -> GenericVerifyResult { + let cell_distance = match calc_cell_distance(beacon_loc, witness_loc) { Ok(d) => d, Err(_) => return Err(InvalidReason::BelowMinDistance), }; @@ -600,17 +591,10 @@ fn verify_witness_rssi( beacon_tx_power: i32, beacon_gain: i32, witness_gain: i32, - beacon_loc: Option, - witness_loc: Option, + beacon_loc: u64, + witness_loc: u64, ) -> GenericVerifyResult { - // other verifications handle location checks but dont assume - // we have a valid location passed in here - // if no location for either beaconer or witness or - // distance between the two cannot be determined - // then default this verification to a fail - let l1 = beacon_loc.ok_or(InvalidReason::BadRssi)?; - let l2 = witness_loc.ok_or(InvalidReason::BadRssi)?; - let distance = match calc_distance(l1, l2) { + let distance = match calc_distance(beacon_loc, witness_loc) { Ok(d) => d, Err(_) => return Err(InvalidReason::BadRssi), }; @@ -1008,12 +992,6 @@ mod tests { ); } - #[test] - fn test_verify_location() { - assert!(verify_gw_location(Some(LOC1)).is_ok()); - assert_eq!(Err(InvalidReason::NotAsserted), verify_gw_location(None)); - } - #[test] fn test_verify_capability() { assert!(verify_gw_capability(true).is_ok()); @@ -1070,10 +1048,10 @@ mod tests { let beacon_loc = LOC0; let witness1_loc = LOC1; let witness2_loc = LOC2; - assert!(verify_witness_distance(Some(beacon_loc), Some(witness1_loc)).is_ok()); + assert!(verify_witness_distance(beacon_loc, witness1_loc).is_ok()); assert_eq!( Err(InvalidReason::MaxDistanceExceeded), - verify_witness_distance(Some(beacon_loc), Some(witness2_loc)) + verify_witness_distance(beacon_loc, witness2_loc) ); } @@ -1086,10 +1064,10 @@ mod tests { // witness 1 location is 7 cells from the beaconer and thus invalid assert_eq!( Err(InvalidReason::BelowMinDistance), - verify_witness_cell_distance(Some(beacon_loc), Some(witness1_loc)) + verify_witness_cell_distance(beacon_loc, witness1_loc) ); // witness 2's location is 28 cells from the beaconer and thus valid - assert!(verify_witness_cell_distance(Some(beacon_loc), Some(witness2_loc)).is_ok()); + assert!(verify_witness_cell_distance(beacon_loc, witness2_loc).is_ok()); } #[test] @@ -1109,8 +1087,8 @@ mod tests { beacon1_tx_power, beacon1_gain, witness1_gain, - Some(beacon_loc), - Some(witness1_loc), + beacon_loc, + witness1_loc, ) .is_ok()); let beacon2_tx_power = 27; @@ -1126,8 +1104,8 @@ mod tests { beacon2_tx_power, beacon2_gain, witness2_gain, - Some(beacon_loc), - Some(witness2_loc), + beacon_loc, + witness2_loc, ) ); } @@ -1271,6 +1249,9 @@ mod tests { // create default data structs let beacon_report = valid_beacon_report(Utc::now() - Duration::minutes(2)); let beaconer_info = beaconer_gateway_info(Some(LOC0), ProtoRegion::Eu868, true); + let beaconer_metadata = beaconer_info + .metadata + .expect("beaconer should have metadata"); let witness_info = witness_gateway_info(Some(LOC4), ProtoRegion::Eu868, true); let entropy_start = Utc.timestamp_millis_opt(1676381847900).unwrap(); let entropy_end = entropy_start + Duration::minutes(3); @@ -1283,7 +1264,7 @@ mod tests { &witness_report1, &witness_info, &beacon_report, - &beaconer_info, + &beaconer_metadata, ); assert_eq!(Err(InvalidReason::SelfWitness), resp1); @@ -1295,7 +1276,7 @@ mod tests { &witness_report2, &witness_info, &beacon_report, - &beaconer_info, + &beaconer_metadata, ); assert_eq!(Err(InvalidReason::EntropyExpired), resp2); @@ -1307,7 +1288,7 @@ mod tests { &witness_report3, &witness_info, &beacon_report, - &beaconer_info, + &beaconer_metadata, ); assert_eq!(Err(InvalidReason::InvalidPacket), resp3); @@ -1320,7 +1301,7 @@ mod tests { &witness_report4, &witness_info4, &beacon_report, - &beaconer_info, + &beaconer_metadata, ); assert_eq!(Err(InvalidReason::NotAsserted), resp4); @@ -1332,7 +1313,7 @@ mod tests { &witness_report5, &witness_info, &beacon_report, - &beaconer_info, + &beaconer_metadata, ); assert_eq!(Err(InvalidReason::InvalidFrequency), resp5); @@ -1345,7 +1326,7 @@ mod tests { &witness_report6, &witness_info6, &beacon_report, - &beaconer_info, + &beaconer_metadata, ); assert_eq!(Err(InvalidReason::InvalidRegion), resp6); @@ -1358,7 +1339,7 @@ mod tests { &witness_report7, &witness_info7, &beacon_report, - &beaconer_info, + &beaconer_metadata, ); assert_eq!(Err(InvalidReason::BelowMinDistance), resp7); @@ -1371,7 +1352,7 @@ mod tests { &witness_report8, &witness_info8, &beacon_report, - &beaconer_info, + &beaconer_metadata, ); assert_eq!(Err(InvalidReason::MaxDistanceExceeded), resp8); @@ -1383,7 +1364,7 @@ mod tests { &witness_report9, &witness_info, &beacon_report, - &beaconer_info, + &beaconer_metadata, ); assert_eq!(Err(InvalidReason::BadRssi), resp9); @@ -1396,7 +1377,7 @@ mod tests { &witness_report10, &witness_info10, &beacon_report, - &beaconer_info, + &beaconer_metadata, ); assert_eq!(Err(InvalidReason::InvalidCapability), resp10); @@ -1409,7 +1390,7 @@ mod tests { &witness_report11, &witness_info11, &beacon_report, - &beaconer_info, + &beaconer_metadata, ); assert_eq!(Ok(()), resp11); } @@ -1419,13 +1400,16 @@ mod tests { region: ProtoRegion, is_full_hotspot: bool, ) -> GatewayInfo { - GatewayInfo { + let metadata = location.map(|location| GatewayMetadata { location, - address: PublicKeyBinary::from_str(PUBKEY1).unwrap(), - is_full_hotspot, gain: 12, - elevation: Some(100), + elevation: 100, region, + }); + GatewayInfo { + address: PublicKeyBinary::from_str(PUBKEY1).unwrap(), + is_full_hotspot, + metadata, } } @@ -1434,13 +1418,16 @@ mod tests { region: ProtoRegion, is_full_hotspot: bool, ) -> GatewayInfo { - GatewayInfo { + let metadata = location.map(|location| GatewayMetadata { location, - address: PublicKeyBinary::from_str(PUBKEY2).unwrap(), - is_full_hotspot, gain: 20, - elevation: Some(100), + elevation: 100, region, + }); + GatewayInfo { + address: PublicKeyBinary::from_str(PUBKEY2).unwrap(), + is_full_hotspot, + metadata, } } diff --git a/iot_verifier/src/region_cache.rs b/iot_verifier/src/region_cache.rs index 6ed8854b8..f891953e8 100644 --- a/iot_verifier/src/region_cache.rs +++ b/iot_verifier/src/region_cache.rs @@ -1,8 +1,7 @@ use helium_crypto::PublicKeyBinary; use helium_proto::Region as ProtoRegion; -use iot_config_client::{ - iot_config_client::{IotConfigClient, IotConfigClientError}, - region_params_resolver::{RegionParamsInfo, RegionParamsResolver}, +use iot_config::client::{ + Client as IotConfigClient, ClientError as IotConfigClientError, RegionParamsInfo, }; use retainer::Cache; use std::time::Duration; diff --git a/iot_verifier/src/runner.rs b/iot_verifier/src/runner.rs index bd03c581a..2337b2a64 100644 --- a/iot_verifier/src/runner.rs +++ b/iot_verifier/src/runner.rs @@ -333,9 +333,10 @@ impl Runner { VerificationStatus::Invalid => witness.reward_unit = Decimal::ZERO, }); + let location = beacon_info.metadata.map(|metadata| metadata.location); let valid_beacon_report = IotValidBeaconReport { received_timestamp: beacon_received_ts, - location: beacon_info.location, + location, hex_scale: beacon_verify_result .hex_scale .ok_or(RunnerError::NotFound("invalid hex scaling factor"))?, diff --git a/iot_verifier/src/settings.rs b/iot_verifier/src/settings.rs index 3db6917e8..679f53217 100644 --- a/iot_verifier/src/settings.rs +++ b/iot_verifier/src/settings.rs @@ -19,7 +19,7 @@ pub struct Settings { #[serde(default = "default_base_stale_period")] pub base_stale_period: i64, pub database: db_store::Settings, - pub iot_config_client: iot_config_client::Settings, + pub iot_config_client: iot_config::client::Settings, pub ingest: file_store::Settings, pub packet_ingest: file_store::Settings, diff --git a/iot_verifier/src/tx_scaler.rs b/iot_verifier/src/tx_scaler.rs index ce268fa65..b93ab9035 100644 --- a/iot_verifier/src/tx_scaler.rs +++ b/iot_verifier/src/tx_scaler.rs @@ -5,7 +5,10 @@ use crate::{ }; use chrono::{DateTime, Duration, Utc}; use futures::stream::StreamExt; -use iot_config_client::iot_config_client::{IotConfigClient, IotConfigClientError}; +use iot_config::{ + client::{Client as IotConfigClient, ClientError as IotConfigClientError}, + gateway_info::GatewayInfoResolver, +}; use sqlx::PgPool; use std::collections::HashMap; @@ -84,11 +87,11 @@ impl Server { .gateways_recent_activity(refresh_start) .await .map_err(sqlx::Error::from)?; - let mut gw_stream = self.iot_config_client.gateway_stream().await?; + let mut gw_stream = self.iot_config_client.stream_gateways_info().await?; while let Some(gateway_info) = gw_stream.next().await { - if let Some(h3index) = gateway_info.location { + if let Some(metadata) = gateway_info.metadata { if active_gateways.contains_key(&gateway_info.address.as_ref().to_vec()) { - global_map.increment_unclipped(h3index) + global_map.increment_unclipped(metadata.location) } } } From 85ee53e47583ab7f1a7b318833ed247d8ff332f4 Mon Sep 17 00:00:00 2001 From: jeffgrunewald Date: Wed, 5 Apr 2023 14:20:53 -0400 Subject: [PATCH 09/15] iot config gateway service uses helium db pool --- iot_config/src/client/mod.rs | 7 +------ iot_config/src/gateway_service.rs | 19 ++++++++++--------- iot_config/src/main.rs | 16 +++++++++++++--- iot_config/src/settings.rs | 3 +++ 4 files changed, 27 insertions(+), 18 deletions(-) diff --git a/iot_config/src/client/mod.rs b/iot_config/src/client/mod.rs index 41c1d68e7..4de77e5f0 100644 --- a/iot_config/src/client/mod.rs +++ b/iot_config/src/client/mod.rs @@ -114,12 +114,7 @@ impl gateway_info::GatewayInfoResolver for Client { .into_inner() .filter_map(|resp| async move { resp.ok() }) .map(move |resp| (resp, pubkey.clone())) - .filter_map(|(resp, pubkey)| async move { - match resp.verify(&pubkey) { - Ok(()) => Some(resp), - Err(_) => None, - } - }) + .filter_map(|(resp, pubkey)| async move { resp.verify(&pubkey).map(|_| resp).ok() }) .flat_map(|resp| stream::iter(resp.gateways.into_iter())) .map(gateway_info::GatewayInfo::from) .boxed(); diff --git a/iot_config/src/gateway_service.rs b/iot_config/src/gateway_service.rs index e6a3f0123..13de3c665 100644 --- a/iot_config/src/gateway_service.rs +++ b/iot_config/src/gateway_service.rs @@ -24,7 +24,7 @@ use tonic::{Request, Response, Status}; pub struct GatewayService { auth_cache: AuthCache, - pool: Pool, + metadata_pool: Pool, region_map: RegionMapReader, signing_key: Arc, } @@ -32,13 +32,13 @@ pub struct GatewayService { impl GatewayService { pub fn new( settings: &Settings, - pool: Pool, + metadata_pool: Pool, region_map: RegionMapReader, auth_cache: AuthCache, ) -> Result { Ok(Self { auth_cache, - pool, + metadata_pool, region_map, signing_key: Arc::new(settings.signing_keypair()?), }) @@ -82,7 +82,7 @@ impl iot_config::Gateway for GatewayService { let address: &PublicKeyBinary = &request.gateway.into(); - let location = gateway_info::db::get_info(&self.pool, address) + let location = gateway_info::db::get_info(&self.metadata_pool, address) .await .map_err(|_| Status::internal("error fetching gateway info"))? .map_or_else( @@ -139,9 +139,10 @@ impl iot_config::Gateway for GatewayService { format!("invalid lora region {}", request.region), ))?; - let (region, gain) = if let Some(info) = gateway_info::db::get_info(&self.pool, address) - .await - .map_err(|_| Status::internal("error fetching gateway info"))? + let (region, gain) = if let Some(info) = + gateway_info::db::get_info(&self.metadata_pool, address) + .await + .map_err(|_| Status::internal("error fetching gateway info"))? { if let (Some(location), Some(gain)) = (info.location, info.gain) { let region = match hextree::Cell::from_raw(location) { @@ -208,7 +209,7 @@ impl iot_config::Gateway for GatewayService { self.verify_request_signature(&signer, &request)?; let address = &request.address.into(); - let metadata_info = gateway_info::db::get_info(&self.pool, address) + let metadata_info = gateway_info::db::get_info(&self.metadata_pool, address) .await .map_err(|_| Status::internal("error fetching gateway info"))? .ok_or(Status::not_found(format!( @@ -241,7 +242,7 @@ impl iot_config::Gateway for GatewayService { tracing::debug!("fetching all gateways' info"); - let pool = self.pool.clone(); + let pool = self.metadata_pool.clone(); let signing_key = self.signing_key.clone(); let batch_size = request.batch_size; let region_map = self.region_map.clone(); diff --git a/iot_config/src/main.rs b/iot_config/src/main.rs index 6db2a69d2..62d67e609 100644 --- a/iot_config/src/main.rs +++ b/iot_config/src/main.rs @@ -71,10 +71,16 @@ impl Daemon { // Create database pool let (pool, db_join_handle) = settings .database - .connect(env!("CARGO_PKG_NAME"), shutdown_listener.clone()) + .connect("iot-config-store", shutdown_listener.clone()) .await?; sqlx::migrate!().run(&pool).await?; + // Create on-chain metadata pool + let (metadata_pool, md_pool_handle) = settings + .metadata + .connect("iot-config-metadata", shutdown_listener.clone()) + .await?; + let listen_addr = settings.listen_addr()?; let (auth_updater, auth_cache) = AuthCache::new(settings, &pool).await?; @@ -82,7 +88,7 @@ impl Daemon { let gateway_svc = GatewayService::new( settings, - pool.clone(), + metadata_pool, region_map.clone(), auth_cache.clone(), )?; @@ -124,7 +130,11 @@ impl Daemon { .serve_with_shutdown(listen_addr, shutdown_listener) .map_err(Error::from); - tokio::try_join!(db_join_handle.map_err(Error::from), server)?; + tokio::try_join!( + db_join_handle.map_err(Error::from), + md_pool_handle.map_err(Error::from), + server + )?; Ok(()) } diff --git a/iot_config/src/settings.rs b/iot_config/src/settings.rs index dd8a6aa32..8e4d825c1 100644 --- a/iot_config/src/settings.rs +++ b/iot_config/src/settings.rs @@ -23,6 +23,9 @@ pub struct Settings { /// Network required in all public keys: mainnet | testnet pub network: Network, pub database: db_store::Settings, + /// Settings passed to the db_store crate for connecting to + /// the database for Solana on-chain data + pub metadata: db_store::Settings, pub metrics: poc_metrics::Settings, } From a43250072ef52f1cb0639dc1d412322e56ae63be Mon Sep 17 00:00:00 2001 From: Jeff Grunewald Date: Thu, 6 Apr 2023 12:08:44 -0400 Subject: [PATCH 10/15] Update iot_config/src/client/settings.rs Co-authored-by: andymck --- iot_config/src/client/settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iot_config/src/client/settings.rs b/iot_config/src/client/settings.rs index 3c7d20550..28efa9ba3 100644 --- a/iot_config/src/client/settings.rs +++ b/iot_config/src/client/settings.rs @@ -6,7 +6,7 @@ pub struct Settings { /// grpc url to the iot config oracle server #[serde(with = "http_serde::uri")] pub url: http::Uri, - /// File from which to load keypair for signing config server requests + /// File from which to load keypair for signing config client requests pub signing_keypair: String, /// B58 encoded public key of the iot config server for verifying responses pub config_pubkey: String, From de56959961c9e87b42882c77099d9dcc715d6394 Mon Sep 17 00:00:00 2001 From: jeffgrunewald Date: Thu, 6 Apr 2023 12:26:32 -0400 Subject: [PATCH 11/15] specify admin-only api auth better where needed --- iot_config/src/admin_service.rs | 22 ++++++++++++++++++---- iot_config/src/org_service.rs | 20 +++++++++++++++----- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/iot_config/src/admin_service.rs b/iot_config/src/admin_service.rs index 2a8eba493..de1a5870a 100644 --- a/iot_config/src/admin_service.rs +++ b/iot_config/src/admin_service.rs @@ -49,7 +49,11 @@ impl AdminService { }) } - fn verify_request_signature(&self, signer: &PublicKey, request: &R) -> Result<(), Status> + fn verify_admin_request_signature( + &self, + signer: &PublicKey, + request: &R, + ) -> Result<(), Status> where R: MsgVerify, { @@ -59,6 +63,16 @@ impl AdminService { Ok(()) } + fn verify_request_signature(&self, signer: &PublicKey, request: &R) -> Result<(), Status> + where + R: MsgVerify, + { + self.auth_cache + .verify_signature(signer, request) + .map_err(|_| Status::permission_denied("invalid request signature"))?; + Ok(()) + } + fn verify_network(&self, public_key: PublicKey) -> Result { if self.required_network == public_key.network { Ok(public_key) @@ -91,7 +105,7 @@ impl iot_config::Admin for AdminService { let request = request.into_inner(); let signer = self.verify_public_key(&request.signer)?; - self.verify_request_signature(&signer, &request)?; + self.verify_admin_request_signature(&signer, &request)?; let key_type = request.key_type().into(); let pubkey = self @@ -137,7 +151,7 @@ impl iot_config::Admin for AdminService { let request = request.into_inner(); let signer = self.verify_public_key(&request.signer)?; - self.verify_request_signature(&signer, &request)?; + self.verify_admin_request_signature(&signer, &request)?; admin::remove_key(request.pubkey.clone().into(), &self.pool) .and_then(|deleted| async move { @@ -177,7 +191,7 @@ impl iot_config::Admin for AdminService { let request = request.into_inner(); let signer = self.verify_public_key(&request.signer)?; - self.verify_request_signature(&signer, &request)?; + self.verify_admin_request_signature(&signer, &request)?; let region = Region::from_i32(request.region).ok_or(Status::invalid_argument(format!( "invalid lora region {}", diff --git a/iot_config/src/org_service.rs b/iot_config/src/org_service.rs index 64ff478f7..2aba3bb9c 100644 --- a/iot_config/src/org_service.rs +++ b/iot_config/src/org_service.rs @@ -60,7 +60,7 @@ impl OrgService { .map_err(|_| Status::invalid_argument(format!("invalid public key: {bytes:?}"))) } - async fn verify_request_signature( + fn verify_admin_request_signature( &self, signer: &PublicKey, request: &R, @@ -74,6 +74,16 @@ impl OrgService { Ok(()) } + fn verify_request_signature(&self, signer: &PublicKey, request: &R) -> Result<(), Status> + where + R: MsgVerify, + { + self.auth_cache + .verify_signature(signer, request) + .map_err(|_| Status::permission_denied("invalid request signature"))?; + Ok(()) + } + fn sign_response(&self, response: &R) -> Result, Status> where R: Message, @@ -147,7 +157,7 @@ impl iot_config::Org for OrgService { let request = request.into_inner(); let signer = self.verify_public_key(&request.signer)?; - self.verify_request_signature(&signer, &request).await?; + self.verify_admin_request_signature(&signer, &request)?; let mut verify_keys: Vec<&[u8]> = vec![request.owner.as_ref(), request.payer.as_ref()]; let mut verify_delegates: Vec<&[u8]> = request @@ -219,7 +229,7 @@ impl iot_config::Org for OrgService { let request = request.into_inner(); let signer = self.verify_public_key(&request.signer)?; - self.verify_request_signature(&signer, &request).await?; + self.verify_admin_request_signature(&signer, &request)?; let mut verify_keys: Vec<&[u8]> = vec![request.owner.as_ref(), request.payer.as_ref()]; let mut verify_delegates: Vec<&[u8]> = request @@ -286,7 +296,7 @@ impl iot_config::Org for OrgService { let request = request.into_inner(); let signer = self.verify_public_key(&request.signer)?; - self.verify_request_signature(&signer, &request).await?; + self.verify_request_signature(&signer, &request)?; if !org::is_locked(request.oui, &self.pool) .await @@ -351,7 +361,7 @@ impl iot_config::Org for OrgService { let request = request.into_inner(); let signer = self.verify_public_key(&request.signer)?; - self.verify_request_signature(&signer, &request).await?; + self.verify_request_signature(&signer, &request)?; if org::is_locked(request.oui, &self.pool) .await From 328c14344eccec1eaa79f8f4266c71374b6246bd Mon Sep 17 00:00:00 2001 From: jeffgrunewald Date: Sat, 8 Apr 2023 11:55:47 -0400 Subject: [PATCH 12/15] implement cleanup nits --- iot_config/src/gateway_service.rs | 42 +++++++++++++------------------ iot_config/src/route_service.rs | 1 - 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/iot_config/src/gateway_service.rs b/iot_config/src/gateway_service.rs index 13de3c665..88e9147ea 100644 --- a/iot_config/src/gateway_service.rs +++ b/iot_config/src/gateway_service.rs @@ -84,31 +84,23 @@ impl iot_config::Gateway for GatewayService { let location = gateway_info::db::get_info(&self.metadata_pool, address) .await - .map_err(|_| Status::internal("error fetching gateway info"))? - .map_or_else( - || { - Err(Status::not_found(format!( - "gateway not found: pubkey = {address:}" - ))) - }, - |info| { - if let Some(location) = info.location { - Ok(location) - } else { - Err(Status::not_found(format!( - "gateway unasserted: pubkey = {address:}" - ))) - } - }, - )?; - - let location = Cell::from_raw(location) - .map_err(|_| { - Status::internal(format!( - "invalid h3 index location {location} for {address}" - )) - })? - .to_string(); + .map_err(|_| Status::internal("error fetching gateway info")) + .and_then(|opt| { + opt.ok_or_else(|| { + Status::not_found(format!("gateway not found: pubkey = {}", address.to_string())) + }) + }) + .and_then(|iot_metadata| { + iot_metadata.location.ok_or_else(|| { + Status::not_found(format!("gateway unasserted: pubkey = {}", address.to_string())) + }) + }) + .and_then(|location| { + Cell::from_raw(location).map_err(|_| { + Status::internal(format!("invalid h3 index location {location} for {}", address.to_string())) + }) + }) + .map(|cell| cell.to_string())?; let mut resp = GatewayLocationResV1 { location, diff --git a/iot_config/src/route_service.rs b/iot_config/src/route_service.rs index db21b6966..37b40b9fb 100644 --- a/iot_config/src/route_service.rs +++ b/iot_config/src/route_service.rs @@ -138,7 +138,6 @@ impl RouteService { route_id: &str, check_constraints: bool, ) -> Result { - // let admin_keys = self.auth_cache.get_keys().into_iter().filter(|(_, keytype)| keytype == &KeyType::Administrator).map(|(pubkey, _)| pubkey).collect(); let admin_keys = self.auth_cache.get_keys_by_type(KeyType::Administrator); DevAddrEuiValidator::new(route_id, admin_keys, &self.pool, check_constraints).await From 8ef0ebdfbd8b84510e779a01631582f6467b974e Mon Sep 17 00:00:00 2001 From: jeffgrunewald Date: Mon, 10 Apr 2023 15:23:34 -0400 Subject: [PATCH 13/15] restore proto dep to master branch --- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 09f368a8e..749fd49a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1105,7 +1105,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "beacon" version = "0.1.0" -source = "git+https://github.com/helium/gateway-rs.git?branch=main#3ad4f7ea4d0fd2f058340ad070982ca2b2f4a883" +source = "git+https://github.com/helium/gateway-rs.git?branch=main#d862bc9962866e575d7c6dd731d65cf849a586ed" dependencies = [ "base64 0.21.0", "byteorder", @@ -2909,7 +2909,7 @@ dependencies = [ [[package]] name = "helium-proto" version = "0.1.0" -source = "git+https://github.com/helium/proto?branch=master#4a5ac3f020dc18bbf075c6738375fb09c0dcd443" +source = "git+https://github.com/helium/proto?branch=master#40388d260fd3603f453a965dbc13f79470b5adcb" dependencies = [ "bytes", "prost", diff --git a/Cargo.toml b/Cargo.toml index 345035650..3044d5ac7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,12 +55,12 @@ sqlx = {version = "0", features = [ ]} helium-crypto = {version = "0.6.8", features=["sqlx-postgres", "multisig"]} -helium-proto = {git = "https://github.com/helium/proto", branch = "jg/oracle-admin-keys", features = ["services"]} +helium-proto = {git = "https://github.com/helium/proto", branch = "master", features = ["services"]} hextree = "*" solana-client = "1.14" solana-sdk = "1.14" reqwest = {version = "0", default-features=false, features = ["gzip", "json", "rustls-tls"]} -beacon = {git = "https://github.com/helium/gateway-rs.git", branch = "jg/temp-proto-upgrade"} +beacon = {git = "https://github.com/helium/gateway-rs.git", branch = "main"} humantime = "2" metrics = "0" metrics-exporter-prometheus = "0" From 7d7edfe9ddf2f2a62853a62b73263fbe8b4c2423 Mon Sep 17 00:00:00 2001 From: jeffgrunewald Date: Mon, 10 Apr 2023 15:33:24 -0400 Subject: [PATCH 14/15] final format and clippy check after rebase --- iot_config/src/admin_service.rs | 2 +- iot_config/src/gateway_service.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/iot_config/src/admin_service.rs b/iot_config/src/admin_service.rs index de1a5870a..1e970ec66 100644 --- a/iot_config/src/admin_service.rs +++ b/iot_config/src/admin_service.rs @@ -13,7 +13,7 @@ use helium_proto::{ self, AdminAddKeyReqV1, AdminKeyResV1, AdminLoadRegionReqV1, AdminLoadRegionResV1, AdminRemoveKeyReqV1, RegionParamsReqV1, RegionParamsResV1, }, - Message, + Message, Region, }; use sqlx::{Pool, Postgres}; use tokio::sync::watch; diff --git a/iot_config/src/gateway_service.rs b/iot_config/src/gateway_service.rs index 88e9147ea..c0198ab89 100644 --- a/iot_config/src/gateway_service.rs +++ b/iot_config/src/gateway_service.rs @@ -87,17 +87,19 @@ impl iot_config::Gateway for GatewayService { .map_err(|_| Status::internal("error fetching gateway info")) .and_then(|opt| { opt.ok_or_else(|| { - Status::not_found(format!("gateway not found: pubkey = {}", address.to_string())) + Status::not_found(format!("gateway not found: pubkey = {address}")) }) }) .and_then(|iot_metadata| { iot_metadata.location.ok_or_else(|| { - Status::not_found(format!("gateway unasserted: pubkey = {}", address.to_string())) + Status::not_found(format!("gateway unasserted: pubkey = {address}")) }) }) .and_then(|location| { Cell::from_raw(location).map_err(|_| { - Status::internal(format!("invalid h3 index location {location} for {}", address.to_string())) + Status::internal(format!( + "invalid h3 index location {location} for {address}" + )) }) }) .map(|cell| cell.to_string())?; From b0c265bb27728b760ba650383d72e6966c318508 Mon Sep 17 00:00:00 2001 From: Jeff Grunewald Date: Mon, 10 Apr 2023 15:58:22 -0400 Subject: [PATCH 15/15] convert "or's" to "or_else's" Co-authored-by: Matthew Plant --- iot_config/src/admin.rs | 2 +- iot_config/src/gateway_info.rs | 2 +- iot_config/src/gateway_service.rs | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/iot_config/src/admin.rs b/iot_config/src/admin.rs index 71addbdb4..b1554fcb5 100644 --- a/iot_config/src/admin.rs +++ b/iot_config/src/admin.rs @@ -99,7 +99,7 @@ impl KeyType { pub fn from_i32(v: i32) -> anyhow::Result { ProtoKeyType::from_i32(v) .map(|kt| kt.into()) - .ok_or(anyhow!("unsupported key type {}", v)) + .ok_or_else(|| anyhow!("unsupported key type {}", v)) } } diff --git a/iot_config/src/gateway_info.rs b/iot_config/src/gateway_info.rs index cd716fa8b..d4e971d96 100644 --- a/iot_config/src/gateway_info.rs +++ b/iot_config/src/gateway_info.rs @@ -62,7 +62,7 @@ fn h3index_to_region( ) -> anyhow::Result { hextree::Cell::from_raw(location) .map(|cell| region_map.get_region(cell))? - .ok_or(anyhow!("invalid region")) + .ok_or_else(|| anyhow!("invalid region")) } #[async_trait::async_trait] diff --git a/iot_config/src/gateway_service.rs b/iot_config/src/gateway_service.rs index c0198ab89..e424537a9 100644 --- a/iot_config/src/gateway_service.rs +++ b/iot_config/src/gateway_service.rs @@ -206,9 +206,7 @@ impl iot_config::Gateway for GatewayService { let metadata_info = gateway_info::db::get_info(&self.metadata_pool, address) .await .map_err(|_| Status::internal("error fetching gateway info"))? - .ok_or(Status::not_found(format!( - "gateway not found: pubkey = {address:}" - )))?; + .ok_or_else(|| Status::not_found(format!("gateway not found: pubkey = {address:}")))?; let gateway_info = GatewayInfo::chain_metadata_to_info(metadata_info, &self.region_map); let mut resp = GatewayInfoResV1 {