Skip to content

Commit

Permalink
Gateway service info perms (#792)
Browse files Browse the repository at this point in the history
* add constructor to make KeyCache without settings file

* Add test for who is authorized to request gateway info

I'm not sure where the mobile_hotspot_infos table exists, but I think
this test does well enough keying off expected grpc error statuses.

* Add verify method that allows gateway to request info about self

* use KeyTag::default in KeyPair generation for tests

* tighten gw info perm check to require proto request

The method before was named specifically for the info request, but the
function signature was much looser than the name implied.

Passing the request reduces the chance of messing up argument order, or
misunderstanding the arguments.

If at some point more methods require the ability for self
authorization, hopefully a better abstraction will reveal itself.
  • Loading branch information
michaeldjeffrey authored Apr 22, 2024
1 parent cb684d2 commit fe059f9
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 23 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 2 additions & 8 deletions ingest/tests/iot_ingest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::{net::SocketAddr, str::FromStr};

use backon::{ExponentialBuilder, Retryable};
use file_store::file_sink::{FileSinkClient, Message as SinkMessage};
use helium_crypto::{KeyTag, KeyType, Keypair, Network, PublicKey, Sign};
use helium_crypto::{KeyTag, Keypair, Network, PublicKey, Sign};
use helium_proto::services::poc_lora::{
lora_stream_request_v1::Request as StreamRequest,
lora_stream_response_v1::Response as StreamResponse, poc_lora_client::PocLoraClient,
Expand Down Expand Up @@ -555,13 +555,7 @@ fn create_test_server(
}

fn generate_keypair() -> Keypair {
Keypair::generate(
KeyTag {
network: Network::MainNet,
key_type: KeyType::Ed25519,
},
&mut OsRng,
)
Keypair::generate(KeyTag::default(), &mut OsRng)
}

fn seconds(s: u64) -> std::time::Duration {
Expand Down
10 changes: 2 additions & 8 deletions iot_config/tests/route_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{net::SocketAddr, str::FromStr, sync::Arc};
use backon::{ExponentialBuilder, Retryable};
use chrono::Utc;
use futures::{Future, StreamExt, TryFutureExt};
use helium_crypto::{KeyTag, KeyType as CryptoKeyType, Keypair, Network, PublicKey, Sign};
use helium_crypto::{KeyTag, Keypair, PublicKey, Sign};
use helium_proto::services::iot_config::{
self as proto, config_org_client::OrgClient, config_route_client::RouteClient, RouteStreamReqV1,
};
Expand Down Expand Up @@ -487,13 +487,7 @@ fn socket_addr(port: u64) -> anyhow::Result<SocketAddr> {
}

fn generate_keypair() -> Keypair {
Keypair::generate(
KeyTag {
network: Network::MainNet,
key_type: CryptoKeyType::Ed25519,
},
&mut OsRng,
)
Keypair::generate(KeyTag::default(), &mut OsRng)
}

fn get_port() -> u64 {
Expand Down
4 changes: 4 additions & 0 deletions mobile_config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,7 @@ tracing-subscriber = {workspace = true}
triggered = {workspace = true}
task-manager = { path = "../task_manager" }
solana-sdk = {workspace = true}

[dev-dependencies]
rand = { workspace = true }
tokio-stream = { workspace = true, features = ["net"] }
15 changes: 13 additions & 2 deletions mobile_config/src/gateway_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ impl GatewayService {
Err(Status::permission_denied("unauthorized request signature"))
}

fn verify_request_signature_for_info(&self, request: &GatewayInfoReqV1) -> Result<(), Status> {
let signer = verify_public_key(&request.signer)?;
let address = verify_public_key(&request.address)?;

if address == signer && request.verify(&signer).is_ok() {
tracing::debug!(%signer, "self authorized");
return Ok(());
}

self.verify_request_signature(&signer, request)
}

fn sign_response(&self, response: &[u8]) -> Result<Vec<u8>, Status> {
self.signing_key
.sign(response)
Expand All @@ -60,8 +72,7 @@ impl mobile_config::Gateway for GatewayService {
let request = request.into_inner();
telemetry::count_request("gateway", "info");

let signer = verify_public_key(&request.signer)?;
self.verify_request_signature(&signer, &request)?;
self.verify_request_signature_for_info(&request)?;

let pubkey: PublicKeyBinary = request.address.into();
tracing::debug!(pubkey = pubkey.to_string(), "fetching gateway info");
Expand Down
12 changes: 8 additions & 4 deletions mobile_config/src/key_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ pub struct KeyCache {
}

impl KeyCache {
pub async fn new(
pub fn new(stored_keys: CacheKeys) -> (watch::Sender<CacheKeys>, Self) {
let (cache_sender, cache_receiver) = watch::channel(stored_keys);

(cache_sender, Self { cache_receiver })
}

pub async fn from_settings(
settings: &Settings,
db: impl sqlx::PgExecutor<'_> + Copy,
) -> anyhow::Result<(watch::Sender<CacheKeys>, Self)> {
Expand All @@ -22,9 +28,7 @@ impl KeyCache {
let mut stored_keys = db::fetch_stored_keys(db).await?;
stored_keys.insert((config_admin, KeyRole::Administrator));

let (cache_sender, cache_receiver) = watch::channel(stored_keys);

Ok((cache_sender, Self { cache_receiver }))
Ok(Self::new(stored_keys))
}

pub fn verify_signature<R>(&self, signer: &PublicKey, request: &R) -> anyhow::Result<()>
Expand Down
2 changes: 1 addition & 1 deletion mobile_config/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ impl Daemon {

let listen_addr = settings.listen_addr()?;

let (key_cache_updater, key_cache) = KeyCache::new(settings, &pool).await?;
let (key_cache_updater, key_cache) = KeyCache::from_settings(settings, &pool).await?;

let admin_svc =
AdminService::new(settings, key_cache.clone(), key_cache_updater, pool.clone())?;
Expand Down
93 changes: 93 additions & 0 deletions mobile_config/tests/gateway_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use helium_crypto::{KeyTag, Keypair, PublicKey, Sign};
use helium_proto::services::mobile_config::{self as proto, GatewayClient};
use mobile_config::{
gateway_service::GatewayService,
key_cache::{CacheKeys, KeyCache},
KeyRole,
};
use prost::Message;
use sqlx::PgPool;
use tokio::net::TcpListener;
use tonic::{transport, Code};

#[sqlx::test]
async fn gateway_info_authorization_errors(pool: PgPool) -> anyhow::Result<()> {
// NOTE(mj): The information we're requesting does not exist in the DB for
// this test. But we're only interested in Authization Errors.

let admin_key = make_keypair(); // unlimited access
let gw_key = make_keypair(); // access to self
let unknown_key = make_keypair(); // no access
let server_key = make_keypair(); // signs responses

// Let the OS assign a port
let listener = TcpListener::bind("127.0.0.1:0").await?;
let addr = listener.local_addr()?;

// Start the gateway server
let keys = CacheKeys::from_iter([(admin_key.public_key().to_owned(), KeyRole::Administrator)]);
let (_key_cache_tx, key_cache) = KeyCache::new(keys);
let gws = GatewayService::new(key_cache, pool.clone(), server_key);
let _handle = tokio::spawn(
transport::Server::builder()
.add_service(proto::GatewayServer::new(gws))
.serve_with_incoming(tokio_stream::wrappers::TcpListenerStream::new(listener)),
);

// Connect with the assigned address
let mut client = GatewayClient::connect(format!("http://{addr}")).await?;

// Request information about ourselves
let req = make_signed_info_request(gw_key.public_key(), &gw_key);
let err = client.info(req).await.expect_err("testing expects error");
assert_ne!(
err.code(),
Code::PermissionDenied,
"gateway can request infomation about itself"
);

// Request gateway info as administrator
let req = make_signed_info_request(gw_key.public_key(), &admin_key);
let err = client.info(req).await.expect_err("testing expects error");
assert_ne!(
err.code(),
Code::PermissionDenied,
"admins have full access"
);

// Request gateway from unknown key
let req = make_signed_info_request(gw_key.public_key(), &unknown_key);
let err = client.info(req).await.expect_err("testing expects errors");
assert_eq!(
err.code(),
Code::PermissionDenied,
"unknown keys are denied"
);

// Request self with a different signer
let mut req = make_signed_info_request(gw_key.public_key(), &gw_key);
req.signature = vec![];
req.signature = admin_key.sign(&req.encode_to_vec()).unwrap();
let err = client.info(req).await.expect_err("testing expects errors");
assert_eq!(
err.code(),
Code::PermissionDenied,
"signature must match signer"
);

Ok(())
}

fn make_keypair() -> Keypair {
Keypair::generate(KeyTag::default(), &mut rand::rngs::OsRng)
}

fn make_signed_info_request(address: &PublicKey, signer: &Keypair) -> proto::GatewayInfoReqV1 {
let mut req = proto::GatewayInfoReqV1 {
address: address.to_vec(),
signer: signer.public_key().to_vec(),
signature: vec![],
};
req.signature = signer.sign(&req.encode_to_vec()).unwrap();
req
}

0 comments on commit fe059f9

Please sign in to comment.