8.5 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 NIP-02/NIP-05 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 theinfo
, 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 finalsecp256k1
private key, such that42 >= 32 + 8
. -
hashToPrivateKey
function is FIPS 186-4 B.4.1 implementation to convert hashkey derived using HKDF to validsecp256k1
private keys. This function is implemented in JavaScript library@noble/secp256k1
ashashToPrivateKey()
.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
- Nostr Tools : Sign-In-With-X (Pull Request #132)
- Nostr Client: Dostr Client
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.