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

payjoin / rust-payjoin / 15280364074

27 May 2025 04:13PM UTC coverage: 41.223% (-43.3%) from 84.553%
15280364074

Pull #705

github

web-flow
Merge 34cbcca7d into 87042266d
Pull Request #705: Adjust _danger-local-https to be the default to prevent dangerous cert vulnerability when building with --all-features

1 of 1 new or added line in 1 file covered. (100.0%)

1600 existing lines in 30 files now uncovered.

1705 of 4136 relevant lines covered (41.22%)

0.59 hits per line

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

0.0
/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

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

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

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

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

55
/// decapsulate ohttp, bhttp response and return http response body and status code
UNCOV
56
pub fn ohttp_decapsulate(
×
UNCOV
57
    res_ctx: ohttp::ClientResponse,
×
UNCOV
58
    ohttp_body: &[u8; ENCAPSULATED_MESSAGE_BYTES],
×
UNCOV
59
) -> Result<http::Response<Vec<u8>>, OhttpEncapsulationError> {
×
UNCOV
60
    let bhttp_body = res_ctx.decapsulate(ohttp_body)?;
×
UNCOV
61
    let mut r = std::io::Cursor::new(bhttp_body);
×
UNCOV
62
    let m: bhttp::Message = bhttp::Message::read_bhttp(&mut r)?;
×
UNCOV
63
    let mut builder = http::Response::builder();
×
UNCOV
64
    for field in m.header().iter() {
×
65
        builder = builder.header(field.name(), field.value());
×
66
    }
×
UNCOV
67
    builder
×
UNCOV
68
        .status(m.control().status().unwrap_or(http::StatusCode::INTERNAL_SERVER_ERROR.into()))
×
UNCOV
69
        .body(m.content().to_vec())
×
UNCOV
70
        .map_err(OhttpEncapsulationError::Http)
×
UNCOV
71
}
×
72

73
/// Error from de/encapsulating an Oblivious HTTP request or response.
74
#[derive(Debug)]
75
pub enum OhttpEncapsulationError {
76
    Http(http::Error),
77
    Ohttp(ohttp::Error),
78
    Bhttp(bhttp::Error),
79
    ParseUrl(url::ParseError),
80
}
81

82
impl From<http::Error> for OhttpEncapsulationError {
83
    fn from(value: http::Error) -> Self { Self::Http(value) }
×
84
}
85

86
impl From<ohttp::Error> for OhttpEncapsulationError {
87
    fn from(value: ohttp::Error) -> Self { Self::Ohttp(value) }
×
88
}
89

90
impl From<bhttp::Error> for OhttpEncapsulationError {
91
    fn from(value: bhttp::Error) -> Self { Self::Bhttp(value) }
×
92
}
93

94
impl From<url::ParseError> for OhttpEncapsulationError {
95
    fn from(value: url::ParseError) -> Self { Self::ParseUrl(value) }
×
96
}
97

98
impl fmt::Display for OhttpEncapsulationError {
99
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
100
        use OhttpEncapsulationError::*;
101

102
        match &self {
×
103
            Http(e) => e.fmt(f),
×
104
            Ohttp(e) => e.fmt(f),
×
105
            Bhttp(e) => e.fmt(f),
×
106
            ParseUrl(e) => e.fmt(f),
×
107
        }
108
    }
×
109
}
110

111
impl error::Error for OhttpEncapsulationError {
112
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
×
113
        use OhttpEncapsulationError::*;
114

115
        match &self {
×
116
            Http(e) => Some(e),
×
117
            Ohttp(e) => Some(e),
×
118
            Bhttp(e) => Some(e),
×
119
            ParseUrl(e) => Some(e),
×
120
        }
121
    }
×
122
}
123

124
#[derive(Debug, Clone)]
125
pub struct OhttpKeys(pub ohttp::KeyConfig);
126

