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

payjoin / rust-payjoin / 22594006325

02 Mar 2026 08:18PM UTC coverage: 82.512% (+0.03%) from 82.484%
22594006325

Pull #1377

github

web-flow
Merge c02d1d13e into 55fbc08be
Pull Request #1377: Url rewrite

314 of 376 new or added lines in 15 files covered. (83.51%)

1 existing line in 1 file now uncovered.

10913 of 13226 relevant lines covered (82.51%)

404.25 hits per line

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

72.35
/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(crate) fn ohttp_encapsulate(
38✔
17
    ohttp_keys: &ohttp::KeyConfig,
38✔
18
    method: &str,
38✔
19
    target_resource: &str,
38✔
20
    body: Option<&[u8]>,
38✔
21
) -> Result<([u8; ENCAPSULATED_MESSAGE_BYTES], ohttp::ClientResponse), OhttpEncapsulationError> {
38✔
22
    use std::fmt::Write;
23
    let mut ohttp_keys = ohttp_keys.clone();
38✔
24

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

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

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

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

63
impl DirectoryResponseError {
64
    pub(crate) fn is_fatal(&self) -> bool {
×
65
        use DirectoryResponseError::*;
66

67
        match self {
×
68
            OhttpDecapsulation(_) => true,
×
69
            InvalidSize(_) => false,
×
70
            UnexpectedStatusCode(status_code) => status_code.is_client_error(),
×
71
        }
72
    }
×
73
}
74

75
impl fmt::Display for DirectoryResponseError {
76
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
77
        use DirectoryResponseError::*;
78

79
        match self {
×
80
            OhttpDecapsulation(e) => write!(f, "OHTTP Decapsulation Error: {e}"),
×
81
            InvalidSize(size) => write!(
×
82
                f,
×
83
                "Unexpected response size {}, expected {} bytes",
84
                size,
85
                crate::directory::ENCAPSULATED_MESSAGE_BYTES
86
            ),
87
            UnexpectedStatusCode(status) => write!(f, "Unexpected status code: {status}"),
×
88
        }
89
    }
×
90
}
91

92
impl error::Error for DirectoryResponseError {
93
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
94
        use DirectoryResponseError::*;
95

96
        match self {
×
97
            OhttpDecapsulation(e) => Some(e),
×
98
            InvalidSize(_) => None,
×
99
            UnexpectedStatusCode(_) => None,
×
100
        }
101
    }
×
102
}
103

104
pub(crate) fn process_get_res(
19✔
105
    res: &[u8],
19✔
106
    ohttp_context: ohttp::ClientResponse,
19✔
107
) -> Result<Option<Vec<u8>>, DirectoryResponseError> {
19✔
108
    let response = process_ohttp_res(res, ohttp_context)?;
19✔
109
    match response.status() {
19✔
110
        http::StatusCode::OK => Ok(Some(response.body().to_vec())),
13✔
111
        http::StatusCode::ACCEPTED => Ok(None),
6✔
112
        status_code => Err(DirectoryResponseError::UnexpectedStatusCode(status_code)),
×
113
    }
114
}
19✔
115

116
pub(crate) fn process_post_res(
13✔
117
    res: &[u8],
13✔
118
    ohttp_context: ohttp::ClientResponse,
13✔
119
) -> Result<(), DirectoryResponseError> {
13✔
120
    let response = process_ohttp_res(res, ohttp_context)?;
13✔
121
    match response.status() {
13✔
122
        http::StatusCode::OK => Ok(()),
13✔
123
        status_code => Err(DirectoryResponseError::UnexpectedStatusCode(status_code)),
×
124
    }
125
}
13✔
126

127
fn process_ohttp_res(
32✔
128
    res: &[u8],
32✔
129
    ohttp_context: ohttp::ClientResponse,
32✔
130
) -> Result<http::Response<Vec<u8>>, DirectoryResponseError> {
32✔
131
    let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] =
32✔
132
        res.try_into().map_err(|_| DirectoryResponseError::InvalidSize(res.len()))?;
