nips/xx.md
2023-02-26 17:53:08 +05:45

8.4 KiB

NIP-XX

Nostr-Specific Private Keys from Deterministic Wallet Signatures (Sign-In-With-X)

draft optional author:0xc0de4c0ffee author:sshmatrix

Abstract

This specification provides an optional method for Nostr clients and NIP-07 providers and coin wallet providers to generate deterministic private keys from Chain Agnostic Sign-in-With-X(CAIP-122) signature. Nostr-specific private key is derived from HKDF-SHA-256 using NIP02/NIP05 names, (CAIP-02: Blockchain ID Specification), CAIP-10: Account ID Specification identifiers, and deterministic signature from connected coin-wallet as inputs.

Terminology

a) Username

Username can be either of the following:

  • petname is a NIP-02 compatible name,
  • petname@example.com is a NIP-05 identifier,
  • example.com is NIP-05 identifier _@example.com,
  • sub.example.com is NIP-05 identifier _@sub.example.com.

b) Password

Password is an optional string value used in HKDF salt,

let password = "horse staple battery"
//...
let salt = await sha256(`${caip10}:${username}:${password?password:""}:${signature.slice(68)}`);

c) Message

Deterministic message to be signed by coin-wallet provider.

let message = `Login to Nostr as ${username}\n\nImportant: Please verify the integrity and authenticity of your Nostr client before signing this message.\n${info}`

d) Signature

RFC6979 compatible deterministic signature from coin-wallet provider.

let signature = wallet.signMessage(message);

e) Blockchain and Address Identifier

Chain Agnostic CAIP-02: Blockchain ID Specification and CAIP-10: Account ID Specification are used to generate blockchain and address identifiers.

let caip02 =
      `eip155:<evm_chain_id>` || 
      `cosmos:<hub_id_name>` ||
      `bip122:<16 bytes genesis/fork hash>`;

let caip10 = `${caip02}:<checksum_address>`;

f) HKDF

HKDF-SHA-256 is used to derive the 42 bytes long hash key: hkdf(sha256, inputKey, salt, info, dkLen = 42)

  • Input key is SHA-256 hash of signature bytes.

    let inputKey = await sha256(hexToBytes(signature.slice(2)));
    
  • Info is CAIP10 and NIP02/NIP05 identifier string formatted as :

    let info = `${caip10}:${username}`;
    
  • Salt is SHA-256 hash of the info, optional password and last 32 bytes of signature string formatted as :

    let salt = await sha256(`${info}:${password?password:""}:${signature.slice(68)}`);
    

    where, signature.slice(68) last 32 bytes of deterministic signature.

  • Derived Key Length dkLen is set to 42.

    let dkLen = 42;
    

    FIPS 186/4 B.4.1 requires hashkey length to be >= n + 8. Where n = 32 bytes length of final secp256k1 private key, such that 42 >= 32 + 8.

  • hashToPrivateKey function is FIPS 186-4 B.4.1 implementation to convert hashkey derived using HKDF to valid secp256k1 private keys. This function is implemented in JavaScript library @noble/secp256k1 as hashToPrivateKey().

    let hashKey = hkdf(sha256, inputKey, salt, info, dkLen=42);
    let privKey = secp256k1.utils.hashToPrivateKey(hashKey);
    let pubKey = secp256k1.schnorr.getPublicKey(privKey);
    

Implementation Requirements

  • Connected Ethereum wallet signer MUST be EIP191 and RFC6979 compatible.
  • The message MUST be string formatted as Login to Nostr as ${username}\n\nImportant: Please verify the integrity and authenticity of your Nostr client before signing this message.\n${info}.
  • HKDF input key MUST be generated as the SHA-256 hash of 65 bytes signature.
  • HKDF salt MUST be generated as SHA-256 hash of string ${info}:${username}:${password?password:""}:${signature.slice(68)}.
  • HKDF derived key length MUST be 42.
  • HKDF info MUST be string formatted as ${CAIP_10}:${address}:${username}.

JS Example

import * as secp256k1 from '@noble/secp256k1'
import {hkdf} from '@noble/hashes/hkdf'
import {sha256} from '@noble/hashes/sha256'
import {queryProfile} from './nip05'
import {getPublicKey} from './keys'
import {ProfilePointer} from './nip19'


// const wallet = connected ethereum wallet with ethers.js
let username = "me@example.com"
let chainId = wallet.getChainId(); // get chainid from connected wallet
let address = wallet.getAddress(); // get address from wallet
let caip10 = `eip155:${chainId}:${address}`;
let message = `Login to Nostr as ${username}\n\nImportant: Please verify the integrity and authenticity of your Nostr client before signing this message.\n${caip10}`
let signature = wallet.signMessage(message); // request signature from wallet
let password = "horse staple battery"


/**
 * 
 * @param username nip02/nip05 identifier
 * @param caip10 CAIP identifier for the blockchain account
 * @param sig Deterministic signature from X-wallet provider
 * @param password Optional password
 * @returns Deterministic private key as hex string
 */
export async function privateKeyFromX(
  username: string,
  caip10: string,
  sig: string,
  password: string | undefined
): Promise < string > {
  if (sig.length < 64)
    throw new Error("Signature too short");
  let inputKey = await sha256(secp256k1.utils.hexToBytes(sig.toLowerCase().startsWith("0x") ? sig.slice(2) : sig))
  let info = `${caip10}:${username}`
  let salt = await sha256(`${info}:${password?password:""}:${sig.slice(-64)}`)
  let hashKey = await hkdf(sha256, inputKey, salt, info, 42)
  return secp256k1.utils.bytesToHex(secp256k1.utils.hashToPrivateKey(hashKey))
}

/**
 * 
 * @param username nip02/nip05 identifier
 * @param caip10 CAIP identifier for the blockchain account
 * @param sig Deterministic signature from X-wallet provider
 * @param password Optional password
 * @returns 
 */
export async function signInWithX(
  username: string,
  caip10: string,
  sig: string,
  password: string | undefined
): Promise < {
  petname: string,
  profile: ProfilePointer | null,
  privkey: string
} > {
  let profile = null
  let petname = username
  if (username.includes(".")) {
    try {
      profile = await queryProfile(username)
    } catch (e) {
      console.log(e)
      throw new Error("Nostr Profile Not Found")
    }
    if(profile == null){
      throw new Error("Nostr Profile Not Found")
    } 
    petname = (username.split("@").length == 2) ? username.split("@")[0] : username.split(".")[0]
  }
  let privkey = await privateKeyFromX(username, caip10, sig, password)
  let pubkey = getPublicKey(privkey)
  if (profile?.pubkey && pubkey !== profile.pubkey) {
    throw new Error("Invalid Signature/Password")
  }
  return {
    petname,
    profile,
    privkey
  }
}

Implementations

  1. Nostr tools : https://github.com/nbd-wtf/nostr-tools/pull/132 (PR)
  2. Nostr client: (WIP)

Security Considerations

  • Users should always verify the integrity and authenticity of the Nostr client before signing the message.
  • Users should ensure that they only input their Nostr Username and Password in trusted and secure clients.

References: