• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

payjoin / rust-payjoin / 17746390338

15 Sep 2025 08:55PM UTC coverage: 85.807%. First build
17746390338

push

github

nothingmuch
error if OH fragment param has incorrect length

The primary motivation for this commit is to reject trailing data, which
was not covered before.

This commit also makes the `InvalidFormat` variant of
`ParseOhttpKeysError` only defined in test configs, since
`IncorrectLength` is now also used when the value is too short, which
was previously represented as `InvalidFormat`.

14 of 18 new or added lines in 2 files covered. (77.78%)

8077 of 9413 relevant lines covered (85.81%)

496.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.16
/payjoin/src/core/ohttp.rs
1
use std::ops::{Deref, DerefMut};
2
use std::{error, fmt};
3

4
use bitcoin::bech32::{self, EncodeError};
5
use bitcoin::key::constants::UNCOMPRESSED_PUBLIC_KEY_SIZE;
6
use hpke::rand_core::{OsRng, RngCore};
7

8
use crate::directory::ENCAPSULATED_MESSAGE_BYTES;
9

10
const N_ENC: usize = UNCOMPRESSED_PUBLIC_KEY_SIZE;
11
const N_T: usize = crate::hpke::POLY1305_TAG_SIZE;
12
const OHTTP_REQ_HEADER_BYTES: usize = 7;
13
pub const PADDED_BHTTP_REQ_BYTES: usize =
14
    ENCAPSULATED_MESSAGE_BYTES - (N_ENC + N_T + OHTTP_REQ_HEADER_BYTES);
15

16
pub fn ohttp_encapsulate(
24✔
17
    ohttp_keys: &mut ohttp::KeyConfig,
24✔
18
    method: &str,
24✔
19
    target_resource: &str,
24✔
20
    body: Option<&[u8]>,
24✔
21
) -> Result<([u8; ENCAPSULATED_MESSAGE_BYTES], ohttp::ClientResponse), OhttpEncapsulationError> {
24✔
22
    use std::fmt::Write;
23

24
    let ctx = ohttp::ClientRequest::from_config(ohttp_keys)?;
24✔
25
    let url = url::Url::parse(target_resource)?;
24✔
26
    let authority_bytes = url.host().map_or_else(Vec::new, |host| {
24✔
27
        let mut authority = host.to_string();
24✔
28
        if let Some(port) = url.port() {
24✔
29
            write!(authority, ":{port}").unwrap();
20✔
30
        }
20✔
31
        authority.into_bytes()
24✔
32
    });
24✔
33
    let mut bhttp_message = bhttp::Message::request(
24✔
34
        method.as_bytes().to_vec(),
24✔
35
        url.scheme().as_bytes().to_vec(),
24✔
36
        authority_bytes,
24✔
37
        url.path().as_bytes().to_vec(),
24✔
38
    );
39
    // None of our messages include headers, so we don't add them
40
    if let Some(body) = body {
24✔
41
        bhttp_message.write_content(body);
16✔
42
    }
16✔
43

44
    let mut bhttp_req = [0u8; PADDED_BHTTP_REQ_BYTES];
24✔
45
    OsRng.fill_bytes(&mut bhttp_req);
24✔
46
    bhttp_message.write_bhttp(bhttp::Mode::KnownLength, &mut bhttp_req.as_mut_slice())?;
24✔
47
    let (encapsulated, ohttp_ctx) = ctx.encapsulate(&bhttp_req)?;
24✔
48

49
    let mut buffer = [0u8; ENCAPSULATED_MESSAGE_BYTES];
24✔
50
    let len = encapsulated.len().min(ENCAPSULATED_MESSAGE_BYTES);
24✔
51
    buffer[..len].copy_from_slice(&encapsulated[..len]);
24✔
52
    Ok((buffer, ohttp_ctx))
24✔
53
}
24✔
54

55
#[derive(Debug)]
56
pub enum DirectoryResponseError {
57
    InvalidSize(usize),
58
    OhttpDecapsulation(OhttpEncapsulationError),
59
    UnexpectedStatusCode(http::StatusCode),
60
}
61

62
impl fmt::Display for DirectoryResponseError {
63
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
64
        use DirectoryResponseError::*;
65

66
        match self {
×
67
            OhttpDecapsulation(e) => write!(f, "OHTTP Decapsulation Error: {e}"),
×
68
            InvalidSize(size) => write!(
×
69
                f,
×
70
                "Unexpected response size {}, expected {} bytes",
×
71
                size,
72
                crate::directory::ENCAPSULATED_MESSAGE_BYTES
73
            ),
74
            UnexpectedStatusCode(status) => write!(f, "Unexpected status code: {status}"),
×
75
        }
76
    }
×
77
}
78

79
impl error::Error for DirectoryResponseError {
80
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
81
        use DirectoryResponseError::*;
82

83
        match self {
×
84
            OhttpDecapsulation(e) => Some(e),
×
85
            InvalidSize(_) => None,
×
86
            UnexpectedStatusCode(_) => None,
×
87
        }
88
    }
×
89
}
90

91
pub fn process_get_res(
9✔
92
    res: &[u8],
9✔
93
    ohttp_context: ohttp::ClientResponse,
9✔
94
) -> Result<Option<Vec<u8>>, DirectoryResponseError> {
9✔
95
    let response = process_ohttp_res(res, ohttp_context)?;
9✔
96
    match response.status() {
9✔
97
        http::StatusCode::OK => Ok(Some(response.body().to_vec())),
6✔
98
        http::StatusCode::ACCEPTED => Ok(None),
3✔
99
        status_code => Err(DirectoryResponseError::UnexpectedStatusCode(status_code)),
×
100
    }
101
}
9✔
102

103
pub fn process_post_res(
6✔
104
    res: &[u8],
6✔
105
    ohttp_context: ohttp::ClientResponse,
6✔
106
) -> Result<(), DirectoryResponseError> {
6✔
107
    let response = process_ohttp_res(res, ohttp_context)?;
6✔
108
    match response.status() {
6✔
109
        http::StatusCode::OK => Ok(()),
6✔
110
        status_code => Err(DirectoryResponseError::UnexpectedStatusCode(status_code)),
×
111
    }
112
}
6✔
113

114
fn process_ohttp_res(
15✔
115
    res: &[u8],
15✔
116
    ohttp_context: ohttp::ClientResponse,
15✔
117
) -> Result<http::Response<Vec<u8>>, DirectoryResponseError> {
15✔
118
    let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] =
15✔
119
        res.try_into().map_err(|_| DirectoryResponseError::InvalidSize(res.len()))?;
15✔
120
    tracing::trace!("decapsulating directory response");
15✔
121
    let res = ohttp_decapsulate(ohttp_context, response_array)
15✔
122
        .map_err(DirectoryResponseError::OhttpDecapsulation)?;
15✔
123
    Ok(res)
15✔
124
}
15✔
125

126
/// decapsulate ohttp, bhttp response and return http response body and status code
127
pub fn ohttp_decapsulate(
15✔
128
    res_ctx: ohttp::ClientResponse,
15✔
129
    ohttp_body: &[u8; ENCAPSULATED_MESSAGE_BYTES],
15✔
130
) -> Result<http::Response<Vec<u8>>, OhttpEncapsulationError> {
15✔
131
    let bhttp_body = res_ctx.decapsulate(ohttp_body)?;
15✔
132
    let mut r = std::io::Cursor::new(bhttp_body);
15✔
133
    let m: bhttp::Message = bhttp::Message::read_bhttp(&mut r)?;
15✔
134
    let mut builder = http::Response::builder();
15✔
135
    for field in m.header().iter() {
15✔
136
        builder = builder.header(field.name(), field.value());
×
137
    }
×
138
    builder
15✔
139
        .status({
15✔
140
            let code = m.control().status().ok_or(bhttp::Error::InvalidStatus)?;
15✔
141

142
            http::StatusCode::from_u16(code.code()).map_err(|_| bhttp::Error::InvalidStatus)?
15✔
143
        })
144
        .body(m.content().to_vec())
15✔
145
        .map_err(OhttpEncapsulationError::Http)
15✔
146
}
15✔
147

148
/// Error from de/encapsulating an Oblivious HTTP request or response.
149
#[derive(Debug)]
150
pub enum OhttpEncapsulationError {
151
    Http(http::Error),
152
    Ohttp(ohttp::Error),
153
    Bhttp(bhttp::Error),
154
    ParseUrl(url::ParseError),
155
}
156

157
impl From<http::Error> for OhttpEncapsulationError {
158
    fn from(value: http::Error) -> Self { Self::Http(value) }
×
159
}
160

161
impl From<ohttp::Error> for OhttpEncapsulationError {
162
    fn from(value: ohttp::Error) -> Self { Self::Ohttp(value) }
×
163
}
164

165
impl From<bhttp::Error> for OhttpEncapsulationError {
166
    fn from(value: bhttp::Error) -> Self { Self::Bhttp(value) }
×
167
}
168

169
impl From<url::ParseError> for OhttpEncapsulationError {
170
    fn from(value: url::ParseError) -> Self { Self::ParseUrl(value) }
×
171
}
172

173
impl fmt::Display for OhttpEncapsulationError {
174
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
175
        use OhttpEncapsulationError::*;
176

177
        match &self {
×
178
            Http(e) => e.fmt(f),
×
179
            Ohttp(e) => e.fmt(f),
×
180
            Bhttp(e) => e.fmt(f),
×
181
            ParseUrl(e) => e.fmt(f),
×
182
        }
183
    }