32✔
133
    tracing::trace!("decapsulating directory response");
32✔
134
    let res = ohttp_decapsulate(ohttp_context, response_array)
32✔
135
        .map_err(DirectoryResponseError::OhttpDecapsulation)?;
32✔
136
    Ok(res)
32✔
137
}
32✔
138

139
/// decapsulate ohttp, bhttp response and return http response body and status code
140
pub(crate) fn ohttp_decapsulate(
32✔
141
    res_ctx: ohttp::ClientResponse,
32✔
142
    ohttp_body: &[u8; ENCAPSULATED_MESSAGE_BYTES],
32✔
143
) -> Result<http::Response<Vec<u8>>, OhttpEncapsulationError> {
32✔
144
    let bhttp_body = res_ctx.decapsulate(ohttp_body)?;
32✔
145
    let mut r = std::io::Cursor::new(bhttp_body);
32✔
146
    let m: bhttp::Message = bhttp::Message::read_bhttp(&mut r)?;
32✔
147
    let mut builder = http::Response::builder();
32✔
148
    for field in m.header().iter() {
32✔
149
        builder = builder.header(field.name(), field.value());
×
150
    }
×
151
    builder
32✔
152
        .status({
32✔
153
            let code = m.control().status().ok_or(bhttp::Error::InvalidStatus)?;
32✔
154

155
            http::StatusCode::from_u16(code.code()).map_err(|_| bhttp::Error::InvalidStatus)?
32✔
156
        })
157
        .body(m.content().to_vec())
32✔
158
        .map_err(OhttpEncapsulationError::Http)
32✔
159
}
32✔
160

161
/// Error from de/encapsulating an Oblivious HTTP request or response.
162
#[derive(Debug)]
163
pub enum OhttpEncapsulationError {
164
    Http(http::Error),
165
    Ohttp(ohttp::Error),
166
    Bhttp(bhttp::Error),
167
    ParseUrl(crate::core::UrlParseError),
168
}
169

170
impl From<http::Error> for OhttpEncapsulationError {
171
    fn from(value: http::Error) -> Self { Self::Http(value) }
×
172
}
173

174
impl From<ohttp::Error> for OhttpEncapsulationError {
175
    fn from(value: ohttp::Error) -> Self { Self::Ohttp(value) }
×
176
}
177

178
impl From<bhttp::Error> for OhttpEncapsulationError {
179
    fn from(value: bhttp::Error) -> Self { Self::Bhttp(value) }
×
180
}
181

182
impl From<crate::core::UrlParseError> for OhttpEncapsulationError {
NEW
183
    fn from(value: crate::core::UrlParseError) -> Self { Self::ParseUrl(value) }
×
184
}
185

186
impl fmt::Display for OhttpEncapsulationError {
187
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
188
        use OhttpEncapsulationError::*;
189

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

199
impl error::Error for OhttpEncapsulationError {
200
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
201
        use OhttpEncapsulationError::*;
202

203
        match &self {
×
204
            Http(e) => Some(e),
×
205
            Ohttp(e) => Some(e),
×
206
            Bhttp(e) => Some(e),
×
207
            ParseUrl(e) => Some(e),
×
208
        }
209
    }
×
210
}
211

212
#[derive(Debug, Clone)]
213
pub struct OhttpKeys(pub ohttp::KeyConfig);
214

215
impl OhttpKeys {
216
    /// Decode an OHTTP KeyConfig
217
    pub fn decode(bytes: &[u8]) -> Result<Self, ohttp::Error> {
19✔
218
        ohttp::KeyConfig::decode(bytes).map(Self)
19✔
219
    }
19✔
220

221
    pub fn to_bytes(&self) -> Result<Vec<u8>, ohttp::Error> {
59✔
222
        let bytes = self.encode()?;
59✔
223

224
        let key_id = bytes[0];
59✔
225
        let uncompressed_pubkey = &bytes[3..68];
59✔
226

227
        let compressed_pubkey = bitcoin::secp256k1::PublicKey::from_slice(uncompressed_pubkey)
59✔
228
            .expect("serialization of public key should be deserializable without error")
59✔
229
            .serialize();
59✔
230

231
        let mut buf = vec![key_id];
59✔
232
        buf.extend_from_slice(&compressed_pubkey);
59✔
233

234
        Ok(buf)
59✔
235
    }
59✔
236
}
237

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

