1use crate::headers::{VssHeaderProvider, VssHeaderProviderError};
2use async_trait::async_trait;
3use base64::engine::general_purpose::URL_SAFE_NO_PAD;
4use base64::Engine;
5use bitcoin::bip32::{ChildNumber, DerivationPath, Xpriv};
6use bitcoin::hashes::hex::FromHex;
7use bitcoin::hashes::sha256;
8use bitcoin::hashes::{Hash, HashEngine, Hmac, HmacEngine};
9use bitcoin::secp256k1::{Message, Secp256k1, SignOnly};
10use bitcoin::PrivateKey;
11use bitreq::Url;
12use serde::Deserialize;
13use std::collections::HashMap;
14use std::sync::RwLock;
15use std::time::{Duration, SystemTime};
16
17const HASHING_DERIVATION_INDEX: u32 = 0;
19const EXPIRY_BUFFER: Duration = Duration::from_secs(60);
21const K1_QUERY_PARAM: &str = "k1";
23const SIG_QUERY_PARAM: &str = "sig";
25const KEY_QUERY_PARAM: &str = "key";
27const AUTHORIZATION: &str = "Authorization";
29const MAX_RESPONSE_BODY_SIZE: usize = 16 * 1024 * 1024; #[derive(Debug, Clone)]
33struct JwtToken {
34 token_str: String,
35 expiry: Option<SystemTime>,
36}
37
38impl JwtToken {
39 fn is_expired(&self) -> bool {
40 self.expiry
41 .and_then(|expiry| {
42 SystemTime::now()
43 .checked_add(EXPIRY_BUFFER)
44 .map(|now_with_buffer| now_with_buffer > expiry)
45 })
46 .unwrap_or(false)
47 }
48}
49
50const DEFAULT_TIMEOUT_SECS: u64 = 10;
51
52pub struct LnurlAuthToJwtProvider {
54 engine: Secp256k1<SignOnly>,
55 parent_key: Xpriv,
56 url: String,
57 default_headers: HashMap<String, String>,
58 cached_jwt_token: RwLock<Option<JwtToken>>,
59}
60
61impl LnurlAuthToJwtProvider {
62 pub fn new(
75 parent_key: Xpriv, url: String, default_headers: HashMap<String, String>,
76 ) -> LnurlAuthToJwtProvider {
77 let engine = Secp256k1::signing_only();
78
79 LnurlAuthToJwtProvider {
80 engine,
81 parent_key,
82 url,
83 default_headers,
84 cached_jwt_token: RwLock::new(None),
85 }
86 }
87
88 async fn fetch_jwt_token(&self) -> Result<JwtToken, VssHeaderProviderError> {
89 let client = bitreq::Client::new(1);
90 let lnurl_request = bitreq::get(&self.url)
92 .with_headers(self.default_headers.clone())
93 .with_timeout(DEFAULT_TIMEOUT_SECS)
94 .with_max_body_size(Some(MAX_RESPONSE_BODY_SIZE));
95 let lnurl_response =
96 client.send_async(lnurl_request).await.map_err(VssHeaderProviderError::from)?;
97 let lnurl_str = String::from_utf8(lnurl_response.into_bytes()).map_err(|e| {
98 VssHeaderProviderError::InvalidData {
99 error: format!("LNURL response is not valid UTF-8: {}", e),
100 }
101 })?;
102
103 let signed_lnurl = sign_lnurl(&self.engine, &self.parent_key, &lnurl_str)?;
105 let auth_request = bitreq::get(&signed_lnurl)
106 .with_headers(self.default_headers.clone())
107 .with_timeout(DEFAULT_TIMEOUT_SECS)
108 .with_max_body_size(Some(MAX_RESPONSE_BODY_SIZE));
109 let auth_response =
110 client.send_async(auth_request).await.map_err(VssHeaderProviderError::from)?;
111 let lnurl_auth_response: LnurlAuthResponse =
112 serde_json::from_slice(&auth_response.into_bytes()).map_err(|e| {
113 VssHeaderProviderError::InvalidData {
114 error: format!("Failed to parse LNURL Auth response as JSON: {}", e),
115 }
116 })?;
117
118 let untrusted_token = match lnurl_auth_response {
119 LnurlAuthResponse { token: Some(token), .. } => token,
120 LnurlAuthResponse { reason: Some(reason), .. } => {
121 return Err(VssHeaderProviderError::AuthorizationError {
122 error: format!("LNURL Auth failed, reason is: {}", reason.escape_debug()),
123 });
124 },
125 _ => {
126 return Err(VssHeaderProviderError::InvalidData {
127 error: "LNURL Auth response did not contain a token nor an error".to_string(),
128 });
129 },
130 };
131 parse_jwt_token(untrusted_token)
132 }
133
134 async fn get_jwt_token(&self) -> Result<String, VssHeaderProviderError> {
135 let cached_token_str = self
136 .cached_jwt_token
137 .read()
138 .unwrap()
139 .as_ref()
140 .filter(|t| !t.is_expired())
141 .map(|t| t.token_str.clone());
142 if let Some(token_str) = cached_token_str {
143 Ok(token_str)
144 } else {
145 let jwt_token = self.fetch_jwt_token().await?;
146 *self.cached_jwt_token.write().unwrap() = Some(jwt_token.clone());
147 Ok(jwt_token.token_str)
148 }
149 }
150}
151
152#[async_trait]
153impl VssHeaderProvider for LnurlAuthToJwtProvider {
154 async fn get_headers(
155 &self, _request: &[u8],
156 ) -> Result<HashMap<String, String>, VssHeaderProviderError> {
157 let jwt_token = self.get_jwt_token().await?;
158 let mut headers = self.default_headers.clone();
159 headers.insert(AUTHORIZATION.to_string(), format!("Bearer {}", jwt_token));
160 Ok(headers)
161 }
162}
163
164fn hashing_key(
165 engine: &Secp256k1<SignOnly>, parent_key: &Xpriv,
166) -> Result<PrivateKey, VssHeaderProviderError> {
167 let hashing_child_number = ChildNumber::from_normal_idx(HASHING_DERIVATION_INDEX)
168 .map_err(VssHeaderProviderError::from)?;
169 parent_key
170 .derive_priv(engine, &vec![hashing_child_number])
171 .map(|xpriv| xpriv.to_priv())
172 .map_err(VssHeaderProviderError::from)
173}
174
175fn linking_key_path(
176 hashing_key: &PrivateKey, domain_name: &str,
177) -> Result<DerivationPath, VssHeaderProviderError> {
178 let mut engine = HmacEngine::<sha256::Hash>::new(&hashing_key.inner[..]);
179 engine.input(domain_name.as_bytes());
180 let result = Hmac::<sha256::Hash>::from_engine(engine).to_byte_array();
181 let children = result
183 .chunks_exact(4)
184 .take(4)
185 .map(|i| u32::from_be_bytes(i.try_into().unwrap()))
186 .map(ChildNumber::from);
187 Ok(DerivationPath::from_iter(children))
188}
189
190fn sign_lnurl(
191 engine: &Secp256k1<SignOnly>, parent_key: &Xpriv, lnurl_str: &str,
192) -> Result<String, VssHeaderProviderError> {
193 let invalid_lnurl = || VssHeaderProviderError::InvalidData {
195 error: format!("invalid lnurl: {}", lnurl_str.escape_debug()),
196 };
197 let mut lnurl = Url::parse(lnurl_str).map_err(|_| invalid_lnurl())?;
198 let domain = lnurl.base_url();
199 let k1_str = lnurl
200 .query_pairs()
201 .find(|(k, _)| k == K1_QUERY_PARAM)
202 .ok_or(invalid_lnurl())?
203 .1
204 .to_string();
205 let k1: [u8; 32] = FromHex::from_hex(&k1_str).map_err(|_| invalid_lnurl())?;
206
207 let hashing_private_key = hashing_key(engine, parent_key)?;
209 let linking_key_path = linking_key_path(&hashing_private_key, domain)?;
210 let linking_private_key = parent_key
211 .derive_priv(engine, &linking_key_path)
212 .map_err(VssHeaderProviderError::from)?
213 .to_priv();
214 let linking_public_key = linking_private_key.public_key(engine);
215 let message = Message::from_digest_slice(&k1).map_err(|_| {
216 VssHeaderProviderError::InvalidData { error: format!("invalid k1: {:?}", k1) }
217 })?;
218 let sig = engine.sign_ecdsa(&message, &linking_private_key.inner);
219
220 let serialized_sig = sig.serialize_der().to_string();
222 let serialized_pubkey = linking_public_key.to_string();
223 let query_params =
224 [(SIG_QUERY_PARAM, serialized_sig.as_str()), (KEY_QUERY_PARAM, serialized_pubkey.as_str())];
225 lnurl.append_query_params(query_params.into_iter());
226 Ok(lnurl.to_string())
227}
228
229#[derive(Deserialize, Debug, Clone)]
230struct LnurlAuthResponse {
231 reason: Option<String>,
232 token: Option<String>,
233}
234
235#[derive(Deserialize, Debug, Clone)]
236struct ExpiryClaim {
237 #[serde(rename = "exp")]
238 expiry_secs: Option<u64>,
239}
240
241fn parse_jwt_token(jwt_token: String) -> Result<JwtToken, VssHeaderProviderError> {
242 let parts: Vec<&str> = jwt_token.split('.').collect();
243 let invalid = || VssHeaderProviderError::InvalidData {
244 error: format!("invalid JWT token: {}", jwt_token.escape_debug()),
245 };
246 if parts.len() != 3 {
247 return Err(invalid());
248 }
249 let _ = URL_SAFE_NO_PAD.decode(parts[0]).map_err(|_| invalid())?;
250 let bytes = URL_SAFE_NO_PAD.decode(parts[1]).map_err(|_| invalid())?;
251 let _ = URL_SAFE_NO_PAD.decode(parts[2]).map_err(|_| invalid())?;
252 let claim: ExpiryClaim = serde_json::from_slice(&bytes).map_err(|_| invalid())?;
253 let expiry =
254 claim.expiry_secs.and_then(|e| SystemTime::UNIX_EPOCH.checked_add(Duration::from_secs(e)));
255 Ok(JwtToken { token_str: jwt_token, expiry })
256}
257
258impl From<bitcoin::bip32::Error> for VssHeaderProviderError {
259 fn from(e: bitcoin::bip32::Error) -> VssHeaderProviderError {
260 VssHeaderProviderError::InternalError { error: e.to_string() }
261 }
262}
263
264impl From<bitreq::Error> for VssHeaderProviderError {
265 fn from(e: bitreq::Error) -> VssHeaderProviderError {
266 VssHeaderProviderError::RequestError { error: e.to_string() }
267 }
268}
269
270#[cfg(test)]
271mod test {
272 use crate::headers::lnurl_auth_jwt::{linking_key_path, sign_lnurl};
273 use bitcoin::bip32::Xpriv;
274 use bitcoin::hashes::hex::FromHex;
275 use bitcoin::secp256k1::Secp256k1;
276 use bitcoin::secp256k1::SecretKey;
277 use bitcoin::Network;
278 use bitcoin::PrivateKey;
279 use std::str::FromStr;
280
281 #[test]
282 fn test_linking_key_path() {
283 let hashing_key = PrivateKey::new(
286 SecretKey::from_str("7d417a6a5e9a6a4a879aeaba11a11838764c8fa2b959c242d43dea682b3e409b")
287 .unwrap(),
288 Network::Testnet, );
290 let path = linking_key_path(&hashing_key, "site.com").unwrap();
291 let numbers: Vec<u32> = path.into_iter().map(|c| u32::from(c.clone())).collect();
292 assert_eq!(numbers, vec![1588488367, 2659270754, 38110259, 4136336762]);
293 }
294
295 #[test]
296 fn test_sign_lnurl() {
297 let engine = Secp256k1::signing_only();
298 let parent_key_bytes: [u8; 32] =
299 FromHex::from_hex("abababababababababababababababababababababababababababababababab")
300 .unwrap();
301 let parent_key = Xpriv::new_master(Network::Testnet, &parent_key_bytes).unwrap();
302 let signed = sign_lnurl(
303 &engine,
304 &parent_key,
305 "https://example.com/path?tag=login&k1=e2af6254a8df433264fa23f67eb8188635d15ce883e8fc020989d5f82ae6f11e",
306 )
307 .unwrap();
308 assert_eq!(
309 signed,
310 "https://example.com/path?tag=login&k1=e2af6254a8df433264fa23f67eb8188635d15ce883e8fc020989d5f82ae6f11e&sig=3045022100a75df468de452e618edb8030016eb0894204655c7d93ece1be007fcf36843522022048bc2f00a0a5a30601d274b49cfaf9ef4c76176e5401d0dfb195f5d6ab8ab4c4&key=02d9eb1b467517d685e3b5439082c14bb1a2c9ae672df4d9046d208c193a5846e0",
311 );
312 }
313}