@@ -7,6 +7,15 @@ use reqwest::{Client, Proxy};
77use crate :: into_url:: IntoUrl ;
88use crate :: OhttpKeys ;
99
10+ /// Upper bound on the size of an OHTTP key configuration response body.
11+ ///
12+ /// Derived from the ECHKeyConfig wire format: `key_id(1) + kem_id(2) +
13+ /// K-256 public key(65) + cipher suite vector length(2) + cipher suites` where
14+ /// the suite vector is u16-length-bounded (at most 65532 bytes of suites). Any
15+ /// larger response cannot decode and is rejected before being fully buffered
16+ /// to prevent memory exhaustion from a hostile payjoin directory.
17+ pub const MAX_OHTTP_KEYS_BODY_LEN : usize = 1 + 2 + 65 + 2 + u16:: MAX as usize - 3 ;
18+
1019/// Fetch the ohttp keys from the specified payjoin directory via proxy.
1120///
1221/// * `ohttp_relay`: The http CONNECT method proxy to request the ohttp keys from a payjoin
@@ -69,7 +78,21 @@ async fn parse_ohttp_keys_response(res: reqwest::Response) -> Result<OhttpKeys,
6978 return Err ( Error :: UnexpectedStatusCode ( res. status ( ) ) ) ;
7079 }
7180
72- let body = res. bytes ( ) . await ?. to_vec ( ) ;
81+ if let Some ( len) = res. content_length ( ) {
82+ if len as usize > MAX_OHTTP_KEYS_BODY_LEN {
83+ return Err ( Error :: OhttpKeysBodyTooLarge ( len) ) ;
84+ }
85+ }
86+
87+ let mut body = Vec :: with_capacity ( MAX_OHTTP_KEYS_BODY_LEN ) ;
88+ let mut res = res;
89+ while let Some ( chunk) = res. chunk ( ) . await ? {
90+ body. extend_from_slice ( & chunk) ;
91+ if body. len ( ) > MAX_OHTTP_KEYS_BODY_LEN {
92+ return Err ( Error :: OhttpKeysBodyTooLarge ( body. len ( ) as u64 ) ) ;
93+ }
94+ }
95+
7396 OhttpKeys :: decode ( & body) . map_err ( |e| {
7497 Error :: Internal ( InternalError ( InternalErrorInner :: InvalidOhttpKeys ( e. to_string ( ) ) ) )
7598 } )
@@ -80,6 +103,9 @@ async fn parse_ohttp_keys_response(res: reqwest::Response) -> Result<OhttpKeys,
80103pub enum Error {
81104 /// When the payjoin directory returns an unexpected status code
82105 UnexpectedStatusCode ( http:: StatusCode ) ,
106+ /// When the payjoin directory returns an OHTTP key configuration body
107+ /// larger than [`MAX_OHTTP_KEYS_BODY_LEN`]
108+ OhttpKeysBodyTooLarge ( u64 ) ,
83109 /// Internal errors that should not be pattern matched by users
84110 #[ doc( hidden) ]
85111 Internal ( InternalError ) ,
@@ -126,6 +152,10 @@ impl std::fmt::Display for Error {
126152 Self :: UnexpectedStatusCode ( code) => {
127153 write ! ( f, "Unexpected status code from payjoin directory: {code}" )
128154 }
155+ Self :: OhttpKeysBodyTooLarge ( len) => write ! (
156+ f,
157+ "OHTTP keys body of {len} bytes exceeds the maximum of {MAX_OHTTP_KEYS_BODY_LEN} bytes"
158+ ) ,
129159 Self :: Internal ( InternalError ( e) ) => e. fmt ( f) ,
130160 }
131161 }
@@ -153,6 +183,7 @@ impl std::error::Error for Error {
153183 match self {
154184 Self :: Internal ( InternalError ( e) ) => e. source ( ) ,
155185 Self :: UnexpectedStatusCode ( _) => None ,
186+ Self :: OhttpKeysBodyTooLarge ( _) => None ,
156187 }
157188 }
158189}
@@ -234,4 +265,33 @@ mod tests {
234265 "expected InvalidOhttpKeys error"
235266 ) ;
236267 }
268+
269+ #[ tokio:: test]
270+ async fn test_max_body_len_boundary ( ) {
271+ // number is literal pin of MAX_OHTTP_KEYS_BODY_LEN
272+ let response = mock_response ( StatusCode :: OK , vec ! [ 0u8 ; 65602 ] ) ;
273+ assert ! (
274+ matches!(
275+ parse_ohttp_keys_response( response) . await ,
276+ Err ( Error :: Internal ( InternalError ( InternalErrorInner :: InvalidOhttpKeys ( _) ) ) )
277+ ) ,
278+ "body of exactly MAX_OHTTP_KEYS_BODY_LEN must not be rejected as oversized"
279+ ) ;
280+ }
281+
282+ #[ tokio:: test]
283+ async fn test_parse_oversized_body_without_content_length ( ) {
284+ // number is literal pin of MAX_OHTTP_KEYS_BODY_LEN
285+ let oversized_body = vec ! [ 0u8 ; 65602 + 1 ] ;
286+
287+ let response = mock_response ( StatusCode :: OK , oversized_body) ;
288+
289+ assert ! (
290+ matches!(
291+ parse_ohttp_keys_response( response) . await ,
292+ Err ( Error :: OhttpKeysBodyTooLarge ( _) )
293+ ) ,
294+ "expected OhttpKeysBodyTooLarge error"
295+ ) ;
296+ }
237297}
0 commit comments