242
impl fmt::Display for OhttpKeys {
243
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56✔
244
        let buf = self.to_bytes().map_err(|_| fmt::Error)?;
56✔
245

246
        let oh_hrp: bech32::Hrp =
56✔
247
            bech32::Hrp::parse("OH").expect("parsing a valid HRP constant should never fail");
56✔
248

249
        crate::bech32::nochecksum::encode_to_fmt(f, oh_hrp, &buf).map_err(|e| match e {
56✔
250
            EncodeError::Fmt(e) => e,
×
251
            _ => fmt::Error,
×
252
        })
×
253
    }
56✔
254
}
255

256
impl TryFrom<&[u8]> for OhttpKeys {
257
    type Error = ParseOhttpKeysError;
258

259
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
25✔
260
        let buf: [u8; 34] =
23✔
261
            bytes.try_into().map_err(|_| ParseOhttpKeysError::IncorrectLength(bytes.len()))?;
25✔
262

263
        let key_id = buf[0];
23✔
264
        let compressed_pk = &buf[1..];
23✔
265

266
        let pubkey = bitcoin::secp256k1::PublicKey::from_slice(compressed_pk)
23✔
267
            .map_err(|_| ParseOhttpKeysError::InvalidPublicKey)?;
23✔
268

269
        let mut buf = vec![key_id];
23✔
270
        buf.extend_from_slice(KEM_ID);
23✔
271
        buf.extend_from_slice(&pubkey.serialize_uncompressed());
23✔
272
        buf.extend_from_slice(SYMMETRIC_LEN);
23✔
273
        buf.extend_from_slice(SYMMETRIC_KDF_AEAD);
23✔
274

275
        ohttp::KeyConfig::decode(&buf).map(Self).map_err(ParseOhttpKeysError::DecodeKeyConfig)
23✔
276
    }
25✔
277
}
278

279
#[cfg(test)]
280
impl std::str::FromStr for OhttpKeys {
281
    type Err = ParseOhttpKeysError;
282

283
    /// Parses a base64URL-encoded string into OhttpKeys.
284
    /// The string format is: key_id || compressed_public_key
285
    fn from_str(s: &str) -> Result<Self, Self::Err> {
2✔
286
        let oh_hrp: bech32::Hrp =
2✔
287
            bech32::Hrp::parse("OH").expect("parsing a valid HRP constant should never fail");
2✔
288

289
        let (hrp, bytes) =
2✔
290
            crate::bech32::nochecksum::decode(s).map_err(|_| ParseOhttpKeysError::InvalidFormat)?;
2✔
291

292
        if hrp != oh_hrp {
2✔
293
            return Err(ParseOhttpKeysError::InvalidFormat);
×
294
        }
2✔
295

296
        Self::try_from(&bytes[..])
2✔
297
    }
2✔
298
}
299

300
impl PartialEq for OhttpKeys {
301
    fn eq(&self, other: &Self) -> bool {
24✔
302
        match (self.encode(), other.encode()) {
24✔
303
            (Ok(self_encoded), Ok(other_encoded)) => self_encoded == other_encoded,
24✔
304
            // If OhttpKeys::encode(&self) is Err, return false
305
            _ => false,
×
306
        }
307
    }
24✔
308
}
309

310
impl Eq for OhttpKeys {}
311

312
impl Deref for OhttpKeys {
313
    type Target = ohttp::KeyConfig;
314

315
    fn deref(&self) -> &Self::Target { &self.0 }
148✔
316
}
317

318
impl DerefMut for OhttpKeys {
319
    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
×
320
}
321

