Skip to content

Commit bf2f0b0

Browse files
authored
Remove VC response signing and fix HTTP error handling (#5529)
* and_then to then remove expect move convert_rejection to utils remove signer from vc api * remove key * remove auth header * revert * Merge branch 'unstable' of https://github.com/sigp/lighthouse into vc-api-fix * merge unstable * revert * Merge branch 'unstable' of https://github.com/sigp/lighthouse into vc-api-fix * Merge branch 'unstable' of https://github.com/sigp/lighthouse into vc-api-fix * refactor blocking json task * linting * revert logging * remove response signing checks in validtor http_api client * remove notion of public key, prefixes, and simplify token generation * fmt * Remove outdated comment on public key
1 parent 77d491b commit bf2f0b0

File tree

8 files changed

+168
-444
lines changed

8 files changed

+168
-444
lines changed

beacon_node/http_api/src/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ use warp::hyper::Body;
9797
use warp::sse::Event;
9898
use warp::Reply;
9999
use warp::{http::Response, Filter, Rejection};
100-
use warp_utils::{query::multi_key_query, uor::UnifyingOrFilter};
100+
use warp_utils::{query::multi_key_query, reject::convert_rejection, uor::UnifyingOrFilter};
101101

102102
const API_PREFIX: &str = "eth";
103103

@@ -1802,7 +1802,7 @@ pub fn serve<T: BeaconChainTypes>(
18021802
)
18031803
.await
18041804
.map(|()| warp::reply::json(&()));
1805-
task_spawner::convert_rejection(result).await
1805+
convert_rejection(result).await
18061806
},
18071807
);
18081808

