From e8a977007bef2fe6dad6f03f50d182c4bc35bccc Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Tue, 24 Mar 2026 14:28:52 -0700 Subject: [PATCH 1/2] fix: accept standard base64 in WWW-Authenticate request field base64url_decode now normalizes standard base64 ('+', '/', '=' padding) to URL-safe alphabet before decoding. Encoding remains strict base64url no-pad per spec. This aligns with the mppx TypeScript SDK, whose ox-powered decoder accepts both alphabets. Without this fix, servers that emit standard base64 in the WWW-Authenticate request field (e.g. OpenVPS) cause tempo-request to fail with 'Unsupported payment method: ' because the challenge parse error cascades before the method field is read. --- src/protocol/core/headers.rs | 26 +++++++++++++++++++ src/protocol/core/types.rs | 50 ++++++++++++++++++++++++++++++++++-- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/protocol/core/headers.rs b/src/protocol/core/headers.rs index e5b4551e..cc704821 100644 --- a/src/protocol/core/headers.rs +++ b/src/protocol/core/headers.rs @@ -845,6 +845,32 @@ mod tests { assert!(err.to_string().contains("Invalid method")); } + #[test] + fn test_parse_www_authenticate_accepts_standard_base64_request() { + // Reproduces real-world interop issue: server sends the `request` + // field as standard base64 ('+', '/', '=' padding) instead of + // base64url (no padding). The parser should accept both variants, + // matching the mppx TypeScript SDK behavior. + use base64::engine::general_purpose::STANDARD; + use base64::Engine as _; + + let payload = r#"{"amount":"94","currency":"0x20c000000000000000000000b9537d11c60e8b50","methodDetails":{"chainId":4217},"recipient":"0x8A739f3A6f40194C0128904bC387e63d9C0577A4"}"#; + let request_b64 = STANDARD.encode(payload.as_bytes()); + // Verify it has padding + assert!(request_b64.ends_with('=')); + + let header = format!( + r#"Payment id="test-123", realm="mpp-hosting", method="tempo", intent="charge", request="{request_b64}", description="VPS provisioning", expires="2026-03-24T21:20:34Z""#, + ); + let challenge = parse_www_authenticate(&header).unwrap(); + assert_eq!(challenge.id, "test-123"); + assert_eq!(challenge.method.to_string(), "tempo"); + assert_eq!(challenge.intent.to_string(), "charge"); + + let decoded: serde_json::Value = challenge.request.decode().unwrap(); + assert_eq!(decoded["amount"], "94"); + } + #[test] fn test_parse_receipt_rejects_non_iso8601_timestamp() { // {"method":"tempo","reference":"0xabc","status":"success","timestamp":"Jan 29 2026 12:00"} diff --git a/src/protocol/core/types.rs b/src/protocol/core/types.rs index 86c301f2..bf9f2e54 100644 --- a/src/protocol/core/types.rs +++ b/src/protocol/core/types.rs @@ -245,10 +245,27 @@ pub fn base64url_encode(data: &[u8]) -> String { URL_SAFE_NO_PAD.encode(data) } -/// Decode a base64url string (no padding) to bytes. +/// Decode a base64url string to bytes. +/// +/// Accepts both URL-safe (`-`, `_`) and standard (`+`, `/`) alphabets, +/// with or without `=` padding. This follows Postel's law — be strict in +/// what you produce (see [`base64url_encode`]) but lenient in what you +/// accept — and aligns with the mppx TypeScript SDK which also accepts +/// both variants. pub fn base64url_decode(input: &str) -> Result> { + // Normalize standard base64 to URL-safe alphabet and strip padding. + let normalized: String = input + .chars() + .filter(|c| *c != '=') + .map(|c| match c { + '+' => '-', + '/' => '_', + other => other, + }) + .collect(); + URL_SAFE_NO_PAD - .decode(input) + .decode(&normalized) .map_err(|e| MppError::InvalidBase64Url(format!("Invalid base64url: {}", e))) } @@ -421,6 +438,35 @@ mod tests { assert!(base64url_decode(noisy).is_err()); } + #[test] + fn test_base64url_decode_accepts_standard_base64_with_padding() { + // Standard base64 ('+', '/') with '=' padding — as produced by + // some servers that use RFC 4648 §4 instead of §5. + let data = b"hello world"; + let standard = base64::engine::general_purpose::STANDARD.encode(data); + assert!(standard.contains('=') || standard.contains('+') || standard.contains('/')); + let decoded = base64url_decode(&standard).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn test_base64url_decode_accepts_url_safe_without_padding() { + let data = b"hello world"; + let encoded = base64url_encode(data); + let decoded = base64url_decode(&encoded).unwrap(); + assert_eq!(decoded, data); + } + + #[test] + fn test_base64url_decode_accepts_standard_alphabet_no_padding() { + // Standard alphabet but padding stripped — another common variant. + let data = b"test data with special chars: <>?"; + let standard = base64::engine::general_purpose::STANDARD.encode(data); + let no_pad = standard.trim_end_matches('='); + let decoded = base64url_decode(no_pad).unwrap(); + assert_eq!(decoded, data); + } + #[test] fn test_base64url_json() { let value = serde_json::json!({"amount": "1000", "currency": "pathUSD"}); From 8917d674ca14c0db17e0447e32f038b1da590315 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Mar 2026 21:29:46 +0000 Subject: [PATCH 2/2] chore: add changelog --- .changelog/vain-tigers-dance.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changelog/vain-tigers-dance.md diff --git a/.changelog/vain-tigers-dance.md b/.changelog/vain-tigers-dance.md new file mode 100644 index 00000000..4afaf23b --- /dev/null +++ b/.changelog/vain-tigers-dance.md @@ -0,0 +1,5 @@ +--- +mpp: patch +--- + +Fixed `base64url_decode` to accept standard base64 (`+`, `/`, `=` padding) in addition to URL-safe base64, following Postel's law and aligning with the mppx TypeScript SDK behavior. Added tests covering standard base64 with padding, URL-safe without padding, and standard alphabet without padding in both `types.rs` and `headers.rs`.