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

payjoin / rust-payjoin / 17809629929

17 Sep 2025 08:18PM UTC coverage: 84.665% (-0.02%) from 84.681%
17809629929

push

github

web-flow
Remove mut ohttp keyconfig (#1085)

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

1 existing line in 1 file now uncovered.

8077 of 9540 relevant lines covered (84.66%)

489.95 hits per line

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

74.06
/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: &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
    let mut ohttp_keys = ohttp_keys.clone();
24✔
24

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

234
        let oh_hrp: bech32::Hrp =
28✔
235
            bech32::Hrp::parse("OH").expect("parsing a valid HRP constant should never fail");
28✔
236

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

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

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

251
        let key_id = buf[0];
17✔
252
        let compressed_pk = &buf[1..];
17✔
253

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

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

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

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

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

277
        let (hrp, bytes) =
2✔
278
            crate::bech32::nochecksum::decode(s).map_err(|_| ParseOhttpKeysError::InvalidFormat)?;
2✔
279

280
        if hrp != oh_hrp {
2✔
281
            return Err(ParseOhttpKeysError::InvalidFormat);
×
282
        }
2✔
283

284
        Self::try_from(&bytes[..])
2✔
285
    }
2✔
286
}
287

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

298
impl Eq for OhttpKeys {}
299

300
impl Deref for OhttpKeys {
301
    type Target = ohttp::KeyConfig;
302

303
    fn deref(&self) -> &Self::Target { &self.0 }
94✔
304
}
305

306
impl DerefMut for OhttpKeys {
UNCOV
307
    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
×
308
}
309

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

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

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

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

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

364
#[cfg(test)]
365
mod test {
366
    use payjoin_test_utils::{KEM, KEY_ID, SYMMETRIC};
367

368
    use super::*;
369

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

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

387
        const KEY_ID_TWO: KeyId = 2;
388
        let keys_two =
1✔
389
            OhttpKeys(ohttp::KeyConfig::new(KEY_ID_TWO, KEM, Vec::from(SYMMETRIC)).unwrap());
1✔
390
        let serialized_two = &keys_two.to_bytes().unwrap();
1✔
391
        let deserialized_two = OhttpKeys::try_from(&serialized_two[..]).unwrap();
1✔
392
        assert!(keys_one.eq(&deserialized_one));
1✔
393
        assert!(keys_two.eq(&deserialized_two));
1✔
394
        assert!(!keys_one.eq(&deserialized_two));
1✔
395
        assert!(!keys_two.eq(&deserialized_one));
1✔
396
    }
1✔
397
}
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