@@ -3817,12 +3817,12 @@ pub fn serve<T: BeaconChainTypes>(
38173817
.await;
38183818

38193819
if initial_result.is_err() {
3820-
return task_spawner::convert_rejection(initial_result).await;
3820+
return convert_rejection(initial_result).await;
38213821
}
38223822

38233823
// Await a response from the builder without blocking a
38243824
// `BeaconProcessor` worker.
3825-
task_spawner::convert_rejection(rx.await.unwrap_or_else(|_| {
3825+
convert_rejection(rx.await.unwrap_or_else(|_| {
38263826
Ok(warp::reply::with_status(
38273827
warp::reply::json(&"No response from channel"),
38283828
eth2::StatusCode::INTERNAL_SERVER_ERROR,

beacon_node/http_api/src/task_spawner.rs

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::future::Future;
44
use tokio::sync::{mpsc::error::TrySendError, oneshot};
55
use types::EthSpec;
66
use warp::reply::{Reply, Response};
7+
use warp_utils::reject::convert_rejection;
78

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

38-
/// Convert a warp `Rejection` into a `Response`.
39-
///
40-
/// This function should *always* be used to convert rejections into responses. This prevents warp
41-
/// from trying to backtrack in strange ways. See: https://github.com/sigp/lighthouse/issues/3404
42-
pub async fn convert_rejection<T: Reply>(res: Result<T, warp::Rejection>) -> Response {
43-
match res {
44-
Ok(response) => response.into_response(),
45-
Err(e) => match warp_utils::reject::handle_rejection(e).await {
46-
Ok(reply) => reply.into_response(),
47-
Err(_) => warp::reply::with_status(
48-
warp::reply::json(&"unhandled error"),
49-
eth2::StatusCode::INTERNAL_SERVER_ERROR,
50-
)
51-
.into_response(),
52-
},
53-
}
54-
}
55-
5639
impl<E: EthSpec> TaskSpawner<E> {
5740
pub fn new(beacon_processor_send: Option<BeaconProcessorSend<E>>) -> Self {
5841
Self {

common/eth2/src/lighthouse_vc/http_client.rs

Lines changed: 23 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
use super::{types::*, PK_LEN, SECRET_PREFIX};
1+
use super::types::*;
22
use crate::Error;
33
use account_utils::ZeroizeString;
4-
use bytes::Bytes;
5-
use libsecp256k1::{Message, PublicKey, Signature};
64
use reqwest::{
75
header::{HeaderMap, HeaderValue},
86
IntoUrl,
97
};
10-
use ring::digest::{digest, SHA256};
118
use sensitive_url::SensitiveUrl;
129
use serde::{de::DeserializeOwned, Serialize};
1310
use std::fmt::{self, Display};
@@ -24,8 +21,7 @@ use types::graffiti::GraffitiString;
2421
pub struct ValidatorClientHttpClient {
2522
client: reqwest::Client,
2623
server: SensitiveUrl,
27-
secret: Option<ZeroizeString>,
28-
server_pubkey: Option<PublicKey>,
24+
api_token: Option<ZeroizeString>,
2925
authorization_header: AuthorizationHeader,
3026
}
3127

@@ -46,45 +42,13 @@ impl Display for AuthorizationHeader {
4642
}
4743
}
4844

49-
/// Parse an API token and return a secp256k1 public key.
50-
///
51-
/// If the token does not start with the Lighthouse token prefix then `Ok(None)` will be returned.
52-
/// An error will be returned if the token looks like a Lighthouse token but doesn't correspond to a
53-
/// valid public key.
54-
pub fn parse_pubkey(secret: &str) -> Result<Option<PublicKey>, Error> {
55-
let secret = if !secret.starts_with(SECRET_PREFIX) {
56-
return Ok(None);
57-
} else {
58-
&secret[SECRET_PREFIX.len()..]
59-
};
60-
61-
serde_utils::hex::decode(secret)
62-
.map_err(|e| Error::InvalidSecret(format!("invalid hex: {:?}", e)))
63-
.and_then(|bytes| {
64-
if bytes.len() != PK_LEN {
65-
return Err(Error::InvalidSecret(format!(
66-
"expected {} bytes not {}",
67-
PK_LEN,
68-
bytes.len()
69-
)));
70-
}
71-
72-
let mut arr = [0; PK_LEN];
73-
arr.copy_from_slice(&bytes);
74-
PublicKey::parse_compressed(&arr)
75-
.map_err(|e| Error::InvalidSecret(format!("invalid secp256k1 pubkey: {:?}", e)))
76-
})
77-
.map(Some)
78-
}
79-
8045
impl ValidatorClientHttpClient {
8146
/// Create a new client pre-initialised with an API token.
8247
pub fn new(server: SensitiveUrl, secret: String) -> Result<Self, Error> {
8348
Ok(Self {
8449
client: reqwest::Client::new(),
8550
server,
86-
server_pubkey: parse_pubkey(&secret)?,
87-
secret: Some(secret.into()),
51+
api_token: Some(secret.into()),
8852
authorization_header: AuthorizationHeader::Bearer,
8953
})
9054
}
@@ -96,8 +60,7 @@ impl ValidatorClientHttpClient {
9660
Ok(Self {
9761
client: reqwest::Client::new(),
9862
server,
99-
secret: None,
100-
server_pubkey: None,
63+
api_token: None,
10164
authorization_header: AuthorizationHeader::Omit,
10265
})
10366
}
@@ -110,15 +73,14 @@ impl ValidatorClientHttpClient {
11073
Ok(Self {
11174
client,
11275
server,
113-
server_pubkey: parse_pubkey(&secret)?,
114-
secret: Some(secret.into()),
76+
api_token: Some(secret.into()),
11577
authorization_header: AuthorizationHeader::Bearer,
11678
})
11779
}
11880

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

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

13092
/// Add an authentication token to use when making requests.
131-
///
132-
/// If the token is Lighthouse-like, a pubkey derivation will be attempted. In the case
133-
/// of failure the token will still be stored, and the client can continue to be used to
134-
/// communicate with non-Lighthouse nodes.
13593
pub fn add_auth_token(&mut self, token: ZeroizeString) -> Result<(), Error> {
136-
let pubkey_res = parse_pubkey(token.as_str());
137-
138-
self.secret = Some(token);
94+
self.api_token = Some(token);
13995
self.authorization_header = AuthorizationHeader::Bearer;
14096

141-
pubkey_res.map(|opt_pubkey| {
142-
self.server_pubkey = opt_pubkey;
143-
})
97+
Ok(())
14498
}
14599

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

163-
async fn signed_body(&self, response: Response) -> Result<Bytes, Error> {
164-
let server_pubkey = self.server_pubkey.as_ref().ok_or(Error::NoServerPubkey)?;
165-
let sig = response
166-
.headers()
167-
.get("Signature")
168-
.ok_or(Error::MissingSignatureHeader)?
169-
.to_str()
170-
.map_err(|_| Error::InvalidSignatureHeader)?
171-
.to_string();
172-
173-
let body = response.bytes().await.map_err(Error::from)?;
174-
175-
let message =
176-
Message::parse_slice(digest(&SHA256, &body).as_ref()).expect("sha256 is 32 bytes");
177-
178-
serde_utils::hex::decode(&sig)
179-
.ok()
180-
.and_then(|bytes| {
181-
let sig = Signature::parse_der(&bytes).ok()?;
182-
Some(libsecp256k1::verify(&message, &sig, server_pubkey))
183-
})
184-
.filter(|is_valid| *is_valid)
185-
.ok_or(Error::InvalidSignatureHeader)?;
186-
187-
Ok(body)
188-
}
189-
190-
async fn signed_json<T: DeserializeOwned>(&self, response: Response) -> Result<T, Error> {
191-
let body = self.signed_body(response).await?;
192-
serde_json::from_slice(&body).map_err(Error::InvalidJson)
193-
}
194-
195117
fn headers(&self) -> Result<HeaderMap, Error> {
196118
let mut headers = HeaderMap::new();
197119

198120
if self.authorization_header == AuthorizationHeader::Basic
199121
|| self.authorization_header == AuthorizationHeader::Bearer
200122
{
201-
let secret = self.secret.as_ref().ok_or(Error::NoToken)?;
123+
let auth_header_token = self.api_token().ok_or(Error::NoToken)?;
202124
let header_value = HeaderValue::from_str(&format!(
203125
"{} {}",
204126
self.authorization_header,
205-
secret.as_str()
127+
auth_header_token.as_str()
206128
))
207129
.map_err(|e| {
208130
Error::InvalidSecret(format!("secret is invalid as a header value: {}", e))
@@ -240,7 +162,8 @@ impl ValidatorClientHttpClient {
240162

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

246169
async fn delete<U: IntoUrl>(&self, url: U) -> Result<(), Error> {
@@ -263,7 +186,14 @@ impl ValidatorClientHttpClient {
263186
/// Perform a HTTP GET request, returning `None` on a 404 error.
264187
async fn get_opt<T: DeserializeOwned, U: IntoUrl>(&self, url: U) -> Result<Option<T>, Error> {
265188
match self.get_response(url).await {
266-
Ok(resp) => self.signed_json(resp).await.map(Option::Some),
189+
Ok(resp) => {
190+
let body = resp.bytes().await.map(Option::Some)?;
191+
if let Some(body) = body {
192+
serde_json::from_slice(&body).map_err(Error::InvalidJson)
193+
} else {
194+
Ok(None)
195+
}
196+
}
267197
Err(err) => {
268198
if err.status() == Some(StatusCode::NOT_FOUND) {
269199
Ok(None)
@@ -297,7 +227,8 @@ impl ValidatorClientHttpClient {
297227
body: &T,
298228
) -> Result<V, Error> {
299229
let response = self.post_with_raw_response(url, body).await?;
300-
self.signed_json(response).await
230+
let body = response.bytes().await.map_err(Error::from)?;
231+
serde_json::from_slice(&body).map_err(Error::InvalidJson)
301232
}
302233

303234
async fn post_with_unsigned_response<T: Serialize, U: IntoUrl, V: DeserializeOwned>(
@@ -319,8 +250,7 @@ impl ValidatorClientHttpClient {
319250
.send()
320251
.await
321252
.map_err(Error::from)?;
322-
let response = ok_or_error(response).await?;
323-
self.signed_body(response).await?;
253+
ok_or_error(response).await?;
324254
Ok(())
325255
}
326256

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
11
pub mod http_client;
22
pub mod std_types;
33
pub mod types;
4-
5-
/// The number of bytes in the secp256k1 public key used as the authorization token for the VC API.
6-
pub const PK_LEN: usize = 33;
7-
8-
/// The prefix for the secp256k1 public key when it is used as the authorization token for the VC
9-
/// API.
10-
pub const SECRET_PREFIX: &str = "api-token-";

common/warp_utils/src/reject.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use eth2::types::{ErrorMessage, Failure, IndexedErrorMessage};
22
use std::convert::Infallible;
33
use std::error::Error;
44
use std::fmt;
5-
use warp::{http::StatusCode, reject::Reject};
5+
use warp::{http::StatusCode, reject::Reject, reply::Response, Reply};
66

77
#[derive(Debug)]
88
pub struct ServerSentEventError(pub String);
@@ -255,3 +255,21 @@ pub async fn handle_rejection(err: warp::Rejection) -> Result<impl warp::Reply,
255255

256256
Ok(warp::reply::with_status(json, code))
257257
}
258+
259+
/// Convert a warp `Rejection` into a `Response`.
260+
///
261+
/// This function should *always* be used to convert rejections into responses. This prevents warp
262+
/// from trying to backtrack in strange ways. See: https://github.com/sigp/lighthouse/issues/3404
263+
pub async fn convert_rejection<T: Reply>(res: Result<T, warp::Rejection>) -> Response {
264+
match res {
265+
Ok(response) => response.into_response(),
266+
Err(e) => match handle_rejection(e).await {
267+
Ok(reply) => reply.into_response(),
268+
Err(_) => warp::reply::with_status(
269+
warp::reply::json(&"unhandled error"),
270+
eth2::StatusCode::INTERNAL_SERVER_ERROR,
271+
)
272+
.into_response(),
273+
},
274+
}
275+
}

common/warp_utils/src/task.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::reject::convert_rejection;
12
use serde::Serialize;
23
use warp::reply::{Reply, Response};
34

@@ -24,14 +25,16 @@ where
2425
}
2526

2627
/// A convenience wrapper around `blocking_task` for use with `warp` JSON responses.
27-
pub async fn blocking_json_task<F, T>(func: F) -> Result<Response, warp::Rejection>
28+
pub async fn blocking_json_task<F, T>(func: F) -> Response
2829
where
2930
F: FnOnce() -> Result<T, warp::Rejection> + Send + 'static,
3031
T: Serialize + Send + 'static,
3132
{
32-
blocking_response_task(|| {
33+
let result = blocking_response_task(|| {
3334
let response = func()?;
3435
Ok(warp::reply::json(&response))
3536
})
36-
.await
37+
.await;
38+
39+
convert_rejection(result).await
3740
}

0 commit comments

Comments
 (0)