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

payjoin / rust-payjoin / 17268773534

27 Aug 2025 01:52PM UTC coverage: 85.72% (+0.01%) from 85.708%
17268773534

Pull #959

github

web-flow
Merge 083620ddd into 554c4e683
Pull Request #959: Update msrv deps

19 of 19 new or added lines in 4 files covered. (100.0%)

21 existing lines in 1 file now uncovered.

7984 of 9314 relevant lines covered (85.72%)

501.94 hits per line

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

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

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

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

49
    let mut buffer = [0u8; ENCAPSULATED_MESSAGE_BYTES];
22✔
50
    let len = encapsulated.len().min(ENCAPSULATED_MESSAGE_BYTES);
22✔
51
    buffer[..len].copy_from_slice(&encapsulated[..len]);
22✔
52
    Ok((buffer, ohttp_ctx))
22✔
53
}
22✔
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
    log::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
            m.control()
15✔
141
                .status()
15✔
142
                .map(|code| {
15✔
143
                    http::StatusCode::from_u16(code.code()).map_err(|_| bhttp::Error::InvalidStatus)
15✔
144
                })
15✔
145
                .unwrap_or(Ok(http::StatusCode::INTERNAL_SERVER_ERROR))?,
15✔
146
        )
147
        .body(m.content().to_vec())
15✔
148
        .map_err(OhttpEncapsulationError::Http)
15✔
149
}
15✔
150

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

160
impl From<http::Error> for OhttpEncapsulationError {
UNCOV
161
    fn from(value: http::Error) -> Self { Self::Http(value) }
×
162
}
163

164
impl From<ohttp::Error> for OhttpEncapsulationError {
UNCOV
165
    fn from(value: ohttp::Error) -> Self { Self::Ohttp(value) }
×
166
}
167

168
impl From<bhttp::Error> for OhttpEncapsulationError {
UNCOV
169
    fn from(value: bhttp::Error) -> Self { Self::Bhttp(value) }
×
170
}
171

172
impl From<url::ParseError> for OhttpEncapsulationError {
UNCOV
173
    fn from(value: url::ParseError) -> Self { Self::ParseUrl(value) }
×
174
}
175

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

180
        match &self {
×
181
            Http(e) => e.fmt(f),
×
UNCOV
182
            Ohttp(e) => e.fmt(f),
×
183
            Bhttp(e) => e.fmt(f),
×
UNCOV
184
            ParseUrl(e) => e.fmt(f),
×
185
        }
UNCOV
186
    }
×
187
}
188

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

193
        match &self {
×
194
            Http(e) => Some(e),
×
UNCOV
195
            Ohttp(e) => Some(e),
×
196
            Bhttp(e) => Some(e),
×
UNCOV
197
            ParseUrl(e) => Some(e),
×
198
        }
UNCOV
199
    }
×
200
}
201

202
#[derive(Debug, Clone)]
203
pub struct OhttpKeys(pub ohttp::KeyConfig);
204

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

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

216
impl fmt::Display for OhttpKeys {
217
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33✔
218
        let bytes = self.encode().map_err(|_| fmt::Error)?;
33✔
219
        let key_id = bytes[0];
33✔
220
        let pubkey = &bytes[3..68];
33✔
221

222
        let compressed_pubkey =
33✔
223
            bitcoin::secp256k1::PublicKey::from_slice(pubkey).map_err(|_| fmt::Error)?.serialize();
33✔
224

225
        let mut buf = vec![key_id];
33✔
226
        buf.extend_from_slice(&compressed_pubkey);
33✔
227

228
        let oh_hrp: bech32::Hrp = bech32::Hrp::parse("OH").unwrap();
33✔
229

230
        crate::bech32::nochecksum::encode_to_fmt(f, oh_hrp, &buf).map_err(|e| match e {
33✔
UNCOV
231
            EncodeError::Fmt(e) => e,
×
UNCOV
232
            _ => fmt::Error,
×
UNCOV
233
        })
×
234
    }
33✔
235
}
236

237
impl TryFrom<&[u8]> for OhttpKeys {
238
    type Error = ParseOhttpKeysError;
239

240
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
17✔
241
        let key_id = *bytes.first().ok_or(ParseOhttpKeysError::InvalidFormat)?;
17✔
242
        let compressed_pk = bytes.get(1..34).ok_or(ParseOhttpKeysError::InvalidFormat)?;
17✔
243

244
        let pubkey = bitcoin::secp256k1::PublicKey::from_slice(compressed_pk)
17✔
245
            .map_err(|_| ParseOhttpKeysError::InvalidPublicKey)?;
17✔
246

247
        let mut buf = vec![key_id];
17✔
248
        buf.extend_from_slice(KEM_ID);
17✔
249
        buf.extend_from_slice(&pubkey.serialize_uncompressed());
17✔
250
        buf.extend_from_slice(SYMMETRIC_LEN);
17✔
251
        buf.extend_from_slice(SYMMETRIC_KDF_AEAD);
17✔
252

253
        ohttp::KeyConfig::decode(&buf).map(Self).map_err(ParseOhttpKeysError::DecodeKeyConfig)
17✔
254
    }
17✔
255
}
256

