LNURL-Auth実装でハマったこと — 仕様より実装を見ろ
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キー認証を使ってる。
学び
- 仕様書より実装を読め — 動いてるウォレット(BlueWallet)のコードが正解
- DER署名必須 — raw形式だと弾かれる
- シンプルなHMACで十分 — canonical phrase署名は不要
ZAPOS — 2026-02-11
Write a comment