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

payjoin / rust-payjoin / 15642374656

13 Jun 2025 07:22PM UTC coverage: 84.642% (-1.4%) from 86.035%
15642374656

Pull #768

github

web-flow
Merge 605fdfd6b into be0597869
Pull Request #768: Add PartialEq/Eq to errors for easier comparison

22 of 162 new or added lines in 13 files covered. (13.58%)

304 existing lines in 9 files now uncovered.

7137 of 8432 relevant lines covered (84.64%)

550.77 hits per line

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

63.11
/payjoin/src/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(
47✔
17
    ohttp_keys: &mut ohttp::KeyConfig,
47✔
18
    method: &str,
47✔
19
    target_resource: &str,
47✔
20
    body: Option<&[u8]>,
47✔
21
) -> Result<([u8; ENCAPSULATED_MESSAGE_BYTES], ohttp::ClientResponse), OhttpEncapsulationError> {
47✔
22
    use std::fmt::Write;
23

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

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

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

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

62
impl PartialEq for DirectoryResponseError {
NEW
63
    fn eq(&self, other: &Self) -> bool {
×
NEW
UNCOV
64
        match (self, other) {
×
NEW
UNCOV
65
            (DirectoryResponseError::InvalidSize(s1), DirectoryResponseError::InvalidSize(s2)) =>
×
NEW
66
                s1 == s2,
×
67
            (
68
                DirectoryResponseError::OhttpDecapsulation(_),
69
                DirectoryResponseError::OhttpDecapsulation(_),
NEW
70
            ) => true,
×
71
            (
NEW
72
                DirectoryResponseError::UnexpectedStatusCode(s1),
×
NEW
73
                DirectoryResponseError::UnexpectedStatusCode(s2),
×
NEW
74
            ) => s1 == s2,
×
NEW
UNCOV
75
            _ => false,
×
76
        }
NEW
UNCOV
77
    }
×
78
}
79

80
impl Eq for DirectoryResponseError {}
81

82
impl fmt::Display for DirectoryResponseError {
83
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
84
        use DirectoryResponseError::*;
85

86
        match self {
×
UNCOV
87
            OhttpDecapsulation(e) => write!(f, "OHTTP Decapsulation Error: {e}"),
×
88
            InvalidSize(size) => write!(
×
UNCOV
89
                f,
×
UNCOV
90
                "Unexpected response size {}, expected {} bytes",
×
UNCOV
91
                size,
×
UNCOV
92
                crate::directory::ENCAPSULATED_MESSAGE_BYTES
×
UNCOV
93
            ),
×
UNCOV
94
            UnexpectedStatusCode(status) => write!(f, "Unexpected status code: {status}"),
×
95
        }
UNCOV
96
    }
×
97
}
98

99
impl error::Error for DirectoryResponseError {
UNCOV
100
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
101
        use DirectoryResponseError::*;
102

UNCOV
103
        match self {
×
UNCOV
104
            OhttpDecapsulation(e) => Some(e),
×
UNCOV
105
            InvalidSize(_) => None,
×
UNCOV
106
            UnexpectedStatusCode(_) => None,
×
107
        }
UNCOV
108
    }
×
109
}
110

111
pub fn process_get_res(
21✔
112
    res: &[u8],
21✔
113
    ohttp_context: ohttp::ClientResponse,
21✔
114
) -> Result<Option<Vec<u8>>, DirectoryResponseError> {
21✔
115
    let response = process_ohttp_res(res, ohttp_context)?;
21✔
116
    match response.status() {
21✔
117
        http::StatusCode::OK => Ok(Some(response.body().to_vec())),
18✔
118
        http::StatusCode::ACCEPTED => Ok(None),
3✔
UNCOV
119
        status_code => Err(DirectoryResponseError::UnexpectedStatusCode(status_code)),
×
120
    }
121
}
21✔
122

123
pub fn process_post_res(
19✔
124
    res: &[u8],
19✔
125
    ohttp_context: ohttp::ClientResponse,
19✔
126
) -> Result<(), DirectoryResponseError> {
19✔
127
    let response = process_ohttp_res(res, ohttp_context)?;
19✔
128
    match response.status() {
19✔
129
        http::StatusCode::OK => Ok(()),
19✔
UNCOV
130
        status_code => Err(DirectoryResponseError::UnexpectedStatusCode(status_code)),
×
131
    }
132
}
19✔
133