127
impl OhttpKeys {
128
    /// Decode an OHTTP KeyConfig
UNCOV
129
    pub fn decode(bytes: &[u8]) -> Result<Self, ohttp::Error> {
×
UNCOV
130
        ohttp::KeyConfig::decode(bytes).map(Self)
×
UNCOV
131
    }
×
132
}
133

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

138
impl fmt::Display for OhttpKeys {
UNCOV
139
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
UNCOV
140
        let bytes = self.encode().map_err(|_| fmt::Error)?;
×
UNCOV
141
        let key_id = bytes[0];
×
UNCOV
142
        let pubkey = &bytes[3..68];
×
143

UNCOV
144
        let compressed_pubkey =
×
UNCOV
145
            bitcoin::secp256k1::PublicKey::from_slice(pubkey).map_err(|_| fmt::Error)?.serialize();
×
UNCOV
146

×
UNCOV
147
        let mut buf = vec![key_id];
×
UNCOV
148
        buf.extend_from_slice(&compressed_pubkey);
×
UNCOV
149

×
UNCOV
150
        let oh_hrp: bech32::Hrp = bech32::Hrp::parse("OH").unwrap();
×
UNCOV
151

×
UNCOV
152
        crate::bech32::nochecksum::encode_to_fmt(f, oh_hrp, &buf).map_err(|e| match e {
×
153
            EncodeError::Fmt(e) => e,
×
154
            _ => fmt::Error,
×
UNCOV
155
        })
×
UNCOV
156
    }
×
157
}
158

159
impl TryFrom<&[u8]> for OhttpKeys {
160
    type Error = ParseOhttpKeysError;
161

UNCOV
162
    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
×
UNCOV
163
        let key_id = *bytes.first().ok_or(ParseOhttpKeysError::InvalidFormat)?;
×
UNCOV
164
        let compressed_pk = bytes.get(1..34).ok_or(ParseOhttpKeysError::InvalidFormat)?;
×
165

UNCOV
166
        let pubkey = bitcoin::secp256k1::PublicKey::from_slice(compressed_pk)
×
UNCOV
167
            .map_err(|_| ParseOhttpKeysError::InvalidPublicKey)?;
×
168

UNCOV
169
        let mut buf = vec![key_id];
×
UNCOV
170
        buf.extend_from_slice(KEM_ID);
×
UNCOV
171
        buf.extend_from_slice(&pubkey.serialize_uncompressed());
×
UNCOV
172
        buf.extend_from_slice(SYMMETRIC_LEN);
×
UNCOV
173
        buf.extend_from_slice(SYMMETRIC_KDF_AEAD);
×
UNCOV
174

×
UNCOV
175
        ohttp::KeyConfig::decode(&buf).map(Self).map_err(ParseOhttpKeysError::DecodeKeyConfig)
×
UNCOV
176
    }
×
177
}
178

179
impl std::str::FromStr for OhttpKeys {
180
    type Err = ParseOhttpKeysError;
181

182
    /// Parses a base64URL-encoded string into OhttpKeys.
183
    /// The string format is: key_id || compressed_public_key
UNCOV
184
    fn from_str(s: &str) -> Result<Self, Self::Err> {
×
UNCOV
185
        // TODO extract to utility function
×
UNCOV
186
        let oh_hrp: bech32::Hrp = bech32::Hrp::parse("OH").unwrap();
×
187

UNCOV
188
        let (hrp, bytes) =
×
UNCOV
189
            crate::bech32::nochecksum::decode(s).map_err(ParseOhttpKeysError::DecodeBech32)?;
×
190

UNCOV
191
        if hrp != oh_hrp {
×
192
            return Err(ParseOhttpKeysError::InvalidFormat);
×
UNCOV
193
        }
×
UNCOV
194

×
UNCOV
195
        Self::try_from(&bytes[..])
×
UNCOV
196
    }
×
197
}
198

