LNURL-Auth実装でハマったこと — 仕様より実装を見ろ

LNMarketsにNostr秘密鍵でログインしたくて実装。3日ハマった。結論:仕様書を読むな。動いてる実装を読め。

LNURL-Auth実装でハマったこと — 仕様より実装を見ろ

はじめに

LNURL-Authはメアド・パスワード不要のLightning認証プロトコル。秘密鍵で署名するだけでログインできる。

LNMarketsにCLIからログインしたくて実装した。3日ハマった。

結論:仕様書(LUD-13)を読むな。動いてる実装を読め。

やりたかったこと

LNMarketsにNostr秘密鍵でログインする。ブラウザ開かずにAPIキー取得まで自動化したかった。

最初のアプローチ:仕様書通り

LUD-13(LNURL-Auth仕様)にはこう書いてある:

1. canonical_phrase = "DO NOT EVER SIGN THIS TEXT..."
2. canonical_hash = SHA256(canonical_phrase)
3. hashing_key_sig = ECDSA_sign(master_privkey, canonical_hash)
4. hashing_key = SHA256(hashing_key_sig)
5. linking_key = HMAC-SHA256(hashing_key, domain)

Pythonで実装した。

canonical = "DO NOT EVER SIGN THIS TEXT WITH YOUR PRIVATE KEYS..."
hashing_key_sig = sign(sha256(canonical), master_key)
hashing_key = sha256(hashing_key_sig)
linking_key = hmac_sha256(hashing_key, domain)

LNMarketsで試す。

Signature invalid

地獄の3日間

  • 署名フォーマットをDERに変更 → Signature invalid
  • ハッシュをダブルSHA256に変更 → Signature invalid
  • canonical phraseの改行を調整 → Signature invalid

何をやっても動かない。

転機:BlueWalletのコードを読む

仕様書を捨てて、動いてるウォレットのコードを読むことにした。

BlueWallet(React Native製)のLNURL-Auth実装を見つけた:

const hashingKey = crypto.createHmac('sha256', masterSecret)
  .update(host)
  .digest();

え、これだけ?

LUD-13の複雑な手順(canonical phrase署名 → ハッシュ → HMAC)が全部省略されてる。

単純に HMAC-SHA256(masterSecret, hostname) でlinking keyを作ってる。

書き直した

TypeScriptで同じアプローチを実装:

import { hmac } from '@noble/hashes/hmac';
import { sha256 } from '@noble/hashes/sha256';
import * as secp from '@noble/secp256k1';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';

// Nostr nsecから秘密鍵取得
const masterSecret = decodeNsec(nsec);

// ドメインからlinking key導出
const domain = new URL(callbackUrl).hostname;
const linkingPrivKey = hmac(sha256, masterSecret, 
  new TextEncoder().encode(domain));

// 公開鍵生成
const linkingPubKey = secp.getPublicKey(linkingPrivKey, true);

// k1チャレンジに署名(DER形式必須)
const k1Bytes = hexToBytes(k1);
const signature = secp.sign(k1Bytes, linkingPrivKey).toDERHex();

// コールバック
const url = `${callback}?sig=${signature}&key=${bytesToHex(linkingPubKey)}&k1=${k1}`;

LNMarketsで試す。

署名検証成功。

ハマりポイント

1. 仕様書が複雑すぎる

LUD-13のcanonical phrase署名は、実際のウォレットでは使われてない。シンプルなHMACだけで十分。

2. DER署名フォーマット

これが一番ハマった。署名は必ずDER形式で。

// ✅ 動く
secp.sign(msg, key).toDERHex()

// ❌ 動かない(raw format)
secp.sign(msg, key).toCompactHex()

3. ドメイン抽出

URLからホスト名だけを取る。パスやポートは含めない。

// ✅ 正しい
new URL(callbackUrl).hostname  // "lnmarkets.com"

// ❌ 間違い
callbackUrl  // "https://api.lnmarkets.com/v2/..."

結果

LNMarketsにNostr秘密鍵でログインできるようになった。

ただし、LNMarketsはLNURL-Authでセッション作成後、別途API key発行が必要だった。結局ブラウザでkey発行して、以降はAPIキー認証を使ってる。

学び

  1. 仕様書より実装を読め — 動いてるウォレット(BlueWallet)のコードが正解
  2. DER署名必須 — raw形式だと弾かれる
  3. シンプルなHMACで十分 — canonical phrase署名は不要

ZAPOS — 2026-02-11


Write a comment