跳至主要內容

NIP-26: Delegated Event Signing

允許其他密鑰代表你簽署事件的委託機制

概述

NIP-26 定義了一種委託簽名機制,允許一個密鑰(委託者)授權另一個密鑰(被委託者) 代表其簽署事件。這對於需要多設備使用、團隊管理或自動化發布等場景非常有用。

委託令牌

委託通過一個特殊的 delegation 標籤實現,包含委託者公鑰、 條件字串和簽名。

標籤格式

["delegation", "<委託者公鑰>", "<條件字串>", "<簽名>"]
欄位 說明
委託者公鑰 授權委託的原始公鑰(hex 格式)
條件字串 定義委託的限制條件
簽名 委託者對條件的 Schnorr 簽名

條件字串

條件字串定義了委託的限制,使用 & 連接多個條件:

條件 說明 範例
kind=<n> 限制為特定事件類型 kind=1
created_at<<n> 事件時間戳必須小於 n created_at<1700000000
created_at><n> 事件時間戳必須大於 n created_at>1600000000

條件範例

# 只允許發布 kind 1(短文)
kind=1

# 只允許在特定時間範圍內發布
created_at>1640000000&created_at<1700000000

# 只允許發布 kind 1,且有時間限制
kind=1&created_at>1640000000&created_at<1700000000

簽名生成

委託簽名是對以下字串的 Schnorr 簽名:

nostr:delegation:<被委託者公鑰>:<條件字串>

範例

委託事件

{
  "id": "...",
  "pubkey": "<被委託者公鑰>",
  "created_at": 1650000000,
  "kind": 1,
  "tags": [
    [
      "delegation",
      "<委託者公鑰>",
      "kind=1&created_at>1640000000&created_at<1700000000",
      "<委託簽名>"
    ]
  ],
  "content": "這是一條由被委託者代發的訊息",
  "sig": "<被委託者簽名>"
}

TypeScript 實作

建立委託令牌

import { schnorr } from '@noble/curves/secp256k1';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
import { sha256 } from '@noble/hashes/sha256';

interface DelegationToken {
  delegatorPubkey: string;
  conditions: string;
  signature: string;
}

interface DelegationConditions {
  kind?: number;
  since?: number;
  until?: number;
}

function buildConditionsString(conditions: DelegationConditions): string {
  const parts: string[] = [];

  if (conditions.kind !== undefined) {
    parts.push(`kind=${conditions.kind}`);
  }
  if (conditions.since !== undefined) {
    parts.push(`created_at>${conditions.since}`);
  }
  if (conditions.until !== undefined) {
    parts.push(`created_at<${conditions.until}`);
  }

  return parts.join('&');
}

function createDelegationToken(
  delegatorSecretKey: Uint8Array,
  delegateePubkey: string,
  conditions: DelegationConditions
): DelegationToken {
  const delegatorPubkey = bytesToHex(
    schnorr.getPublicKey(delegatorSecretKey)
  );

  const conditionsString = buildConditionsString(conditions);

  // 建立要簽名的字串
  const message = `nostr:delegation:${delegateePubkey}:${conditionsString}`;
  const messageHash = sha256(new TextEncoder().encode(message));

  // 簽名
  const signature = bytesToHex(
    schnorr.sign(messageHash, delegatorSecretKey)
  );

  return {
    delegatorPubkey,
    conditions: conditionsString,
    signature,
  };
}

// 使用範例
const token = createDelegationToken(
  delegatorSecretKey,
  '被委託者公鑰hex',
  {
    kind: 1,
    since: Math.floor(Date.now() / 1000),
    until: Math.floor(Date.now() / 1000) + 86400 * 30, // 30 天
  }
);

驗證委託

function verifyDelegation(
  event: any,
  delegationTag: string[]
): boolean {
  if (delegationTag[0] !== 'delegation' || delegationTag.length !== 4) {
    return false;
  }

  const [, delegatorPubkey, conditions, signature] = delegationTag;

  // 1. 驗證條件
  if (!validateConditions(event, conditions)) {
    return false;
  }

  // 2. 驗證簽名
  const message = `nostr:delegation:${event.pubkey}:${conditions}`;
  const messageHash = sha256(new TextEncoder().encode(message));

  try {
    return schnorr.verify(
      hexToBytes(signature),
      messageHash,
      hexToBytes(delegatorPubkey)
    );
  } catch {
    return false;
  }
}

function validateConditions(event: any, conditions: string): boolean {
  const parts = conditions.split('&');

  for (const part of parts) {
    if (part.startsWith('kind=')) {
      const kind = parseInt(part.slice(5));
      if (event.kind !== kind) return false;
    } else if (part.startsWith('created_at>')) {
      const since = parseInt(part.slice(11));
      if (event.created_at <= since) return false;
    } else if (part.startsWith('created_at<')) {
      const until = parseInt(part.slice(11));
      if (event.created_at >= until) return false;
    }
  }

  return true;
}

// 使用範例
const delegationTag = event.tags.find((t: string[]) => t[0] === 'delegation');
if (delegationTag && verifyDelegation(event, delegationTag)) {
  console.log('委託有效,事件代表:', delegationTag[1]);
}

使用委託發布事件

import { finalizeEvent } from 'nostr-tools';

function createDelegatedEvent(
  content: string,
  delegateeSecretKey: Uint8Array,
  delegationToken: DelegationToken
) {
  const event = {
    kind: 1,
    content,
    tags: [
      [
        'delegation',
        delegationToken.delegatorPubkey,
        delegationToken.conditions,
        delegationToken.signature,
      ],
    ],
    created_at: Math.floor(Date.now() / 1000),
  };

  return finalizeEvent(event, delegateeSecretKey);
}

// 使用範例
const delegatedEvent = createDelegatedEvent(
  '這是委託發布的訊息',
  delegateeSecretKey,
  token
);

使用場景

多設備管理

  • 主密鑰保存在冷錢包
  • 為每個設備創建獨立的委託密鑰
  • 設定時間限制和類型限制

團隊發布

  • 品牌帳號由主密鑰控制
  • 團隊成員獲得有限委託權限
  • 可隨時撤銷(通過時間條件過期)

自動化發布

  • 機器人使用委託密鑰
  • 限制只能發布特定類型的事件
  • 降低主密鑰洩露風險

安全考量

  • 時間限制:始終設定 until 條件,避免永久委託
  • 類型限制:限制可發布的事件類型
  • 密鑰隔離:委託密鑰應獨立於主密鑰
  • 監控審計:定期檢查委託發布的內容

中繼器支援

支援 NIP-26 的中繼器應該:

  • 驗證委託簽名的有效性
  • 驗證事件是否符合條件限制
  • 在查詢時,將委託事件關聯到委託者

限制與注意事項

  • 委託無法撤銷,只能等待過期
  • 不是所有客戶端都支援顯示委託資訊
  • 某些中繼器可能不驗證委託
  • NIP-01:基本協議 - 事件簽名
  • NIP-46:遠端簽名 - 另一種密鑰管理方式
  • NIP-07:瀏覽器擴充 - 簽名介面

參考資源

已複製連結
已複製到剪貼簿