199
impl PartialEq for OhttpKeys {
UNCOV
200
    fn eq(&self, other: &Self) -> bool {
×
UNCOV
201
        match (self.encode(), other.encode()) {
×
UNCOV
202
            (Ok(self_encoded), Ok(other_encoded)) => self_encoded == other_encoded,
×
203
            // If OhttpKeys::encode(&self) is Err, return false
204
            _ => false,
×
205
        }
UNCOV
206
    }
×
207
}
208

209
impl Eq for OhttpKeys {}
210

211
impl Deref for OhttpKeys {
212
    type Target = ohttp::KeyConfig;
213

UNCOV
214
    fn deref(&self) -> &Self::Target { &self.0 }
×
215
}
216

217
impl DerefMut for OhttpKeys {
UNCOV
218
    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
×
219
}
220

221
impl<'de> serde::Deserialize<'de> for OhttpKeys {
UNCOV
222
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
×
UNCOV
223
    where
×
UNCOV
224
        D: serde::Deserializer<'de>,
×
UNCOV
225
    {
×
UNCOV
226
        let bytes = Vec::<u8>::deserialize(deserializer)?;
×
UNCOV
227
        OhttpKeys::decode(&bytes).map_err(serde::de::Error::custom)
×
UNCOV
228
    }
×
229
}
230

231
impl serde::Serialize for OhttpKeys {
UNCOV
232
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
×
UNCOV
233
    where
×
UNCOV
234
        S: serde::Serializer,
×
UNCOV
235
    {
×
UNCOV
236
        let bytes = self.encode().map_err(serde::ser::Error::custom)?;
×
UNCOV
237
        bytes.serialize(serializer)
×
UNCOV
238
    }
×
239
}
240

241
#[derive(Debug)]
242
pub enum ParseOhttpKeysError {
243
    InvalidFormat,
244
    InvalidPublicKey,
245
    DecodeBech32(bech32::primitives::decode::CheckedHrpstringError),
246
    DecodeKeyConfig(ohttp::Error),
247
}
248

249
impl std::fmt::Display for ParseOhttpKeysError {
250
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
251
        match self {
×
252
            ParseOhttpKeysError::InvalidFormat => write!(f, "Invalid format"),
×
253
            ParseOhttpKeysError::InvalidPublicKey => write!(f, "Invalid public key"),
×
254
            ParseOhttpKeysError::DecodeBech32(e) => write!(f, "Failed to decode base64: {e}"),
×
255
            ParseOhttpKeysError::DecodeKeyConfig(e) => write!(f, "Failed to decode KeyConfig: {e}"),
×
256
        }
257
    }
×
258
}
259

260
impl std::error::Error for ParseOhttpKeysError {
261
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
×
262
        match self {
×
263
            ParseOhttpKeysError::DecodeBech32(e) => Some(e),
×
264
            ParseOhttpKeysError::DecodeKeyConfig(e) => Some(e),
×
265
            ParseOhttpKeysError::InvalidFormat | ParseOhttpKeysError::InvalidPublicKey => None,
×
266
        }
267
    }
×
268
}
269

270
#[cfg(test)]
271
mod test {
272
    use super::*;
273

274
    #[test]
275
    fn test_ohttp_keys_roundtrip() {
276
        use std::str::FromStr;
277

278
        use ohttp::hpke::{Aead, Kdf, Kem};
279
        use ohttp::{KeyId, SymmetricSuite};
280
        const KEY_ID: KeyId = 1;
281
        const KEM: Kem = Kem::K256Sha256;
282
        const SYMMETRIC: &[SymmetricSuite] =
283
            &[ohttp::SymmetricSuite::new(Kdf::HkdfSha256, Aead::ChaCha20Poly1305)];
284
        let keys = OhttpKeys(ohttp::KeyConfig::new(KEY_ID, KEM, Vec::from(SYMMETRIC)).unwrap());
285
        let serialized = &keys.to_string();
286
        let deserialized = OhttpKeys::from_str(serialized).unwrap();
287
        assert_eq!(keys.encode().unwrap(), deserialized.encode().unwrap());
288
    }
289
}
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