134
fn process_ohttp_res(
40✔
135
    res: &[u8],
40✔
136
    ohttp_context: ohttp::ClientResponse,
40✔
137
) -> Result<http::Response<Vec<u8>>, DirectoryResponseError> {
40✔
138
    let response_array: &[u8; crate::directory::ENCAPSULATED_MESSAGE_BYTES] =
40✔
139
        res.try_into().map_err(|_| DirectoryResponseError::InvalidSize(res.len()))?;
40✔
140
    log::trace!("decapsulating directory response");
40✔
141
    let res = ohttp_decapsulate(ohttp_context, response_array)
40✔
142
        .map_err(DirectoryResponseError::OhttpDecapsulation)?;
40✔
143
    Ok(res)
40✔
144
}
40✔
145

146
/// decapsulate ohttp, bhttp response and return http response body and status code
147
pub fn ohttp_decapsulate(
40✔
148
    res_ctx: ohttp::ClientResponse,
40✔
149
    ohttp_body: &[u8; ENCAPSULATED_MESSAGE_BYTES],
40✔
150
) -> Result<http::Response<Vec<u8>>, OhttpEncapsulationError> {
40✔
151
    let bhttp_body = res_ctx.decapsulate(ohttp_body)?;
40✔
152
    let mut r = std::io::Cursor::new(bhttp_body);
40✔
153
    let m: bhttp::Message = bhttp::Message::read_bhttp(&mut r)?;
40✔
154
    let mut builder = http::Response::builder();
40✔
155
    for field in m.header().iter() {
40✔
UNCOV
156
        builder = builder.header(field.name(), field.value());
×
UNCOV
157
    }
×
158
    builder
40✔
159
        .status(m.control().status().unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR.into()))
40✔
160
        .body(m.content().to_vec())
40✔
161
        .map_err(OhttpEncapsulationError::Http)
40✔
162
}
40✔
163

164
/// Error from de/encapsulating an Oblivious HTTP request or response.
165
#[derive(Debug)]
166
pub enum OhttpEncapsulationError {
167
    Http(http::Error),
168
    Ohttp(ohttp::Error),
169
    Bhttp(bhttp::Error),
170
    ParseUrl(url::ParseError),
171
}
172

173
impl PartialEq for OhttpEncapsulationError {
NEW
174
    fn eq(&self, other: &Self) -> bool {
×
NEW
175
        match (self, other) {
×
NEW
176
            (OhttpEncapsulationError::Http(_), OhttpEncapsulationError::Http(_)) => true,
×
NEW
177
            (OhttpEncapsulationError::Ohttp(_), OhttpEncapsulationError::Ohttp(_)) => true,
×
NEW
UNCOV
178
            (OhttpEncapsulationError::Bhttp(_), OhttpEncapsulationError::Bhttp(_)) => true,
×
NEW
179
            (OhttpEncapsulationError::ParseUrl(u1), OhttpEncapsulationError::ParseUrl(u2)) =>
×
NEW
UNCOV
180
                u1 == u2,
×
NEW
UNCOV
181
            _ => false,
×
182
        }
NEW
183
    }
×
184
}
185

186
impl Eq for OhttpEncapsulationError {}
187

188
impl From<http::Error> for OhttpEncapsulationError {
189
    fn from(value: http::Error) -> Self { Self::Http(value) }
×
190
}
191

192
impl From<ohttp::Error> for OhttpEncapsulationError {
UNCOV
193
    fn from(value: ohttp::Error) -> Self { Self::Ohttp(value) }
×
194
}
195

196
impl From<bhttp::Error> for OhttpEncapsulationError {
UNCOV
197
    fn from(value: bhttp::Error) -> Self { Self::Bhttp(value) }
×
198
}
199

200
impl From<url::ParseError> for OhttpEncapsulationError {
UNCOV
201
    fn from(value: url::ParseError) -> Self { Self::ParseUrl(value) }
×
202
}
203

204
impl fmt::Display for OhttpEncapsulationError {
UNCOV
205
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
206
        use OhttpEncapsulationError::*;
207

UNCOV
208
        match &self {
×
UNCOV
209
            Http(e) => e.fmt(f),
×
UNCOV
210
            Ohttp(e) => e.fmt(f),
×
UNCOV
211
            Bhttp(e) => e.fmt(f),
×
UNCOV
212
            ParseUrl(e) => e.fmt(f),
×
213
        }
UNCOV
214
    }
×
215
}
216

