Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Remove VC response signing and fix HTTP error handling #5529

Merged
merged 16 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions beacon_node/http_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ use warp::hyper::Body;
use warp::sse::Event;
use warp::Reply;
use warp::{http::Response, Filter, Rejection};
use warp_utils::{query::multi_key_query, uor::UnifyingOrFilter};
use warp_utils::{query::multi_key_query, reject::convert_rejection, uor::UnifyingOrFilter};

const API_PREFIX: &str = "eth";

Expand Down Expand Up @@ -1771,7 +1771,7 @@ pub fn serve<T: BeaconChainTypes>(
)
.await
.map(|()| warp::reply::json(&()));
task_spawner::convert_rejection(result).await
convert_rejection(result).await
},
);

Expand Down Expand Up @@ -3710,12 +3710,12 @@ pub fn serve<T: BeaconChainTypes>(
.await;

if initial_result.is_err() {
return task_spawner::convert_rejection(initial_result).await;
return convert_rejection(initial_result).await;
}

// Await a response from the builder without blocking a
// `BeaconProcessor` worker.
task_spawner::convert_rejection(rx.await.unwrap_or_else(|_| {
convert_rejection(rx.await.unwrap_or_else(|_| {
Ok(warp::reply::with_status(
warp::reply::json(&"No response from channel"),
eth2::StatusCode::INTERNAL_SERVER_ERROR,
Expand Down
19 changes: 1 addition & 18 deletions beacon_node/http_api/src/task_spawner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::future::Future;
use tokio::sync::{mpsc::error::TrySendError, oneshot};
use types::EthSpec;
use warp::reply::{Reply, Response};
use warp_utils::reject::convert_rejection;

/// Maps a request to a queue in the `BeaconProcessor`.
#[derive(Clone, Copy)]
Expand Down Expand Up @@ -35,24 +36,6 @@ pub struct TaskSpawner<E: EthSpec> {
beacon_processor_send: Option<BeaconProcessorSend<E>>,
}

/// Convert a warp `Rejection` into a `Response`.
///
/// This function should *always* be used to convert rejections into responses. This prevents warp
/// from trying to backtrack in strange ways. See: https://github.com/sigp/lighthouse/issues/3404
pub async fn convert_rejection<T: Reply>(res: Result<T, warp::Rejection>) -> Response {
match res {
Ok(response) => response.into_response(),
Err(e) => match warp_utils::reject::handle_rejection(e).await {
Ok(reply) => reply.into_response(),
Err(_) => warp::reply::with_status(
warp::reply::json(&"unhandled error"),
eth2::StatusCode::INTERNAL_SERVER_ERROR,
)
.into_response(),
},
}
}

impl<E: EthSpec> TaskSpawner<E> {
pub fn new(beacon_processor_send: Option<BeaconProcessorSend<E>>) -> Self {
Self {
Expand Down
116 changes: 23 additions & 93 deletions common/eth2/src/lighthouse_vc/http_client.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
use super::{types::*, PK_LEN, SECRET_PREFIX};
use super::types::*;
use crate::Error;
use account_utils::ZeroizeString;
use bytes::Bytes;
use libsecp256k1::{Message, PublicKey, Signature};
use reqwest::{
header::{HeaderMap, HeaderValue},
IntoUrl,
};
use ring::digest::{digest, SHA256};
use sensitive_url::SensitiveUrl;
use serde::{de::DeserializeOwned, Serialize};
use std::fmt::{self, Display};
Expand All @@ -24,8 +21,7 @@ use types::graffiti::GraffitiString;
pub struct ValidatorClientHttpClient {
client: reqwest::Client,
server: SensitiveUrl,
secret: Option<ZeroizeString>,
server_pubkey: Option<PublicKey>,
api_token: Option<ZeroizeString>,
authorization_header: AuthorizationHeader,
}

Expand All @@ -46,45 +42,13 @@ impl Display for AuthorizationHeader {
}
}

/// Parse an API token and return a secp256k1 public key.
///
/// If the token does not start with the Lighthouse token prefix then `Ok(None)` will be returned.
/// An error will be returned if the token looks like a Lighthouse token but doesn't correspond to a
/// valid public key.
pub fn parse_pubkey(secret: &str) -> Result<Option<PublicKey>, Error> {
let secret = if !secret.starts_with(SECRET_PREFIX) {
return Ok(None);
} else {
&secret[SECRET_PREFIX.len()..]
};

serde_utils::hex::decode(secret)
.map_err(|e| Error::InvalidSecret(format!("invalid hex: {:?}", e)))
.and_then(|bytes| {
if bytes.len() != PK_LEN {
return Err(Error::InvalidSecret(format!(
"expected {} bytes not {}",
PK_LEN,
bytes.len()
)));
}

let mut arr = [0; PK_LEN];
arr.copy_from_slice(&bytes);
PublicKey::parse_compressed(&arr)
.map_err(|e| Error::InvalidSecret(format!("invalid secp256k1 pubkey: {:?}", e)))
})
.map(Some)
}

impl ValidatorClientHttpClient {
/// Create a new client pre-initialised with an API token.
pub fn new(server: SensitiveUrl, secret: String) -> Result<Self, Error> {
Ok(Self {
client: reqwest::Client::new(),
server,
server_pubkey: parse_pubkey(&secret)?,
secret: Some(secret.into()),
api_token: Some(secret.into()),
authorization_header: AuthorizationHeader::Bearer,
})
michaelsproul marked this conversation as resolved.
Show resolved Hide resolved
}
Expand All @@ -96,8 +60,7 @@ impl ValidatorClientHttpClient {
Ok(Self {
client: reqwest::Client::new(),
server,
secret: None,
server_pubkey: None,
api_token: None,
authorization_header: AuthorizationHeader::Omit,
})
}
Expand All @@ -110,15 +73,14 @@ impl ValidatorClientHttpClient {
Ok(Self {
client,
server,
server_pubkey: parse_pubkey(&secret)?,
secret: Some(secret.into()),
api_token: Some(secret.into()),
authorization_header: AuthorizationHeader::Bearer,
})
}

/// Get a reference to this client's API token, if any.
pub fn api_token(&self) -> Option<&ZeroizeString> {
self.secret.as_ref()
self.api_token.as_ref()
}

/// Read an API token from the specified `path`, stripping any trailing whitespace.
Expand All @@ -128,19 +90,11 @@ impl ValidatorClientHttpClient {
}

/// Add an authentication token to use when making requests.
///
/// If the token is Lighthouse-like, a pubkey derivation will be attempted. In the case
/// of failure the token will still be stored, and the client can continue to be used to
/// communicate with non-Lighthouse nodes.
pub fn add_auth_token(&mut self, token: ZeroizeString) -> Result<(), Error> {
let pubkey_res = parse_pubkey(token.as_str());

self.secret = Some(token);
self.api_token = Some(token);
self.authorization_header = AuthorizationHeader::Bearer;

pubkey_res.map(|opt_pubkey| {
self.server_pubkey = opt_pubkey;
})
Ok(())
}

/// Set to `false` to disable sending the `Authorization` header on requests.
Expand All @@ -160,49 +114,17 @@ impl ValidatorClientHttpClient {
self.authorization_header = AuthorizationHeader::Basic;
}

async fn signed_body(&self, response: Response) -> Result<Bytes, Error> {
let server_pubkey = self.server_pubkey.as_ref().ok_or(Error::NoServerPubkey)?;
let sig = response
.headers()
.get("Signature")
.ok_or(Error::MissingSignatureHeader)?
.to_str()
.map_err(|_| Error::InvalidSignatureHeader)?
.to_string();

let body = response.bytes().await.map_err(Error::from)?;

let message =
Message::parse_slice(digest(&SHA256, &body).as_ref()).expect("sha256 is 32 bytes");

serde_utils::hex::decode(&sig)
.ok()
.and_then(|bytes| {
let sig = Signature::parse_der(&bytes).ok()?;
Some(libsecp256k1::verify(&message, &sig, server_pubkey))
})
.filter(|is_valid| *is_valid)
.ok_or(Error::InvalidSignatureHeader)?;

Ok(body)
}

async fn signed_json<T: DeserializeOwned>(&self, response: Response) -> Result<T, Error> {
let body = self.signed_body(response).await?;
serde_json::from_slice(&body).map_err(Error::InvalidJson)
}

fn headers(&self) -> Result<HeaderMap, Error> {
let mut headers = HeaderMap::new();

if self.authorization_header == AuthorizationHeader::Basic
|| self.authorization_header == AuthorizationHeader::Bearer
{
let secret = self.secret.as_ref().ok_or(Error::NoToken)?;
let auth_header_token = self.api_token().ok_or(Error::NoToken)?;
let header_value = HeaderValue::from_str(&format!(
"{} {}",
self.authorization_header,
secret.as_str()
auth_header_token.as_str()
))
.map_err(|e| {
Error::InvalidSecret(format!("secret is invalid as a header value: {}", e))
Expand Down Expand Up @@ -240,7 +162,8 @@ impl ValidatorClientHttpClient {

async fn get<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<T, Error> {
let response = self.get_response(url).await?;
self.signed_json(response).await
let body = response.bytes().await.map_err(Error::from)?;
serde_json::from_slice(&body).map_err(Error::InvalidJson)
}

async fn delete<U: IntoUrl>(&self, url: U) -> Result<(), Error> {
Expand All @@ -263,7 +186,14 @@ impl ValidatorClientHttpClient {
/// Perform a HTTP GET request, returning `None` on a 404 error.
async fn get_opt<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<Option<T>, Error> {
match self.get_response(url).await {
Ok(resp) => self.signed_json(resp).await.map(Option::Some),
Ok(resp) => {
let body = resp.bytes().await.map(Option::Some)?;
if let Some(body) = body {
serde_json::from_slice(&body).map_err(Error::InvalidJson)
} else {
Ok(None)
}
}
Err(err) => {
if err.status() == Some(StatusCode::NOT_FOUND) {
Ok(None)
Expand Down Expand Up @@ -297,7 +227,8 @@ impl ValidatorClientHttpClient {
body: &T,
) -> Result<V, Error> {
let response = self.post_with_raw_response(url, body).await?;
self.signed_json(response).await
let body = response.bytes().await.map_err(Error::from)?;
serde_json::from_slice(&body).map_err(Error::InvalidJson)
}

async fn post_with_unsigned_response<T: Serialize, U: IntoUrl, V: DeserializeOwned>(
Expand All @@ -319,8 +250,7 @@ impl ValidatorClientHttpClient {
.send()
.await
.map_err(Error::from)?;
let response = ok_or_error(response).await?;
self.signed_body(response).await?;
ok_or_error(response).await?;
Ok(())
}

Expand Down
7 changes: 0 additions & 7 deletions common/eth2/src/lighthouse_vc/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
pub mod http_client;
pub mod std_types;
pub mod types;

/// The number of bytes in the secp256k1 public key used as the authorization token for the VC API.
pub const PK_LEN: usize = 33;

/// The prefix for the secp256k1 public key when it is used as the authorization token for the VC
/// API.
pub const SECRET_PREFIX: &str = "api-token-";
20 changes: 19 additions & 1 deletion common/warp_utils/src/reject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use eth2::types::{ErrorMessage, Failure, IndexedErrorMessage};
use std::convert::Infallible;
use std::error::Error;
use std::fmt;
use warp::{http::StatusCode, reject::Reject};
use warp::{http::StatusCode, reject::Reject, reply::Response, Reply};

#[derive(Debug)]
pub struct ServerSentEventError(pub String);
Expand Down Expand Up @@ -243,3 +243,21 @@ pub async fn handle_rejection(err: warp::Rejection) -> Result<impl warp::Reply,

Ok(warp::reply::with_status(json, code))
}

/// Convert a warp `Rejection` into a `Response`.
///
/// This function should *always* be used to convert rejections into responses. This prevents warp
/// from trying to backtrack in strange ways. See: https://github.com/sigp/lighthouse/issues/3404
pub async fn convert_rejection<T: Reply>(res: Result<T, warp::Rejection>) -> Response {
match res {
Ok(response) => response.into_response(),
Err(e) => match handle_rejection(e).await {
Ok(reply) => reply.into_response(),
Err(_) => warp::reply::with_status(
warp::reply::json(&"unhandled error"),
eth2::StatusCode::INTERNAL_SERVER_ERROR,
)
.into_response(),
},
}
}
9 changes: 6 additions & 3 deletions common/warp_utils/src/task.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::reject::convert_rejection;
use serde::Serialize;
use warp::reply::{Reply, Response};

Expand All @@ -24,14 +25,16 @@ where
}

/// A convenience wrapper around `blocking_task` for use with `warp` JSON responses.
pub async fn blocking_json_task<F, T>(func: F) -> Result<Response, warp::Rejection>
pub async fn blocking_json_task<F, T>(func: F) -> Response
where
F: FnOnce() -> Result<T, warp::Rejection> + Send + 'static,
T: Serialize + Send + 'static,
{
blocking_response_task(|| {
let result = blocking_response_task(|| {
let response = func()?;
Ok(warp::reply::json(&response))
})
.await
.await;

convert_rejection(result).await
}
Loading
Loading