257
impl std::str::FromStr for OhttpKeys {
258
    type Err = ParseOhttpKeysError;
259

260
    /// Parses a base64URL-encoded string into OhttpKeys.
261
    /// The string format is: key_id || compressed_public_key
262
    fn from_str(s: &str) -> Result<Self, Self::Err> {
17✔
263
        // TODO extract to utility function
264
        let oh_hrp: bech32::Hrp = bech32::Hrp::parse("OH").unwrap();
17✔
265

266
        let (hrp, bytes) =
17✔
267
            crate::bech32::nochecksum::decode(s).map_err(ParseOhttpKeysError::DecodeBech32)?;
17✔
268

269
        if hrp != oh_hrp {
17✔
UNCOV
270
            return Err(ParseOhttpKeysError::InvalidFormat);
×
271
        }
17✔
272

273
        Self::try_from(&bytes[..])
17✔
274
    }
17✔
275
}
276

277
impl PartialEq for OhttpKeys {
278
    fn eq(&self, other: &Self) -> bool {
17✔
279
        match (self.encode(), other.encode()) {
17✔
280
            (Ok(self_encoded), Ok(other_encoded)) => self_encoded == other_encoded,
17✔
281
            // If OhttpKeys::encode(&self) is Err, return false
UNCOV
282
            _ => false,
×
283
        }
284
    }
17✔
285
}
286

287
impl Eq for OhttpKeys {}
288

289
impl Deref for OhttpKeys {
290
    type Target = ohttp::KeyConfig;
291

292
    fn deref(&self) -> &Self::Target { &self.0 }
75✔
293
}
294

295
impl DerefMut for OhttpKeys {
296
    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
19✔
297
}
298

299
impl<'de> serde::Deserialize<'de> for OhttpKeys {
300
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
7✔
301
    where
7✔
302
        D: serde::Deserializer<'de>,
7✔
303
    {
304
        let bytes = Vec::<u8>::deserialize(deserializer)?;
7✔
305
        OhttpKeys::decode(&bytes).map_err(serde::de::Error::custom)
7✔
306
    }
7✔
307
}
308

309
impl serde::Serialize for OhttpKeys {
310
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
6✔
311
    where
6✔
312
        S: serde::Serializer,
6✔
313
    {
314
        let bytes = self.encode().map_err(serde::ser::Error::custom)?;
6✔
315
        bytes.serialize(serializer)
6✔
316
    }
6✔
317
}
318

319
#[derive(Debug)]
320
pub enum ParseOhttpKeysError {
321
    InvalidFormat,
322
    InvalidPublicKey,
323
    DecodeBech32(bech32::primitives::decode::CheckedHrpstringError),
324
    DecodeKeyConfig(ohttp::Error),
325
}
326

327
impl std::fmt::Display for ParseOhttpKeysError {
328
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
329
        match self {
×
330
            ParseOhttpKeysError::InvalidFormat => write!(f, "Invalid format"),
×
UNCOV
331
            ParseOhttpKeysError::InvalidPublicKey => write!(f, "Invalid public key"),
×
332
            ParseOhttpKeysError::DecodeBech32(e) => write!(f, "Failed to decode bech32: {e}"),
×
UNCOV
333
            ParseOhttpKeysError::DecodeKeyConfig(e) => write!(f, "Failed to decode KeyConfig: {e}"),
×
334
        }
UNCOV
335
    }
×
336
}
337

338
impl std::error::Error for ParseOhttpKeysError {
339
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
340
        match self {
×
UNCOV
341
            ParseOhttpKeysError::DecodeBech32(e) => Some(e),
×
342
            ParseOhttpKeysError::DecodeKeyConfig(e) => Some(e),
×
UNCOV
343
            ParseOhttpKeysError::InvalidFormat | ParseOhttpKeysError::InvalidPublicKey => None,
×
344
        }
UNCOV
345
    }
×
346
}
347

348
#[cfg(test)]
349
mod test {
350
    use payjoin_test_utils::{KEM, KEY_ID, SYMMETRIC};
351

352
    use super::*;
353

354
    #[test]
355
    fn test_ohttp_keys_roundtrip() {
1✔
356
        use std::str::FromStr;
357

358
        let keys = OhttpKeys(ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap());
1✔
359
        let serialized = &keys.to_string();
1✔
360
        let deserialized = OhttpKeys::from_str(serialized).unwrap();
1✔
361
        assert!(keys.eq(&deserialized));
1✔
362
    }
1✔
363

364
    #[test]
365
    fn test_ohttp_keys_equality() {
1✔
366
        use std::str::FromStr;
367

368
        use ohttp::KeyId;
369
        const KEY_ID_ONE: KeyId = 1;
370
        let keys_one =
1✔
371
            OhttpKeys(ohttp::KeyConfig::new(KEY_ID_ONE, KEM, Vec::from(SYMMETRIC)).unwrap());
1✔
372
        let serialized_one = &keys_one.to_string();
1✔
373
        let deserialized_one = OhttpKeys::from_str(serialized_one).unwrap();
1✔
374

375
        const KEY_ID_TWO: KeyId = 2;
376
        let keys_two =
1✔
377
            OhttpKeys(ohttp::KeyConfig::new(KEY_ID_TWO, KEM, Vec::from(SYMMETRIC)).unwrap());
1✔
378
        let serialized_two = &keys_two.to_string();
1✔
379
        let deserialized_two = OhttpKeys::from_str(serialized_two).unwrap();
1✔
380
        assert!(keys_one.eq(&deserialized_one));
1✔
381
        assert!(keys_two.eq(&deserialized_two));
1✔
382
        assert!(!keys_one.eq(&deserialized_two));
1✔
383
        assert!(!keys_two.eq(&deserialized_one));
1✔
384
    }
1✔
385
}
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

© 2025 Coveralls, Inc