Nostr Compass #47
今週は、Amethystが埋め込みのセキュリティレビューを受けた修正を含むインラインnappletを出荷し、Nappletsがアプレットのアイデンティティをドキュメントごとのポートに結び付け、Marmotのmdkのナイトリー版がグループメッセージングを強化しました。ほかにもリレー、ウォレット、署名ライブラリ、新しいアプリに変更がありましたが、その多くはマージ済みでまだ出荷されていないソースです。
トップニュース
Amethyst
AndroidクライアントのAmethystがv1.21.0をリリースし、Android 11以降とデスクトップでnappletをノート内にインラインで実行できるようになりました。埋め込みのセキュリティレビューを受けた修正により、リレーへのアクセス権を持つnappletなら何でも復号済みのDMを読めてしまう経路が塞がれました。復号には別途ユーザーの許可が、nostr.signEventには署名者の権限が必要になりました。ただし、デスクトップのChromiumレンダラーは引き続きサンドボックス化されていません。ファイルごとの添付暗号化の変更により、NIP-17のプライベートダイレクトメッセージでは添付ファイルごとに新しいAES-GCM鍵とnonceが使われるようになりました。これまでは複数ファイルをまとめて送ると、鍵とnonceの両方が使い回されていました。上流のリスク説明はコード解析に基づくもので、エクスプロイトは再現されていません。また、送信済みのファイルは修復されません。
Marmot Protocol (mdk)
mdkは、MarmotによるMLSベースのNostr向け暗号化メッセージングキットです。今回はテスト専用のナイトリープレリリースのみが公開され、この系列に安定版はありません。11月2日のナイトリーでは、グループ内で新しい送信がキューにある古い送信の完了を待つようになりました。これにより、履歴に追いつこうとしているメンバーにとって暗号化の世代の順序が保たれます。ただし、メッセージが失われないことまでは保証されません。マージ済みでデフォルト無効の失われたWelcomeの修復は、Marmotのハートビート規則に従って動作します。最新のキャリア公開から10分以上経った後の活動が相手のハートビートに現れた時点で、保持していたWelcomeをそのまま再送します。ハートビートの間隔や時計の遅れによって修復が1時間以上遅れることがあり、完了までの時間に上限はありません。デバイスリンクのコマンドは引き続きデフォルト無効のdev-device-groups機能の下にあり、本番環境では無効です。
Napplets - Webパッケージ
サンドボックス化されたNostrアプレット向けのWebパッケージであるNappletsは、core 0.37.0、nap 0.36.0、shim 0.35.0をリリースし、アプレットのアイデンティティをドキュメントごとのMessagePortに結び付けました。検証済みの(dTag, artifactHash)アイデンティティは、作成されたiframeからのhelloで渡されたポートに結び付けられ、NAPの通信はすべてそのポートを通ります。ナビゲーションで読み込まれたドキュメントには権限が一切付与されません。互換性を保つ手段がないため、Windowメッセージングを使い続けているシェルはアップグレードが必要です。その後のソースでは、frame-src ’none’によってプレビューフレームを検証済みのドキュメントに固定しています。
タグ付きリリース
Marble
Cosignのしきい値署名を基盤とするベータ版クライアントMarbleは、0.12.23でリレーをフィードとして閲覧できるようになりました。アドレスを指定してリレーを開くと、まずそのリレー自身の説明が、続いて投稿が新しい順に表示されます。お気に入りのリレーはkind 10012のリストに、リレーグループはkind 30002のセットに保存されます。これらはJumbleと同じリストです。このリリースではリアクションも作り直され、最近使ったリアクションのトレイや自分のカスタム絵文字を使えるようになりました。
Sidecar
ノートやWebハイライトを公開するためのブラウザ拡張機能Sidecarは、1.16.6でリレー上のウォレットのバックアップを削除できるようになりました。確認を経ると、暗号化されたコピーを空のコピーで置き換え、リレーに破棄を依頼します。接続済みのウォレットはそのまま残ります。ただし、リレーがバックアップを確実に消去する保証はありません。v1.16.5以降、SidecarはアップロードするJPEGからEXIF、XMP、IPTCのメタデータを事前に除去しています。さらにアップロード準備の変更により、メタデータを除去できないJPEGはアップロードが阻止されます。
Haps
Hapsは、Nostr鍵のもとでネイティブパッケージを公開・インストールするためのツールです。Haps 0.1.25では、macOS、Linux、Windows向けのバンドルが提供されます。バンドルには、チェックサムで固定されたhtreeヘルパーと、全アーカイブを対象とするSHA256SUMSファイルが含まれます。0.1.24のソースは、Hashtree仕様のドラフトに従ってバージョン付きのHashtree参照を書き込みます。Haps 0.1.23以前では、レガシー移行用のカタログも、更新されたインストーラーまたはCargoビルドも利用できない場合、新しいカタログを読み取れません。
Cosign
Marbleベータを支えるしきい値署名ライブラリCosignは、0.12.9で参加処理の修正を部分的に入れました。スキャンした招待者を通じて参加する場合、その招待者の寄与と、2人以上の異なる送信者からのピースが必要になりました。スマートフォン同士の参加は依然として拘束されていないため、攻撃者は鍵を2つ持っていれば足ります。また、空の復号パーツについては引き続き証明の検証が省略されます。0.12.8では、メンバーリストを書き込むサーバーが、秘密を把握済みのシェアポイントを依然として選べます。スマートフォンにはMarbleのリリースを通じてのみ修正が届きます。
nostream
PostgreSQLを基盤とするTypeScript製リレーのnostreamでは、アウトバウンドのピア同期が3.12.0で追加されました。ワーカーは各ピアのNIP-11リレー情報からスコープを読み取り、NIP-42のクライアント認証で認証したうえで、NIP-77のnegentropy同期で差分を突き合わせます。取得したイベントはID、署名、スコープに照らして検証されます。ピア固有の制限は、設定済みのピアがAUTHを完了した後でのみ適用されます。ソースがリリースされたからといって、実際にどこかのリレーで稼働しているとは限りません。
diVine
短尺ループ動画クライアントのdiVineは、インタラクティブな公開動画ステッカーを搭載したAndroid安定版1.0.27を公開しました。iOSとmacOSで利用できるかどうかは確認されていません。マージ済みのウィンドウより古い範囲の再クエリは、初回の履歴復元が終わった後でのみ実行されます。ギフトラップ、非推奨となったNIP-04の暗号化DM、削除イベントを、リレーごとに100件単位のページで読み込みます。この処理はリレーごとに個別に進みます。一方、既存の履歴取得は1つのカーソルを共有するため、上限に達したリレーでは古い履歴を取りこぼすことがあります。取得範囲には限りがあり、履歴を完全に復元できるわけではありません。
Whistle
モバイル向けのグループ位置情報共有・チャットアプリWhistleは、2.0.1をリリースしました。Marmot protocol v2上で動作する初の公開リリースです。既存のグループは引き継げないため、グループを作り直し、メンバー全員がアップデートする必要があります。npubアイデンティティは保持されます。招待コードやリンクは廃止され、スキャンによる参加に置き換わりました。既知の問題として、オフライン中に送信したチャットメッセージが遅れて届いたり、届かなかったりすることがあります。リリースではMarmotKitが原因とされています。位置情報の共有には影響ありません。
開発中
Quartz
Kotlin Multiplatform向けNostrライブラリのQuartzは、リリース2.14.0で再接続後のライブサブスクリプション再開を提供しました。各リレーは配信済みの最新のcreated_atから再開し、その値はデバイスの時計を上限とします。また、gift-wrapフィルターはバックデートに対応するため、さらに2日分さかのぼります。リレーが障害中に保存した、より古いタイムスタンプのイベントは依然として取りこぼされる可能性があります。マージ済みで未リリースの件数を考慮したnegentropyウィンドウは、任意のNIP-45イベント件数をローカルの件数と比較し、リレーの件数を信頼できないものとして扱います。
Buzz
Buzzは、kind 5および9005の削除を対象のソフトデリートと同一トランザクションでコミットする変更をマージしました。これにより、障害が発生しても、対象が有効なまま削除だけが受理されることはなくなりました。過去に生じた不整合な行は修復されません。デバイス間の既読状態を扱うBuzzのドラフトであるNIP-RSのモバイルプロファイルがマージされ、既読フロンティアをデバイス間で同期するようになりました。read_state_snapshotを持たない古いリレーでは、ローカルの既読処理は引き続き機能しますが、デバイス間同期はフォールバックなしで停止します。
Alby Hub
NIP-47 Nostr Wallet Connectアプリにサービスを提供するセルフカストディ型LightningノードのAlby Hubは、db_migrateまたはPostgres移行の際に各テーブルのIDカウンターを維持するデータベース移行の修正をマージしました。この修正以前は、新しいアプリが削除済みアプリのIDを再利用し、そのトランザクションと分離された残高を引き継ぐ可能性がありました。この修正を含むタグ付きリリースやデプロイは確認されていません。別の通知に関する変更はアプリごとの失敗を分離しますが、失敗した通知は依然として再試行されずに破棄されます。
swift-nostr-client
Swift 6向けNostrライブラリのswift-nostr-clientは、RemoteSignerが最初のコマンドの前にconnectハンドシェイクを実行するようにする破壊的変更をマージしました。新しいconnectsAutomatically設定のデフォルトはtrueで、ハンドシェイクが失敗した場合、コマンドは送信されません。NIP-46のリモート署名シークレットは1回限りしか使えないため、保存済みのクライアント鍵ペアでbunkerセッションを再開するアプリはこれをfalseに設定する必要があります。そうしないと、最初のコマンドで使用済みのシークレットが提示されます。リリースはまだタグ付けされていません。
新規プロジェクト
Trikey
Trikeyは、スマートフォンアプリとガーディアンが暗号化されたNostrリレーメッセージでやり取りする、プレアルファ版で未監査のウォレットです。リリース0.1.10-alphaでは、起動時にアプリがフリーズする問題に対処しました。双方が既読のwrap IDの記録を再起動後も保持するようになり、署名検証や復号の前にそれらをスキップします。リクエストやアラートに付く有効期限のヒントはリレーでの削除を保証するものではなく、公開者によるパフォーマンス測定はノートPC上で行われたものです。
Pacto
従来のMarmotグループとNIP-17 DMを併用するデスクトップ向けコミュニティ組織化アプリのPactoは、送信済みメッセージを1分間編集できる未署名のv0.7.5をリリースしました。DM配信の修正により、プールに信頼済みリレーがない場合は送信が即座に失敗し、入力中インジケーターは信頼済みリレー経由で送られます。未マージのオープンなプルリクエストは、スクワッドイベントと暗号化されたスクワッドドキュメントを追加するものです。
Mialuna
メディア重視のFlutter製NostrクライアントであるMialunaは、バージョン0.19.0のリリース告知を公開し、ユーザー自身のストーリーが期限切れになった際の削除リクエストを追加しました。変更履歴のコミットには、リレーへの削除リクエストとBlossomメディアの削除が記録されており、ローカル鍵のアカウントではこれが自動的に実行され、AmberまたはbunkerのアカウントではユーザーがDelete nowを選択するまで待機します。APKとそのデバイス上での動作は独自に検証されていません。削除リクエストは、リレーやサーバーがデータを消去することを保証できません。
whereAreYou
whereAreYouは、QRコードまたは招待リンクで参加するサークル内で位置情報を共有するAndroidアプリで、座標をAES-256-GCMで暗号化し、公開リレー経由で送信します。メンバー署名のコミットにより、サークル鍵の保持者が別のメンバーになりすまして投稿することを防げるようになり、すべてのデバイスでアップデートが必要です。Base45形式のアイデンティティバックアップについて、作者は、QRコードを撮影した人なら誰でもその8桁のコードをオフラインで推測でき、作者の見積もりではGPU時間で数十分以内に可能だと警告しています。ベンチマークは公開されていません。この情報源はセキュリティ面での保証を示すものではありません。
プロトコルと仕様の取り組み
Buzz、オーナー承認済みエージェントの認証情報ルートを定義
BuzzはNIP-FAのオーナー承認済みエージェント認証情報のドラフトをマージし、NIP-OAのオーナー証明を通じて承認されたエージェント向けに、付与、アサーション、失効のルートを定義しました。付与は、オーナーのセッションとエージェントのNIP-98 HTTP認証の証明を組み合わせたRFC 8693トークン交換によって得られます。失効によりエージェントは切断され、その鍵による再登録も防止されます。NIP-FAはソースにのみマージされたBuzzの仕様であり、本番環境での有効化や相互運用性は主張されていません。
ポスト量子イベント署名に厳密な検証ルールが追加
長期にわたってオープンになっているポスト量子署名の提案に、NostrイベントのためのML-DSA-44の厳密な検証ルールが追加されました。鍵と署名は固定長で、検証者は空のコンテキストを用いて、イベントIDの32バイトの生のダイジェストに対する署名を検証します。フォールバックアルゴリズムは認められません。提案されたアイデンティティにはNIP-19のbech32エンコーディングがなく、新しい署名によってNIP-44の暗号化ペイロードやNIP-59のgift wrapがポスト量子安全になるわけではありません。
署名付き前提条件の提案でイベント置換の消失を防止
Buzzのオープンなプルリクエストは、置換可能イベントおよびアドレス指定可能イベント向けにプロジェクト独自のNIP-CS署名付き前提条件を提案しており、これは公式のNIPではありません。署名付きタグは現在のヘッドIDまたはnoneを指定し、リレーはそれを座標ロックの下でアトミックにチェックします。古い書き込みはfalseのOKまたはHTTP 409で拒否され、何も保存されません。タグのない書き込みは従来どおり動作し、このタグはリレー間でのcompare-and-swapを保証しません。
NIPディープダイブ: NIP-31とNIP-73
クライアントは、ネイティブには理解できないコンテンツによく出会います。NIP-31: Dealing with Unknown Event Kindsは見慣れないイベントkindに読みやすいラベルを付けます。NIP-73: External Content IDsは、書籍やウェブページなどNostrの外にある対象に名前を付けます。この2つの規約は、互いに独立した既存の仕様です(NIP-31本文、NIP-73本文)。
NIP-31は非推奨とされている
現在のNIP-31本文は冒頭に警告を掲げ、「不必要に肥大化している」(“unnecessarily bloated”)としてunrecommendedと位置付けています。ステータス行はdraft、unrecommended、optionalです。この規約を使ったイベントはすでにリレー上に存在します。そのため以下の仕組みは、クライアントがそれらをどう読むかを説明するものであり、新しいプロトコルに推奨するものではありません。
altタグが運ぶもの
仕様では、テキストとして読まれることを意図しないカスタムkindの作成者は、短いプレーンテキストの要約を入れたaltタグを追加します。kind 1中心のクライアントが他のkindを要求することは想定されていません。それでもユーザーはノートからそうしたイベントを参照でき、その際に要約が文脈を補います。ラベルで投票したり、ウォレットのリクエストに応じたりすることはできません。より豊かなやり取りについては、仕様はNIP-89: Recommended Application Handlersを参照するよう示しています。
altはタグなので、NIP-01: Basic protocol flow descriptionのイベントIDと署名の対象に含まれます。有効な署名が認証するのは、作成者が管理する要約です。作成者がそのラベルを公開したことは示しますが、内容の正確さは示さないため、誤解を招く要約もありえます。タグは公開されるため、暗号化されたコンテンツを持つイベントにaltを付けると要約が露出し、コンテンツが隠している内容が明らかになるおそれがあります。これらの性質はNIP-31の規範的な本文ではなく、NIP-01のエンベロープに由来します。また、トップレベルのaltタグはimeta内にある画像説明用のaltフィールドとは別物であり、レンダラーはこれらを個別に解析します。
2つのクライアントによるaltの読み方
noStrudelのコミット7c68acf時点の不明イベントカードでは、最初のaltタグがトリムされてメインのラベルになり、任意でUsing <client>の行が付きます。値がない場合や空白の場合はUnknown event (kind ...)となり、最大3行のコンテンツがそれとは独立して表示されます。同じコンポーネントはNIP-89のハンドラーテンプレートを通じてclientタグを解決します。共有可能なアドレスがあればOpenを、有効なハンドラーリンクが解決されればOpen in <client>を表示します。説明とアプリへの振り分けは別々のコントロールです。
Amethystのコミット553f761時点のUnknownEventDigest.ktは、散文と分類したコンテンツを優先し、それがなければaltにフォールバックします。パーサーは値をトリムし、最初の空白でないaltを採用します。そのため最初のaltが空白のイベントには、noStrudelとは異なるラベルが付く可能性があります。どちらの説明もそれぞれのコミット時点のソースに基づくもので、リリースでの動作を確認したものではありません。
例: altラベル付きの投票
以下のkind 1068の投票はwss://relay.damus.ioから取得したもので、IDとBIP340署名はNIP-01の規則に従って正しく検証されます。汎用レンダラーは2言語で書かれたaltテキストを表示でき、投票に対応したクライアントはoptionタグとpolltypeタグを読み取ります。
{
"kind": 1068,
"id": "7d1df3922833b47645922de43c2edd83e928a0ad083cee592beeee2b262bb743",
"pubkey": "5f6dc275f3f9f8752d010a110c226148a9ba3399c5b63a0f469ee93b1394a53e",
"created_at": 1793626294,
"tags": [
[
"option",
"z25mgp",
"Windows"
],
[
"option",
"ncddq0",
"macOS"
],
[
"option",
"4wta6q",
"Linux"
],
[
"option",
"p9ykti",
"UNIX or FreeBSD/NetBSD/OpenBSD"
],
[
"polltype",
"singlechoice"
],
[
"alt",
"Poll: Which one do you usually use?\n普段どれを使っていますか?"
],
[
"client",
"Ditto",
"31990:781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5:ditto"
]
],
"content": "Which one do you usually use?\n普段どれを使っていますか?",
"sig": "f1378a723c65e83bd3105b593f10767903bde993788cfaf625583466fca21f1ef2be945bb2ca526aa9e898c93d5f1677a2f5ceaf70cb3a59c99f97eb179658fa"
}
NIP-73の対象とファミリー
NIP-73仕様(draft、optional、unrecommendedフラグなし)は、外部の対象をiタグに、その識別子ファミリーをkタグに入れます。これによりクライアントは、特定の1つの対象もファミリー全体もクエリできます。書籍なら["i", "isbn:9780765382030"]と["k", "isbn"]を使い、ウェブページならそのURLを使ってkをwebにします。NIP-73はイベントkindを定義しておらず、これらのタグは他のイベントに含められます。
正規化とURLヒント
照合は文字列が完全に一致するかどうかで決まるため、仕様はファミリーごとに形式を定めています。URLはフラグメントを除いて正規化し、ISBNはハイフンなし、geohashは小文字、ISO 3166コードは大文字です。任意の3番目の配列要素には、その識別子を独自に扱わないクライアント向けのURLヒントを入れます。このヒントは作成者が選ぶもので、アイデンティティの一部ではありません。所有権を証明するものでも、正規のサイトを示すものでもありません。
共有された識別子が証明しないこと
NIP-73では、同一の識別子は会話を結び付けるだけで、それ以外は何も確立しません。署名付きコメントからわかるのはコメント投稿者です。書籍、サイト、ポッドキャストの発行者を認証するものでも、誰がそれらを管理しているかを示すものでもありません。別名、版ごとに異なるISBN、正規化の結果が異なるURLによって、1つの対象が複数のスレッドに分かれることがあります。文脈にも注意が必要です。NIP-22: Commentはルートのスコープに大文字のI/Kを、親に小文字のi/kを使います。NIP-39: External Identities in Profilesはプロフィールのアイデンティティにiを使います。そのため、すべてのiタグがNIP-73の対象とは限りません。
パーサーとレンダラーのコードにおけるNIP-73
rust-nostrのコミット4c8099a時点のnip73.rsは、既知のプレフィックスを型付きのExternalContentIdに解析し、任意のヒントを型付きURLに変換します。3番目のタグ要素を出力するのはヒントがある場合だけです。テストではハイフン付きのISBNがそのまま残ります。パーサーはプレフィックスを取り除いた残りを格納するだけで、仕様の正規化を適用しません。つまり型付きの解析は、正規の文字列を保証しません。チェーンIDはファミリーから分離され、ethereum:100:tx:<hash>はethereum:txに対応付けられます。このリビジョンには、国を表すiso3166のバリアントがありません。
Amethystのコミット553f761時点のDisplayExternalId.ktは、geohashとハッシュタグの識別子をそれぞれ専用の表示に振り分けます。URL識別子にも専用の表示があり、それ以外のサポート対象の型は汎用のスコープチップで表示されます。URLを開くと、そのページに対するアプリ独自のスレッド画面に移動します。プレビューが無効なら、通常の描画ではプレーンなリンクチップが表示され、プレビューは要求されません。プレビューが有効なら、未取得のプレビューを取得する際に対象サイトへアクセスする可能性があります。プレビューをオフにすれば、通常の描画ではそのリクエストを避けられます。どちらのファイルも、リリースでの動作を裏付けるものではありません。
例: ウェブページへのコメント
wss://nos.lolから取得したこのkind 1111のコメントは、同じ公開RedditスレッドのURLをI/Kとi/kの両方のペアに入れています。ファミリーは、NIP-22とNIP-73に従ってwebです。IDと署名は正しく検証されます。これでコメントの作成者は特定できますが、リンク先のページについては何もわかりません。
{
"kind": 1111,
"id": "e4229e38d93fad5d612ae21c401573bf8e87f0462dc2bd653804a252f98a238a",
"pubkey": "ce5b8496c144a9dcd4990df6e265d9b60a21cde2515f3b206dc67b1c343790a9",
"created_at": 1793590574,
"tags": [
[
"I",
"https://www.reddit.com/r/writinghelp/comments/1thd194/what_tools_and_systems_do_you_use_for_tracking"
],
[
"K",
"web"
],
[
"i",
"https://www.reddit.com/r/writinghelp/comments/1thd194/what_tools_and_systems_do_you_use_for_tracking"
],
[
"k",
"web"
],
[
"client",
"LayerTake"
],
[
"L",
"layertake"
],
[
"l",
"pin",
"layertake"
],
[
"anchor",
"{\"type\":\"text\",\"exact\":\"tools and systems\",\"prefix\":\"help • 6mo ago u/TheFarSea What \",\"suffix\":\" do you use for tracking and org\"}"
]
],
"content": "Like this. I'm making this Public but could be just for me or friends/groups.",
"sig": "a979389480d6ca919e4ab7b1ba92e532bf281be45c5df222e17d7d4ca62ef07d52c1004c0ce88620663dfac012b306154cfd73b858225764c701c40e3d9b7dfe"
}