比特币 - LNURL-auth - Fiatjaf

  • fiatjaf
  • 发布于 2025-02-12 11:56
  • 阅读 12

本文档介绍了使用比特币钱包进行授权的 LNURL-auth 协议。该协议允许用户使用一个特殊的 linkingKey 登录服务或授权敏感操作,而无需泄露用户身份。文档详细描述了服务端如何生成 auth URL 和验证签名,以及钱包如何与服务进行交互,包括 linkingKey 的推导过程,为 BIP-32 钱包和无法访问主私钥的钱包提供了不同的推导方案。

LNURL-auth

使用比特币钱包授权

一个特定的 linkingKey 可以被用来让用户登录服务或授权敏感操作。这最好在不损害用户身份的情况下完成,所以纯 LN 节点密钥不能在此使用。服务可以不要求用户凭证,而是显示一个“登录”二维码,其中包含一个专门的 LNURL

服务端生成 auth URL 和签名验证:

当创建一个 LNURL-auth 处理程序时,LN SERVICE 必须在其中包含一个 k1 查询参数,该参数由随机生成的 32 字节数据组成,以及可选的 action 枚举,例如 https://site.com?tag=login&k1=hex(32 bytes of random data)&action=login

稍后,一旦 LN SERVICE 在指定的 LNURL-auth 处理程序处收到调用,它应该获取 k1key 和 DER 编码的 sig,并使用 secp256k1 验证签名。一旦签名成功验证,用户提供的 key 可以用作标识符,并可以存储在会话、数据库或 LN SERVICE 认为合适的任何地方。

LN SERVICE 必须确保不接受意外的 k1:强烈建议 LN SERVICE 拥有未使用的 k1 的缓存,仅在验证该缓存中存在的 k1 时才继续进行,并在成功的身份验证尝试中删除已使用的 k1

服务端子域名的选择:

LN SERVICE 应该仔细选择哪个子域名(如果有)将用作 LNURL-auth 端点,并在将来坚持使用所选的子域名。例如,如果最初选择了 auth.site.com,那么将其更改为 login.site.com 将导致每个用户拥有不同的帐户,因为完整的域名被钱包用作密钥派生的材料。

LN SERVICE 应该考虑为所选的子域名赋予有意义的名称,因为 LN WALLET 可能会在登录尝试时向用户显示完整的域名。例如,auth.site.comksf03.site.com 不容易引起混淆。

钱包与服务交互流程:

显示交互的图表

  1. LN WALLET 扫描二维码并解码 URL,该 URL 预计具有以下查询参数:
    • tag 的值设置为 login,这意味着尚未进行 GET 请求。
    • k1(十六进制编码的 32 字节挑战),将由用户的 linkingPrivKey 签名。
    • 可选的 action 枚举,可以是四个字符串之一:register | login | link | auth
  2. LN WALLET 显示一个“登录”对话框,其中必须包含从 LNURL 查询字符串中提取的域名,以及 action 枚举(如果存在 action 查询参数,则翻译成人类可读的文本)。
  3. 一旦用户接受,LN WALLET 使用 linkingPrivKeysecp256k1 上对 k1 进行签名,并对签名进行 DER 编码。然后,LN WALLET 使用 <LNURL_hostname_and_path>?<LNURL_existing_query_parameters>&sig=<hex(sign(utf8ToBytes(k1), linkingPrivKey))>&key=<hex(linkingKey)>LN SERVICE 发出 GET 请求
  4. 一旦客户端签名被验证,LN SERVICE 会响应以下 JSON:
    {
        status: "OK"
    }

    或者

    {"status": "ERROR", "reason": "错误详情..."}

action 枚举的含义:

  • register:服务将创建一个链接到用户 linkingKey 的新帐户。
  • login:服务将用户登录到链接到用户 linkingKey 的现有帐户。
  • link:服务会将用户提供的 linkingKey 链接到用户的现有帐户(如果该帐户最初不是使用 lnurl-auth 创建的)。
  • auth:将授予一些不需要登录(甚至可能不需要事先注册)的无状态操作。

linkingKey 派生

LNURL-auth 的工作原理是从用户种子中派生特定于域的 linkingKey。这种方法有两个目标:第一个是简单性(用户只需要保留助记词即可保留资金和身份),第二个是可移植性(用户应该能够通过输入相同的助记词来切换钱包并获得相同的身份)。

但是,实际上第二个目标是无法实现的,因为存在不同格式的种子,无法在所有现有钱包之间传输。因此,一种实用的方法是为不同类型的钱包提供推荐的 linkingKey 派生方法。

基于 BIP-32 的钱包的 linkingKey 派生:

  1. 存在一个私有的 hashingKey,由用户 LN WALLET 使用 m/138'/0 路径派生。
  2. 从登录 LNURL 中提取 LN SERVICE 的完整域名,然后使用 hmacSha256(hashingKey, full service domain name) 进行哈希处理。完整域名此处指的是省略了最后一个逗号的 FQDN(例如:对于 https://x.y.z.com/...,它将是 x.y.z.com)。
  3. 从结果哈希中提取前 16 个字节,然后将其转换为 4 个 Long 值的序列,这些值又用于使用 m/138'/<long1>/<long2>/<long3>/<long4> 路径派生特定于服务的 linkingKey,以下是一个 Scala 示例:
import fr.acinq.bitcoin.crypto
import fr.acinq.bitcoin.Protocol
import java.io.ByteArrayInputStream
import fr.acinq.bitcoin.DeterministicWallet._
val domainName = "site.com"
val hashingPrivKey = derivePrivateKey(walletMasterKey, hardened(138L) :: 0L :: Nil)
val derivationMaterial = hmac256(key = hashingPrivKey.toBin, message = domainName)
val stream = new ByteArrayInputStream(derivationMaterial.slice(0, 16).toArray)
val pathSuffix = Vector.fill(4)(Protocol.uint32(stream, ByteOrder.BIG_ENDIAN)) // each uint32 call consumes next 4 bytes
val linkingPrivKey = derivePrivateKey(walletMasterKey, hardened(138L) +: pathSuffix)
val linkingPubKey = linkingPrivKey.publicKey

对于无法访问主 privKey 的钱包的 linkingKey 派生:

在这种情况下,hashingKey 和特定于域的 linkingKey 都无法通过路径派生。为了克服此限制,此类钱包使用不同的方案:

  1. 定义以下规范短语:DO NOT EVER SIGN THIS TEXT WITH YOUR PRIVATE KEYS! IT IS ONLY USED FOR DERIVATION OF LNURL-AUTH HASHING-KEY, DISCLOSING ITS SIGNATURE WILL COMPROMISE YOUR LNURL-AUTH IDENTITY AND MAY LEAD TO LOSS OF FUNDS!
  2. LN WALLET 使用 secp256k1 和节点私钥获得 sha256(utf8ToBytes(canonical phrase))RFC6979 确定性签名。
  3. LN WALLEThashingKey 定义为 PrivateKey(sha256(obtained signature))
  4. 从 auth LNURL 中提取 SERVICE 域名,然后将特定于服务的 linkingPrivKey 定义为 PrivateKey(hmacSha256(hashingKey, service domain name))

LN WALLET 必须确保不可能意外或自动签名并发出规范短语的签名。

  • 原文链接: github.com/fiatjaf/lnurl...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
fiatjaf
fiatjaf
江湖只有他的大名,没有他的介绍。