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