217
impl error::Error for OhttpEncapsulationError {
UNCOV
218
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
219
        use OhttpEncapsulationError::*;
220

UNCOV
221
        match &self {
×
UNCOV
222
            Http(e) => Some(e),
×
UNCOV
223
            Ohttp(e) => Some(e),
×
224
            Bhttp(e) => Some(e),
×
225
            ParseUrl(e) => Some(e),
×
226
        }
UNCOV
227
    }
×
228
}
229

230
#[derive(Debug, Clone)]
231
pub struct OhttpKeys(pub ohttp::KeyConfig);
232

233
impl OhttpKeys {
234
    /// Decode an OHTTP KeyConfig
235
    pub fn decode(bytes: &[u8]) -> Result<Self, ohttp::Error> {
16✔
236
        ohttp::KeyConfig::decode(bytes).map(Self)
16✔
237
    }
16✔
238
}
239

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

244
impl fmt::Display for OhttpKeys {
245
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
25✔
246
        let bytes = self.encode().map_err(|_| fmt::Error)?;
25✔
247
        let key_id = bytes[0];
25✔
248
        let pubkey = &bytes[3..68];
25✔
249

250
        let compressed_pubkey =
25✔
251
            bitcoin::secp256k1::PublicKey::from_slice(pubkey).map_err(|_| fmt::Error)?.serialize();
25✔
252

25✔
253
        let mut buf = vec![key_id];
25✔
254
        buf.extend_from_slice(&compressed_pubkey);
25✔
255

25✔
256
        let oh_hrp: bech32::Hrp = bech32::Hrp::parse("OH").unwrap();
25✔
257

25✔
258
        crate::bech32::nochecksum::encode_to_fmt(f, oh_hrp, &buf).map_err(|e| match e {
25✔
UNCOV
259
            EncodeError::Fmt(e) => e,
×
UNCOV
260
            _ => fmt::Error,
×
261
        })
25✔
262
    }
25✔
263
}
264

265
impl TryFrom<&[u8]> for OhttpKeys {
266
    type Error = ParseOhttpKeysError;
267

268
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
29✔
269
        let key_id = *bytes.first().ok_or(ParseOhttpKeysError::InvalidFormat)?;
29✔
270
        let compressed_pk = bytes.get(1..34).ok_or(ParseOhttpKeysError::InvalidFormat)?;
29✔
271

272
        let pubkey = bitcoin::secp256k1::PublicKey::from_slice(compressed_pk)
29✔
273
            .map_err(|_| ParseOhttpKeysError::InvalidPublicKey)?;
29✔
274

275
        let mut buf = vec![key_id];
29✔
276
        buf.extend_from_slice(KEM_ID);
29✔
277
        buf.extend_from_slice(&pubkey.serialize_uncompressed());
29✔
278
        buf.extend_from_slice(SYMMETRIC_LEN);
29✔
279
        buf.extend_from_slice(SYMMETRIC_KDF_AEAD);
29✔
280

29✔
281
        ohttp::KeyConfig::decode(&buf).map(Self).map_err(ParseOhttpKeysError::DecodeKeyConfig)
29✔
282
    }
29✔
283
}
284

285
impl std::str::FromStr for OhttpKeys {
286
    type Err = ParseOhttpKeysError;
287

288
    /// Parses a base64URL-encoded string into OhttpKeys.
289
    /// The string format is: key_id || compressed_public_key
290
    fn from_str(s: &str) -> Result<Self, Self::Err> {
30✔
291
        // TODO extract to utility function
30✔
292
        let oh_hrp: bech32::Hrp = bech32::Hrp::parse("OH").unwrap();
30✔
293

294
        let (hrp, bytes) =
29✔
295
            crate::bech32::nochecksum::decode(s).map_err(ParseOhttpKeysError::DecodeBech32)?;
30✔
296

297
        if hrp != oh_hrp {
29✔
UNCOV
298
            return Err(ParseOhttpKeysError::InvalidFormat);
×
299
        }
29✔
300

29✔
301
        Self::try_from(&bytes[..])
29✔
302
    }
30✔
303
}
304

305
impl PartialEq for OhttpKeys {
306
    fn eq(&self, other: &Self) -> bool {
4✔
307
        match (self.encode(), other.encode()) {
4✔
308
            (Ok(self_encoded), Ok(other_encoded)) => self_encoded == other_encoded,
4✔
309
            // If OhttpKeys::encode(&self) is Err, return false
UNCOV
310
            _ => false,
×
311
        }
312
    }
4✔
313
}
314

