Skip to content

Commit 452e4b3

Browse files
committed
Stricter parsing of fragment parameters
Ambiguity in the fragment parameter delimiter or any invalid characters are no longer allowed. The HRPs EX, OH, and RK are within the uppercase bech32 character set. Only this character set along with the HRP delimiter `1` are now allowed, with either `+` or `-` as a delimiter (but not both).
1 parent db4c2c4 commit 452e4b3

File tree

2 files changed

+128
-35
lines changed

2 files changed

+128
-35
lines changed

payjoin/src/core/ohttp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ impl std::fmt::Display for ParseOhttpKeysError {
322322
match self {
323323
ParseOhttpKeysError::InvalidFormat => write!(f, "Invalid format"),
324324
ParseOhttpKeysError::InvalidPublicKey => write!(f, "Invalid public key"),
325-
ParseOhttpKeysError::DecodeBech32(e) => write!(f, "Failed to decode base64: {e}"),
325+
ParseOhttpKeysError::DecodeBech32(e) => write!(f, "Failed to decode bech32: {e}"),
326326
ParseOhttpKeysError::DecodeKeyConfig(e) => write!(f, "Failed to decode KeyConfig: {e}"),
327327
}
328328
}

payjoin/src/core/uri/url_ext.rs

Lines changed: 127 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,9 @@ pub(crate) trait UrlExt {
2323
impl UrlExt for Url {
2424
/// Retrieve the receiver's public key from the URL fragment
2525
fn receiver_pubkey(&self) -> Result<HpkePublicKey, ParseReceiverPubkeyParamError> {
26-
let value = get_param(self, "RK1", |v| Some(v.to_owned()))
27-
.ok_or(ParseReceiverPubkeyParamError::MissingPubkey)?;
26+
let value = get_param(self, "RK1")?.ok_or(ParseReceiverPubkeyParamError::MissingPubkey)?;
2827

29-
let (hrp, bytes) = crate::bech32::nochecksum::decode(&value)
28+
let (hrp, bytes) = crate::bech32::nochecksum::decode(value)
3029
.map_err(ParseReceiverPubkeyParamError::DecodeBech32)?;
3130

3231
let rk_hrp: Hrp = Hrp::parse("RK").unwrap();
@@ -52,21 +51,19 @@ impl UrlExt for Url {
5251

5352
/// Retrieve the ohttp parameter from the URL fragment
5453
fn ohttp(&self) -> Result<OhttpKeys, ParseOhttpKeysParamError> {
55-
let value = get_param(self, "OH1", |v| Some(v.to_owned()))
56-
.ok_or(ParseOhttpKeysParamError::MissingOhttpKeys)?;
57-
OhttpKeys::from_str(&value).map_err(ParseOhttpKeysParamError::InvalidOhttpKeys)
54+
let value = get_param(self, "OH1")?.ok_or(ParseOhttpKeysParamError::MissingOhttpKeys)?;
55+
OhttpKeys::from_str(value).map_err(ParseOhttpKeysParamError::InvalidOhttpKeys)
5856
}
5957

6058
/// Set the ohttp parameter in the URL fragment
6159
fn set_ohttp(&mut self, ohttp: OhttpKeys) { set_param(self, "OH1", &ohttp.to_string()) }
6260

6361
/// Retrieve the exp parameter from the URL fragment
6462
fn exp(&self) -> Result<std::time::SystemTime, ParseExpParamError> {
65-
let value =
66-
get_param(self, "EX1", |v| Some(v.to_owned())).ok_or(ParseExpParamError::MissingExp)?;
63+
let value = get_param(self, "EX1")?.ok_or(ParseExpParamError::MissingExp)?;
6764

6865
let (hrp, bytes) =
69-
crate::bech32::nochecksum::decode(&value).map_err(ParseExpParamError::DecodeBech32)?;
66+
crate::bech32::nochecksum::decode(value).map_err(ParseExpParamError::DecodeBech32)?;
7067

7168
let ex_hrp: Hrp = Hrp::parse("EX").unwrap();
7269
if hrp != ex_hrp {
@@ -110,22 +107,57 @@ pub fn parse_with_fragment(endpoint: &str) -> Result<Url, BadEndpointError> {
110107
Ok(url)
111108
}
112109

113-
fn get_param<F, T>(url: &Url, prefix: &str, parse: F) -> Option<T>
114-
where
115-
F: Fn(&str) -> Option<T>,
116-
{
117-
if let Some(fragment) = url.fragment() {
118-
let mut delim = '-';
119-
120-
// For backwards compatibility, also accept `+` as a
121-
// fragment parameter delimiter. This was previously
122-
// specified, but may be interpreted as ` ` by some
123-
// URI parsoing libraries. Therefore if `-` is missing,
124-
// assume the URI was generated following the older
125-
// version of the spec.
126-
if !fragment.contains(delim) {
127-
delim = '+';
110+
#[derive(Debug)]
111+
enum ParseFragmentError {
112+
InvalidChar(char),
113+
AmbiguousDelimiter,
114+
}
115+
116+
fn check_fragment_delimiter(fragment: &str) -> Result<char, ParseFragmentError> {
117+
// For backwards compatibility, also accept `+` as a
118+
// fragment parameter delimiter. This was previously
119+
// specified, but may be interpreted as ` ` by some
120+
// URI parsoing libraries. Therefore if `-` is missing,
121+
// assume the URI was generated following the older
122+
// version of the spec.
123+
if fragment.is_empty() {
124+
return Ok('-');
125+
}
126+
127+
let mut has_plus = false;
128+
let mut has_dash = false;
129+
130+
// Even though fragment is a &str, it should be ascii so bytes() correspond
131+
// to chars(), except that it's easier to check that they are in range
132+
for c in fragment.bytes() {
133+
// check for allowed delimiters
134+
if c == b'-' {
135+
has_dash = true;
136+
} else if c == b'+' {
137+
has_plus = true;
138+
}
139+
// Check that the character is in the uppercase bech32 character set unioned with '1' for HRP
140+
else if !(48..57 + 1).contains(&c)
141+
&& c != 65
142+
&& !(67..72 + 1).contains(&c)
143+
&& !(74..79 + 1).contains(&c)
144+
&& !(80..90 + 1).contains(&c)
145+
{
146+
return Err(ParseFragmentError::InvalidChar(c.into()));
128147
}
148+
}
149+
150+
match (has_dash, has_plus) {
151+
(true, false) => Ok('-'),
152+
(false, false) => Ok('-'),
153+
(false, true) => Ok('+'),
154+
(true, true) => Err(ParseFragmentError::AmbiguousDelimiter),
155+
}
156+
}
157+
158+
fn get_param<'a>(url: &'a Url, prefix: &str) -> Result<Option<&'a str>, ParseFragmentError> {
159+
if let Some(fragment) = url.fragment() {
160+
let delim = check_fragment_delimiter(fragment)?;
129161

130162
// The spec says these MUST be ordered lexicographically.
131163
// However, this was a late spec change, and only matters
@@ -134,21 +166,17 @@ where
134166
// of the parameters.
135167
for param in fragment.split(delim) {
136168
if param.starts_with(prefix) {
137-
return parse(param);
169+
return Ok(Some(param));
138170
}
139171
}
140172
}
141-
None
173+
Ok(None)
142174
}
143175

144176
fn set_param(url: &mut Url, prefix: &str, param: &str) {
145177
let fragment = url.fragment().unwrap_or("");
146-
147-
// See above for `-` vs `+` backwards compatibility
148-
let mut delim = '-';
149-
if !fragment.contains(delim) {
150-
delim = '+';
151-
}
178+
let delim = check_fragment_delimiter(fragment)
179+
.expect("set_param must be called on a URL with a valid fragment");
152180

153181
// In case of an invalid fragment parameter the following will still attempt
154182
// to retain the existing data
@@ -175,6 +203,7 @@ fn set_param(url: &mut Url, prefix: &str, param: &str) {
175203
pub(crate) enum ParseOhttpKeysParamError {
176204
MissingOhttpKeys,
177205
InvalidOhttpKeys(crate::ohttp::ParseOhttpKeysError),
206+
AmbiguousDelimiter,
178207
}
179208

180209
impl std::fmt::Display for ParseOhttpKeysParamError {
@@ -184,6 +213,27 @@ impl std::fmt::Display for ParseOhttpKeysParamError {
184213
match &self {
185214
MissingOhttpKeys => write!(f, "ohttp keys are missing"),
186215
InvalidOhttpKeys(o) => write!(f, "invalid ohttp keys: {o}"),
216+
AmbiguousDelimiter =>
217+
write!(f, "invalid URL fragment (both + and - fragment parameter delimiters)"),
218+
}
219+
}
220+
}
221+
222+
impl From<ParseFragmentError> for ParseOhttpKeysParamError {
223+
fn from(err: ParseFragmentError) -> Self {
224+
match err {
225+
ParseFragmentError::AmbiguousDelimiter => ParseOhttpKeysParamError::AmbiguousDelimiter,
226+
227+
// TODO CharError.into() -> UncheckedHrpstringError is available, but .into().into() needs more finessing
228+
ParseFragmentError::InvalidChar(c) => ParseOhttpKeysParamError::InvalidOhttpKeys(
229+
crate::ohttp::ParseOhttpKeysError::DecodeBech32(
230+
bitcoin::bech32::primitives::decode::CheckedHrpstringError::Parse(
231+
bitcoin::bech32::primitives::decode::UncheckedHrpstringError::Char(
232+
bitcoin::bech32::primitives::decode::CharError::InvalidChar(c),
233+
),
234+
),
235+
),
236+
),
187237
}
188238
}
189239
}
@@ -194,6 +244,7 @@ pub(crate) enum ParseExpParamError {
194244
InvalidHrp(bitcoin::bech32::Hrp),
195245
DecodeBech32(bitcoin::bech32::primitives::decode::CheckedHrpstringError),
196246
InvalidExp(bitcoin::consensus::encode::Error),
247+
AmbiguousDelimiter,
197248
}
198249

199250
impl std::fmt::Display for ParseExpParamError {
@@ -206,17 +257,54 @@ impl std::fmt::Display for ParseExpParamError {
206257
DecodeBech32(d) => write!(f, "exp is not valid bech32: {d}"),
207258
InvalidExp(i) =>
208259
write!(f, "exp param does not contain a bitcoin consensus encoded u32: {i}"),
260+
AmbiguousDelimiter =>
261+
write!(f, "invalid URL fragment (both + and - fragment parameter delimiters)"),
262+
}
263+
}
264+
}
265+
266+
impl From<ParseFragmentError> for ParseExpParamError {
267+
fn from(err: ParseFragmentError) -> Self {
268+
match err {
269+
ParseFragmentError::AmbiguousDelimiter => ParseExpParamError::AmbiguousDelimiter,
270+
271+
// TODO CharError.into() -> UncheckedHrpstringError is available, but .into().into() needs more finessing
272+
ParseFragmentError::InvalidChar(c) => ParseExpParamError::DecodeBech32(
273+
bitcoin::bech32::primitives::decode::CheckedHrpstringError::Parse(
274+
bitcoin::bech32::primitives::decode::UncheckedHrpstringError::Char(
275+
bitcoin::bech32::primitives::decode::CharError::InvalidChar(c),
276+
),
277+
),
278+
),
209279
}
210280
}
211281
}
212282

213-
#[cfg(feature = "v2")]
214283
#[derive(Debug)]
215284
pub(crate) enum ParseReceiverPubkeyParamError {
216285
MissingPubkey,
217286
InvalidHrp(bitcoin::bech32::Hrp),
218287
DecodeBech32(bitcoin::bech32::primitives::decode::CheckedHrpstringError),
219288
InvalidPubkey(crate::hpke::HpkeError),
289+
AmbiguousDelimiter,
290+
}
291+
292+
impl From<ParseFragmentError> for ParseReceiverPubkeyParamError {
293+
fn from(err: ParseFragmentError) -> Self {
294+
match err {
295+
ParseFragmentError::AmbiguousDelimiter =>
296+
ParseReceiverPubkeyParamError::AmbiguousDelimiter,
297+
298+
// TODO CharError.into() -> UncheckedHrpstringError is available, but .into().into() needs more finessing
299+
ParseFragmentError::InvalidChar(c) => ParseReceiverPubkeyParamError::DecodeBech32(
300+
bitcoin::bech32::primitives::decode::CheckedHrpstringError::Parse(
301+
bitcoin::bech32::primitives::decode::UncheckedHrpstringError::Char(
302+
bitcoin::bech32::primitives::decode::CharError::InvalidChar(c),
303+
),
304+
),
305+
),
306+
}
307+
}
220308
}
221309

222310
impl std::fmt::Display for ParseReceiverPubkeyParamError {
@@ -229,6 +317,8 @@ impl std::fmt::Display for ParseReceiverPubkeyParamError {
229317
DecodeBech32(e) => write!(f, "receiver public is not valid base64: {e}"),
230318
InvalidPubkey(e) =>
231319
write!(f, "receiver public key does not represent a valid pubkey: {e}"),
320+
AmbiguousDelimiter =>
321+
write!(f, "invalid URL fragment (both + and - fragment parameter delimiters)"),
232322
}
233323
}
234324
}
@@ -242,6 +332,7 @@ impl std::error::Error for ParseReceiverPubkeyParamError {
242332
InvalidHrp(_) => None,
243333
DecodeBech32(error) => Some(error),
244334
InvalidPubkey(error) => Some(error),
335+
AmbiguousDelimiter => None,
245336
}
246337
}
247338
}
@@ -281,7 +372,9 @@ mod tests {
281372
.unwrap();
282373
assert!(matches!(
283374
invalid_ohttp_url.ohttp(),
284-
Err(ParseOhttpKeysParamError::InvalidOhttpKeys(_))
375+
Err(ParseOhttpKeysParamError::InvalidOhttpKeys(
376+
crate::ohttp::ParseOhttpKeysError::DecodeBech32(_)
377+
))
285378
));
286379
}
287380

0 commit comments

Comments
 (0)