×
184
}
185

186
impl error::Error for OhttpEncapsulationError {
187
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
188
        use OhttpEncapsulationError::*;
189

190
        match &self {
×
191
            Http(e) => Some(e),
×
192
            Ohttp(e) => Some(e),
×
193
            Bhttp(e) => Some(e),
×
194
            ParseUrl(e) => Some(e),
×
195
        }
196
    }
×
197
}
198

199
#[derive(Debug, Clone)]
200
pub struct OhttpKeys(pub ohttp::KeyConfig);
201

202
impl OhttpKeys {
203
    /// Decode an OHTTP KeyConfig
204
    pub fn decode(bytes: &[u8]) -> Result<Self, ohttp::Error> {
18✔
205
        ohttp::KeyConfig::decode(bytes).map(Self)
18✔
206
    }
18✔
207

208
    pub fn to_bytes(&self) -> Result<Vec<u8>, ohttp::Error> {
31✔
209
        let bytes = self.encode()?;
31✔
210

211
        let key_id = bytes[0];
31✔
212
        let uncompressed_pubkey = &bytes[3..68];
31✔
213

214
        let compressed_pubkey = bitcoin::secp256k1::PublicKey::from_slice(uncompressed_pubkey)
31✔
215
            .expect("serialization of public key should be deserializable without error")
31✔
216
            .serialize();
31✔
217

218
        let mut buf = vec![key_id];
31✔
219
        buf.extend_from_slice(&compressed_pubkey);
31✔
220

221
        Ok(buf)
31✔
222
    }
31✔
223
}
224

225
const KEM_ID: &[u8] = b"\x00\x16"; // DHKEM(secp256k1, HKDF-SHA256)
226
const SYMMETRIC_LEN: &[u8] = b"\x00\x04"; // 4 bytes
227
const SYMMETRIC_KDF_AEAD: &[u8] = b"\x00\x01\x00\x03"; // KDF(HKDF-SHA256), AEAD(ChaCha20Poly1305)
228

229
impl fmt::Display for OhttpKeys {
230
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28✔
231
        let buf = self.to_bytes().map_err(|_| fmt::Error)?;
28✔
232

233
        let oh_hrp: bech32::Hrp = bech32::Hrp::parse("OH").unwrap();
28✔
234

235
        crate::bech32::nochecksum::encode_to_fmt(f, oh_hrp, &buf).map_err(|e| match e {
28✔
236
            EncodeError::Fmt(e) => e,
×
237
            _ => fmt::Error,
×
238
        })
×
239
    }
28✔
240
}
241

242
impl TryFrom<&[u8]> for OhttpKeys {
243
    type Error = ParseOhttpKeysError;
244

245
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
19✔
246
        let buf: [u8; 34] =
17✔
247
            bytes.try_into().map_err(|_| ParseOhttpKeysError::IncorrectLength(bytes.len()))?;
19✔
248

249
        let key_id = buf[0];
17✔
250
        let compressed_pk = &buf[1..];
17✔
251

252
        let pubkey = bitcoin::secp256k1::PublicKey::from_slice(compressed_pk)
17✔
253
            .map_err(|_| ParseOhttpKeysError::InvalidPublicKey)?;
17✔
254

255
        let mut buf = vec![key_id];
17✔
256
        buf.extend_from_slice(KEM_ID);
17✔
257
        buf.extend_from_slice(&pubkey.serialize_uncompressed());
17✔
258
        buf.extend_from_slice(SYMMETRIC_LEN);
17✔
259
        buf.extend_from_slice(SYMMETRIC_KDF_AEAD);
17✔
260

261
        ohttp::KeyConfig::decode(&buf).map(Self).map_err(ParseOhttpKeysError::DecodeKeyConfig)
17✔
262
    }
19✔
263
}
264

265
#[cfg(test)]
266
impl std::str::FromStr for OhttpKeys {
267
    type Err = ParseOhttpKeysError;
268

269
    /// Parses a base64URL-encoded string into OhttpKeys.
270
    /// The string format is: key_id || compressed_public_key
271
    fn from_str(s: &str) -> Result<Self, Self::Err> {
2✔
272
        let oh_hrp: bech32::Hrp = bech32::Hrp::parse("OH").unwrap();
2✔
273

274
        let (hrp, bytes) =
2✔
275
            crate::bech32::nochecksum::decode(s).map_err(|_| ParseOhttpKeysError::InvalidFormat)?;
2✔
276

277
        if hrp != oh_hrp {
2✔
278
            return Err(ParseOhttpKeysError::InvalidFormat);
×
279
        }
2✔
280

281
        Self::try_from(&bytes[..])
2✔
282
    }
2✔
283
}
284

