From 16f134c0a2563e728506762e60001d5da8ba3185 Mon Sep 17 00:00:00 2001 From: 0xc0de4c0ffee <43116881+0xc0de4c0ffee@users.noreply.github.com> Date: Sun, 26 Feb 2023 17:53:08 +0545 Subject: [PATCH] update identifiers to caip10 --- xx.md | 169 ++++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 128 insertions(+), 41 deletions(-) diff --git a/xx.md b/xx.md index 602534a8..da37d9df 100644 --- a/xx.md +++ b/xx.md @@ -1,66 +1,82 @@ -# NIP-XX -Nostr-Specific Deterministic Private Key Generation from Ethereum Wallet Signature +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 to generate deterministic private keys from Ethereum wallet signatures. The private keys are derived using HMAC Key Derivation Function (HKDF) applied to SHA-256 of ECDSA signatures native to Ethereum wallets (EIP-191), allowing Nostr to interact with Ethereum infrastructure and vice-versa. +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)](https://github.com/ChainAgnostic/CAIPs/pull/122) signature. Nostr-specific private key is derived from HKDF-SHA-256 using NIP02/NIP05 names, [(CAIP-02: Blockchain ID Specification)](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md), [CAIP-10: Account ID Specification](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md) identifiers, and deterministic signature from connected coin-wallet as inputs. ## Terminology + ### a) Username Username can be either of the following: -`petname` or `petname@domain.eth.limo` or `domain.eth.limo` or `sub.domain.eth.limo`, where - - `petname` is a NIP-02 compatible name, -- `petname@domain.eth.limo` is a NIP-05 compatible name, -- `domain.eth.limo` is NIP-05 equivalent of `_@domain.eth.limo`, -- `sub.domain.eth.limo` is NIP-05 equivalent of `_@sub.domain.eth.limo`. -> a) `sub@domain.eth.limo` and `sub.domain.eth.limo` are NOT equivalent as their signatures will be different - -> b) `petname` can be the same as `domain` +- `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, ```js let password = "horse staple battery" -let salt = await sha256(`eip155:${chainId}:${username}:${password?password:""}:${signature.slice(68)}`); +//... +let salt = await sha256(`${caip10}:${username}:${password?password:""}:${signature.slice(68)}`); ``` ### c) Message -Message is text on screen that should warn the users to not sign messages indiscriminately, +Deterministic message to be signed by coin-wallet provider. ```js 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 -Signature is the deterministic signature from connected Ethereum wallet. Ethereum signatures `(v,r,s)` are 65 bytes long, i.e. `bytes1(v) + bytes32(r) + bytes32(s)`, +RFC6979 compatible deterministic signature from coin-wallet provider. ```js -let signature = wallet.signMessage(message); // implements ethereum-native ECDSA signatures in format (v,r,s) +let signature = wallet.signMessage(message); ``` -### e) HKDF (HMAC Key Derivation Function) + +### e) Blockchain and Address Identifier +Chain Agnostic [CAIP-02: Blockchain ID Specification](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md) and [CAIP-10: Account ID Specification](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md) are used to generate blockchain and address identifiers. +``` +let caip02 = + `eip155:` || + `cosmos:` || + `bip122:<16 bytes genesis/fork hash>`; + +let caip10 = `${caip02}:`; +``` + +### 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. ```js let inputKey = await sha256(hexToBytes(signature.slice(2))); ``` -- `Salt` is SHA-256 hash of the following identifier string: +- `Info` is CAIP10 and NIP02/NIP05 identifier string formatted as : ```js - let salt = await sha256(`eip155:${chainId}:${username}:${password?password:""}:${signature.slice(68)}`); + let info = `${caip10}:${username}`; ``` - where, `signature.slice(68)` is hex `s` value of Ethereum signature, i.e. the last 32 bytes. -- `Info` is a string with the following format: +- `Salt` is SHA-256 hash of the `info`, optional password and last 32 bytes of signature string formatted as : ```js - let info = `eip155:${chainId}:${username}:${address}`; + 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. ```js let dkLen = 42; ``` - FIPS 186/4 B.4.1 requires hash length to be `>= n + 8`, where n is the length of final private key, such that `42 >= 32 + 8`. + 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()`. -- `hashToPrivateKey` function is FIPS 186-4 B.4.1 implementation to convert hash keys derived using HKDF to valid `secp256k1` private keys. This function is implemented in JavaScript library `@noble/secp256k1` as `hashToPrivateKey()`. ```js let hashKey = hkdf(sha256, inputKey, salt, info, dkLen=42); let privKey = secp256k1.utils.hashToPrivateKey(hashKey); @@ -72,36 +88,103 @@ HKDF-SHA-256 is used to derive the 42 bytes long hash key: `hkdf(sha256, inputKe - 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 `eip155:${chainID}:${username}:${password?password:""}:${signature.slice(68)}`. +- 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 `eip155:${chainId}:${username}:${address}`. +- HKDF info MUST be string formatted as `${CAIP_10}:${address}:${username}`. ## JS Example ```js -const secp256k1 = require('@noble/secp256k1'); -const {hexToBytes, bytesToHex} = require('@noble/hashes/utils'); -const {hkdf} = require('@noble/hashes/hkdf'); -const {sha256} = require('@noble/hashes/sha256'); +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@domain.eth.limo" +let username = "me@example.com" let chainId = wallet.getChainId(); // get chainid from connected wallet let address = wallet.getAddress(); // get address from wallet -let info = `eip155:${chainId}:${username}:${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${info}` +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" -let inputKey = await sha256(hexToBytes(signature.slice(2))); //skip "0x" -let salt = await sha256(`eip155:${chainId}:${username}:${password?password:""}:${signature.slice(68)}`); -let dkLen = 42; -let hashKey = await hkdf(sha256, inputKey, salt, info, dkLen); -let privKey = secp256k1.utils.hashToPrivateKey(hashKey); -let pubKey = secp256k1.schnorr.getPublicKey(privKey); + +/** + * + * @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. @@ -110,6 +193,10 @@ let pubKey = secp256k1.schnorr.getPublicKey(privKey); ## References: - [RFC6979: Deterministic Usage of the DSA and ECDSA](https://datatracker.ietf.org/doc/html/rfc6979) - [RFC5869: HKDF (HMAC-based Extract-and-Expand Key Derivation Function)](https://datatracker.ietf.org/doc/html/rfc5869) +- [CAIP-02: Blockchain ID Specification](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md) +- [CAIP-10: Account ID Specification](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md) +- [CAIP-122: Sign-in-With-X)](https://github.com/ChainAgnostic/CAIPs/pull/122) + - [Digital Signature Standard (DSS), FIPS 186-4 B.4.1](https://csrc.nist.gov/publications/detail/fips/186/4/final) - [BIP340: Schnorr Signature Standard](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) - [ERC191: Signed Data Standard](https://eips.ethereum.org/EIPS/eip-191) @@ -117,4 +204,4 @@ let pubKey = secp256k1.schnorr.getPublicKey(privKey); - [NIP-02: Contact List and Petnames](https://github.com/nostr-protocol/nips/blob/master/02.md) - [NIP-05: Mapping Nostr keys to DNS-based internet identifiers](https://github.com/nostr-protocol/nips/blob/master/05.md) - [@noble/hashes](https://github.com/paulmillr/noble-hashes) -- [@noble/secp256k1](https://github.com/paulmillr/noble-secp256k1) +- [@noble/secp256k1](https://github.com/paulmillr/noble-secp256k1) \ No newline at end of file