nips/95.md
2024-07-29 14:03:50 -04:00

7.0 KiB

NIP-95 - Nostr Frost Remote Signing

draft optional

Rationale

Private keys should be exposed to as few systems - apps, operating systems, devices - as possible, ideally none to prevent single points of failure.

This NIP describes an implementation of a Flexible Round Optimized Schnorr Threshold (FROST) signature scheme for a Nostr client and one or many remote signers. The remote signer could be, for example, a webserver or a hardware device dedicated to signing Nostr events, while the client is a normal Nostr client.

Terminology

  • Client keypair: A local public and private key-pair which is a valid share of the FROST signature scheme and used to coordinate the signature aggregation and communicate with the remote signer(s).
  • Remote keypair: This is a public and private key-pair which represents a share of the FROST signature scheme.
  • Root user pubkey: The public key that the user wants to sign as. The aggregated signature produces a valid signature for this pubkey.

All pubkeys specified in this NIP are in hex format.

Set up

The inital set up for all keys involved

Root key

The Root key is the key-pair that is perminant and identifies the user. (i.e. when someone wants to follow this user, they would follow this Root Pubkey) The Root key MUST publish a kind 11100 event which specifies the signature scheme that should be used to create a valid signature for this pubkey.

The list of tags are as follows:

  • p (required, repeated) 32-bytes hex pubkey of all FROST key shares involved in the signature scheme, index of FROST share, optional signature request URL
  • threshold (required) the number of signature shares required to create a valid aggregated signature for the root pubkey

Kind 11100 Event

{
  "kind": 11100,
  "pubkey": "fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
  "content": "",
  "tags": [
    [
      "p",
      "eff37350d839ce3707332348af4549a96051bd695d3223af4aabce4993531d86",
      "1",
      "<optional url of remote signer>"
    ],
    [
      "p",
      "1ce5a164bc3482a63efd010af17ea3585b4ae8fd35dc1d1cd0bd3037ba9acb51",
      "2"
    ],
    ["threshold", "2"]
  ]
}

The flow

  1. User creates FROST key shares for their root key-pair.
  2. User publishes kind 11100 event with the data about the key shares.
  3. User signs into Nostr client with the Client key (a share of the Root key). Client treats the user as if they are signed in with the Root key. There are multiple ways for the user to communicate to the client that they wish to be treated as the Root key rather than the Client key, perhaps searching for the kind 11100 event or having the user specify the Root pubkey they wish to be signed in as in the case that the client key is a FROST share for multiple Root keys (i.e. the client key is tagged in multiple kind 11100 events).
  4. When the user needs to produce a signature for the Root key, the client will find the Root key's kind 11100 event and coordinate the nonce communication and signature aggregations steps necessary to produce a valid Root key signature
  5. The client publishes the event from the Root pubkey with the created aggregated signature.

Signature aggregation

In order to the client to produce a valid signature, it must aggregate nonce commitments from all partial signers involved, distribute those nonce commitments to all partial signers, collect partial signatures from all partial signers involved, and finally perform a signature aggregation step.

This process scales in complexity as more partial signers are involved. In the simple case where threshold = 2, this coordination is rather simple:

Threshold = 2

  1. Client generates the event id to be signed and creates a nonce commitment for the local Client key.
  2. Based on the Root key's kind 11100 event, the client requests a partial signature from the remote signing key by passing the remote signer the event id to sign, the Root pubkey being signed for, the index of the Client key, and the nonce commitment that was just created.
  3. The Remote signer receives the request and first generates a nonce commitment for it's own remote key, then by combining the 2 nonce comitments, the remote signer can now create a partial signature share for the remote key.
  4. The Remote signer returns it's partial signature and nonce commitment to the client.
  5. The client combines the nonce commitment from the remote signer with it's own to produce the same shared nonce commitment that the remote signer had generated, and then creates the partial client signature.
  6. Lastely, the 2 partial signatures are combined to create a valid signature for the Root key.

Cases where threshold > 2 are more complex as multiple rounds of communication are needed to share the nonce commitments from all signers, but the core flow is unchanged

Threshold > 2

  1. Client generates the event id to be signed and creates a nonce commitment for the local Client key.
  2. Based on the Root key's kind 11100 event, the client requests a nonce commitments for all remote signers needed for the signature. (Note: this nonce commitment step could also occur prior to the signature request and the client could cache nonce commitments for remote signers before hand).
  3. Remote signers receive nonce commitment request and return a nonce commitment.
  4. The Client creates aggregates the nonce commitments to create it's own partial signature.
  5. The Client relays all nonce commitments back to the remote signers so they can each produce their partial signature. The remote signer then returns its partial signature back to the client.
  6. Lastely, the client aggregates all partial signatures to create a valid signature for the Root key.

Communication between client and remote signer

Direct connection initiated by the client

In this case, the client uses the remote signer url specificed in the kind 11100 tags array as the 4th element for the remote signer's pubkey.

Signature request

The client sends a POST request to the specified url with body

{
  "pubkey": "<remote signer pubkey being requested to sign>",
  "event": "<event to sign JSON>",
  "nonceCommitments": [["<nonce commitment>", "partial signer index"]]
}

Signature request response

{
  "nonceCommitment": "<nonce commitment>",
  "partialSignature": "<partial signature"
}

Nonce commitment request

The client sends a GET request to the specified url with with search param of the pubkey being requested and an optional amount value https://remote-signer.com/?pubkey=<remote pubkey>&amount=1

Nonce commitment request response

["<nonce commitment>"]

Important notes

  • It is essential that the remote signers keep track of the nonce commitments that they have generated and used to ensure that a malicious actor could not preform a nonce reuse attack.

Coming soon...