315
impl Eq for OhttpKeys {}
316

317
impl Deref for OhttpKeys {
318
    type Target = ohttp::KeyConfig;
319

320
    fn deref(&self) -> &Self::Target { &self.0 }
40✔
321
}
322

323
impl DerefMut for OhttpKeys {
324
    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
47✔
325
}
326

327
impl<'de> serde::Deserialize<'de> for OhttpKeys {
328
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
4✔
329
    where
4✔
330
        D: serde::Deserializer<'de>,
4✔
331
    {
4✔
332
        let bytes = Vec::<u8>::deserialize(deserializer)?;
4✔
333
        OhttpKeys::decode(&bytes).map_err(serde::de::Error::custom)
4✔
334
    }
4✔
335
}
336

337
impl serde::Serialize for OhttpKeys {
338
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3✔
339
    where
3✔
340
        S: serde::Serializer,
3✔
341
    {
3✔
342
        let bytes = self.encode().map_err(serde::ser::Error::custom)?;
3✔
343
        bytes.serialize(serializer)
3✔
344
    }
3✔
345
}
346

347
#[derive(Debug)]
348
pub enum ParseOhttpKeysError {
349
    InvalidFormat,
350
    InvalidPublicKey,
351
    DecodeBech32(bech32::primitives::decode::CheckedHrpstringError),
352
    DecodeKeyConfig(ohttp::Error),
353
}
354

355
impl PartialEq for ParseOhttpKeysError {
NEW
UNCOV
356
    fn eq(&self, other: &Self) -> bool {
×
NEW
UNCOV
357
        match (self, other) {
×
NEW
UNCOV
358
            (ParseOhttpKeysError::InvalidFormat, ParseOhttpKeysError::InvalidFormat) => true,
×
NEW
UNCOV
359
            (ParseOhttpKeysError::InvalidPublicKey, ParseOhttpKeysError::InvalidPublicKey) => true,
×
NEW
UNCOV
360
            (ParseOhttpKeysError::DecodeBech32(e1), ParseOhttpKeysError::DecodeBech32(e2)) =>
×
NEW
UNCOV
361
                e1 == e2,
×
NEW
UNCOV
362
            _ => false,
×
363
        }
NEW
UNCOV
364
    }
×
365
}
366

367
impl Eq for ParseOhttpKeysError {}
368

369
impl std::fmt::Display for ParseOhttpKeysError {
UNCOV
370
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
UNCOV
371
        match self {
×
UNCOV
372
            ParseOhttpKeysError::InvalidFormat => write!(f, "Invalid format"),
×
UNCOV
373
            ParseOhttpKeysError::InvalidPublicKey => write!(f, "Invalid public key"),
×
UNCOV
374
            ParseOhttpKeysError::DecodeBech32(e) => write!(f, "Failed to decode base64: {e}"),
×
UNCOV
375
            ParseOhttpKeysError::DecodeKeyConfig(e) => write!(f, "Failed to decode KeyConfig: {e}"),
×
376
        }
UNCOV
377
    }
×
378
}
379

380
impl std::error::Error for ParseOhttpKeysError {
UNCOV
381
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
UNCOV
382
        match self {
×
UNCOV
383
            ParseOhttpKeysError::DecodeBech32(e) => Some(e),
×
UNCOV
384
            ParseOhttpKeysError::DecodeKeyConfig(e) => Some(e),
×
UNCOV
385
            ParseOhttpKeysError::InvalidFormat | ParseOhttpKeysError::InvalidPublicKey => None,
×
386
        }
UNCOV
387
    }
×
388
}
389

390
#[cfg(test)]
391
mod test {
392
    use super::*;
393

394
    #[test]
395
    fn test_ohttp_keys_roundtrip() {
1✔
396
        use std::str::FromStr;
397

398
        use ohttp::hpke::{Aead, Kdf, Kem};
399
        use ohttp::{KeyId, SymmetricSuite};
400
        const KEY_ID: KeyId = 1;
401
        const KEM: Kem = Kem::K256Sha256;
402
        const SYMMETRIC: &[SymmetricSuite] =
403
            &[ohttp::SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)];
404
        let keys = OhttpKeys(ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap());
1✔
405
        let serialized = &keys.to_string();
1✔
406
        let deserialized = OhttpKeys::from_str(serialized).unwrap();
1✔
407
        assert_eq!(keys.encode().unwrap(), deserialized.encode().unwrap());
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