322
impl<'de> serde::Deserialize<'de> for OhttpKeys {
323
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
5✔
324
    where
5✔
325
        D: serde::Deserializer<'de>,
5✔
326
    {
327
        let bytes = Vec::<u8>::deserialize(deserializer)?;
5✔
328
        OhttpKeys::decode(&bytes).map_err(serde::de::Error::custom)
5✔
329
    }
5✔
330
}
331

332
impl serde::Serialize for OhttpKeys {
333
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
4✔
334
    where
4✔
335
        S: serde::Serializer,
4✔
336
    {
337
        let bytes = self.encode().map_err(serde::ser::Error::custom)?;
4✔
338
        bytes.serialize(serializer)
4✔
339
    }
4✔
340
}
341

342
#[derive(Debug)]
343
pub enum ParseOhttpKeysError {
344
    IncorrectLength(usize),
345
    InvalidPublicKey,
346
    DecodeKeyConfig(ohttp::Error),
347
    #[cfg(test)]
348
    InvalidFormat,
349
}
350

351
impl std::fmt::Display for ParseOhttpKeysError {
352
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
353
        use ParseOhttpKeysError::*;
354
        match self {
×
355
            IncorrectLength(l) => write!(f, "Invalid length, got {l} expected 34"),
×
356
            InvalidPublicKey => write!(f, "Invalid public key"),
×
357
            DecodeKeyConfig(e) => write!(f, "Failed to decode KeyConfig: {e}"),
×
358
            #[cfg(test)]
359
            InvalidFormat => write!(f, "Invalid format"),
×
360
        }
361
    }
×
362
}
363

364
impl std::error::Error for ParseOhttpKeysError {
365
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
366
        use ParseOhttpKeysError::*;
367
        match self {
×
368
            DecodeKeyConfig(e) => Some(e),
×
369
            IncorrectLength(_) | InvalidPublicKey => None,
×
370
            #[cfg(test)]
371
            InvalidFormat => None,
×
372
        }
373
    }
×
374
}
375

376
#[cfg(test)]
377
mod test {
378
    use payjoin_test_utils::{KEM, KEY_ID, SYMMETRIC};
379

380
    use super::*;
381

382
    #[test]
383
    fn test_ohttp_keys_roundtrip() {
1✔
384
        let keys = OhttpKeys(ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap());
1✔
385
        let serialized = keys.to_bytes().unwrap();
1✔
386
        let deserialized = OhttpKeys::try_from(&serialized[..]).unwrap();
1✔
387
        assert!(keys.eq(&deserialized));
1✔
388
    }
1✔
389

390
    #[test]
391
    fn test_ohttp_keys_equality() {
1✔
392
        use ohttp::KeyId;
393
        const KEY_ID_ONE: KeyId = 1;
394
        let keys_one =
1✔
395
            OhttpKeys(ohttp::KeyConfig::new(KEY_ID_ONE, KEM, Vec::from(SYMMETRIC)).unwrap());
1✔
396
        let serialized_one = &keys_one.to_bytes().unwrap();
1✔
397
        let deserialized_one = OhttpKeys::try_from(&serialized_one[..]).unwrap();
1✔
398

399
        const KEY_ID_TWO: KeyId = 2;
400
        let keys_two =
1✔
401
            OhttpKeys(ohttp::KeyConfig::new(KEY_ID_TWO, KEM, Vec::from(SYMMETRIC)).unwrap());
1✔
402
        let serialized_two = &keys_two.to_bytes().unwrap();
1✔
403
        let deserialized_two = OhttpKeys::try_from(&serialized_two[..]).unwrap();
1✔
404
        assert!(keys_one.eq(&deserialized_one));
1✔
405
        assert!(keys_two.eq(&deserialized_two));
1✔
406
        assert!(!keys_one.eq(&deserialized_two));
1✔
407
        assert!(!keys_two.eq(&deserialized_one));
1✔
408
    }
1✔
409
}
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