285
impl PartialEq for OhttpKeys {
286
    fn eq(&self, other: &Self) -> bool {
18✔
287
        match (self.encode(), other.encode()) {
18✔
288
            (Ok(self_encoded), Ok(other_encoded)) => self_encoded == other_encoded,
18✔
289
            // If OhttpKeys::encode(&self) is Err, return false
290
            _ => false,
×
291
        }
292
    }
18✔
293
}
294

295
impl Eq for OhttpKeys {}
296

297
impl Deref for OhttpKeys {
298
    type Target = ohttp::KeyConfig;
299

300
    fn deref(&self) -> &Self::Target { &self.0 }
75✔
301
}
302

303
impl DerefMut for OhttpKeys {
304
    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
19✔
305
}
306

307
impl<'de> serde::Deserialize<'de> for OhttpKeys {
308
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7✔
309
    where
7✔
310
        D: serde::Deserializer<'de>,
7✔
311
    {
312
        let bytes = Vec::<u8>::deserialize(deserializer)?;
7✔
313
        OhttpKeys::decode(&bytes).map_err(serde::de::Error::custom)
7✔
314
    }
7✔
315
}
316

317
impl serde::Serialize for OhttpKeys {
318
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
6✔
319
    where
6✔
320
        S: serde::Serializer,
6✔
321
    {
322
        let bytes = self.encode().map_err(serde::ser::Error::custom)?;
6✔
323
        bytes.serialize(serializer)
6✔
324
    }
6✔
325
}
326

327
#[derive(Debug)]
328
pub enum ParseOhttpKeysError {
329
    IncorrectLength(usize),
330
    InvalidPublicKey,
331
    DecodeKeyConfig(ohttp::Error),
332
    #[cfg(test)]
333
    InvalidFormat,
334
}
335

336
impl std::fmt::Display for ParseOhttpKeysError {
337
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
338
        use ParseOhttpKeysError::*;
339
        match self {
×
NEW
340
            IncorrectLength(l) => write!(f, "Invalid length, got {l} expected 34"),
×
341
            InvalidPublicKey => write!(f, "Invalid public key"),
×
342
            DecodeKeyConfig(e) => write!(f, "Failed to decode KeyConfig: {e}"),
×
343
            #[cfg(test)]
NEW
344
            InvalidFormat => write!(f, "Invalid format"),
×
345
        }
346
    }
×
347
}
348

349
impl std::error::Error for ParseOhttpKeysError {
350
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
351
        use ParseOhttpKeysError::*;
352
        match self {
×
353
            DecodeKeyConfig(e) => Some(e),
×
NEW
354
            IncorrectLength(_) | InvalidPublicKey => None,
×
355
            #[cfg(test)]
NEW
356
            InvalidFormat => None,
×
357
        }
358
    }
×
359
}
360

361
#[cfg(test)]
362
mod test {
363
    use payjoin_test_utils::{KEM, KEY_ID, SYMMETRIC};
364

365
    use super::*;
366

367
    #[test]
368
    fn test_ohttp_keys_roundtrip() {
1✔
369
        let keys = OhttpKeys(ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap());
1✔
370
        let serialized = keys.to_bytes().unwrap();
1✔
371
        let deserialized = OhttpKeys::try_from(&serialized[..]).unwrap();
1✔
372
        assert!(keys.eq(&deserialized));
1✔
373
    }
1✔
374

375
    #[test]
376
    fn test_ohttp_keys_equality() {
1✔
377
        use ohttp::KeyId;
378
        const KEY_ID_ONE: KeyId = 1;
379
        let keys_one =
1✔
380
            OhttpKeys(ohttp::KeyConfig::new(KEY_ID_ONE, KEM, Vec::from(SYMMETRIC)).unwrap());
1✔
381
        let serialized_one = &keys_one.to_bytes().unwrap();
1✔
382
        let deserialized_one = OhttpKeys::try_from(&serialized_one[..]).unwrap();
1✔
383

384
        const KEY_ID_TWO: KeyId = 2;
385
        let keys_two =
1✔
386
            OhttpKeys(ohttp::KeyConfig::new(KEY_ID_TWO, KEM, Vec::from(SYMMETRIC)).unwrap());
1✔
387
        let serialized_two = &keys_two.to_bytes().unwrap();
1✔
388
        let deserialized_two = OhttpKeys::try_from(&serialized_two[..]).unwrap();
1✔
389
        assert!(keys_one.eq(&deserialized_one));
1✔
390
        assert!(keys_two.eq(&deserialized_two));
1✔
391
        assert!(!keys_one.eq(&deserialized_two));
1✔
392
        assert!(!keys_two.eq(&deserialized_one));
1✔
393
    }
1✔
394
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc