Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NIP-111: Nostr-specific Private Keys from Deterministic Wallet Signatures (Sign-in-With-X) #268

Open
wants to merge 16 commits into
base: master
Choose a base branch
from

Conversation

0xc0de4c0ffee
Copy link

@0xc0de4c0ffee 0xc0de4c0ffee commented Feb 18, 2023

Note: NIP-111 is open for reviews.

NIP-111: Deterministic Private Key Generation from Ethereum Wallet Signature

Example code:

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

// const wallet = connected ethereum wallet with ethers.js
let username = "virgil.eth.limo"
let chainId = wallet.getChainId(); // get ChainID from connected wallet
let address = wallet.getAddress(); // get Address from wallet
let caip10 = `eip155:${chainId}:${address}`;
let message = `Log into Nostr client as '${username}'\n\nIMPORTANT: Please verify the integrity and authenticity of connected Nostr client before signing this message\n\nSIGNED BY: ${caip10}`
let signature = wallet.signMessage(message); // request Signature from wallet
let password = "horse staple battery"

/**
 *
 * @param username NIP-02/NIP-05 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))
}

** updated url & example to latest specs under review

@barkyq
Copy link
Contributor

barkyq commented Feb 18, 2023

Seems a bit risky that the web app would have the private key, unless this got implemented in, e.g., Metamask. But if we are going down that road, perhaps Metamask could just implement NIP-07, and use this method under the hood.

In any case, do we really want to open the door to putting various key generation methods into their own NIPs? What about a NIP to turn a BTC private key into a nostr private key, and a NIP to turn a Monero private key into a nostr private key, and a NIP to turn a NANO private key into a nostr private key, etc...

On a further thought, perhaps each app can just implement their own method of: <insert private key format> to <nostr private key> and then offer an "export" function which allows the nostr private key to be exported. Perhaps using something like #133

@barkyq
Copy link
Contributor

barkyq commented Feb 18, 2023

See also #258 for the inverse problem.

@fiatjaf
Copy link
Member

fiatjaf commented Feb 19, 2023

I agree the only way for this to work would be if this was done on Metamask directly, and Metamask implemented NIP-07.

@0xc0de4c0ffee
Copy link
Author

Thanks for feedback @barkyq @fiatjaf 🙏

Seems a bit risky that the web app would have the private key, unless this got implemented in, e.g., metamask. But if we are going down that road, perhaps metamask could just implement NIP-07, and use this method under the hood.

It's less $ risker compared to reusing same ETH keys in Nostr clients. This is optional specs for web/mobile Nostr clients and window.nostr/window.ethereum providers to generate multiple ID/keys with deterministic signature, username and optional password.

In any case, do we really want to open the door to putting various key generation methods into their own NIPs? What about a NIP to turn a BTC private key into a nostr private key.....

We can update this as Chain Agnostic Improvement Proposals (CAIP) approach to support ALL coins/chains compatible with RFC6979 like deterministic signature.

Example using BIP44 (SLIP44) coin type and EIP155 chain ID.

let info1 = `bip44:${coin_type}:${username}:${address}`;
let info2 = `eip155:${chain_id}:${username}:${address}`;
let info3 = `cosmos:${hub_id}:${username}:${address}`; // not sure about this, have to lookup
//... 
let salt1 = await sha256(`bip44:${coin_type}:${username}:${password?password:""}:${last_32bytes_ofSignatureHex}`);
let salt2 = await sha256(`eip155:${chain_id}:${username}:${password?password:""}:${last_32bytes_ofSignatureHex}`);
//...

@0xc0de4c0ffee 0xc0de4c0ffee changed the title NIP-XX: Deterministic Private Key Generation from Ethereum Wallet Signature NIP-XX: Nostr-specific Private Key from Deterministic Wallet Signature (Sign-in-With-X) Feb 21, 2023
@0xc0de4c0ffee
Copy link
Author

Login with Nostr #154 is our inverse-brah 😆

We're working on different BIP47 "like" stealth address generation too, more details at #258 (comment)

I agree the only way for this to work would be if this was done on Metamask directly, and Metamask implemented NIP-07.

There's "Metamask Flask" plugin for Nostr, Schnorr Snap. MM-Flask is experimental wallet not ready for average users, it will take more time for major coin-wallets to implement Nostr+schnorr feature. Can't wait for major wallet provider to implement that, & hope they don't reuse their coin-keys for Nostr again.

This specification is for Nostr + X compatible app/clients to sign Nostr stuffs with window.nostr and X-chain stuffs with window.ethereum like wallet providers. CAIP-122: Sign in with X feature is used to generate app-specific deterministic keys for Nostr app/clients and compatible NIP07 providers.

Updating chain and account identifiers to as defined in CAIP-02:Blockchain ID Specification and CAIP-10:Account ID Specification.

Note : We were exploring SLIP/BIP44 coin_type as chain identifier but CAIPs are using that for assets type inside BIP122, so we're using that BIP122:.. leaving BIP44/:coin_type as assets type for future. 🙏

Example: CAIP-10, CAIP-02 with NIP02/NIP05 user identifiers.

# Bitcoin mainnet (BIP122)
bip122:<first 16 bytes of genesis/fork hash>:address:username
bip122:000000000019d6689c085ae165831e93:address:username

# EVM chains  (EIP155)
eip155:<chain_id>:address:username
eip155:1:address:username

# Cosmos Hub (Tendermint + Cosmos SDK/Binance)
cosmos:<hub/ChainName>:address:username
cosmos:cosmoshub-2:address:username
cosmos:Binance-Chain-Tigris:address:username
cosmos:iov-mainnet:address:username

...

# Dummy max length (8+1+32 = 41 chars/bytes) +":address:username"
chainstd:8c3444cf8970a9e41a706fab93e7a6c4

Still WIP, Example

let nostr = widow.nostr || nostrTools ;
let coinWallet = await connectEthers(window.ethereum);

let username = 'me@example.com';
let password = 'opt#password';
let pubkey = await nostr.nipxx.newIdentity(username, password="", chainType="eip155", coinWallet="");
// wrapped as nipxx.loginWithX(...) function. 

// registration: set new nip02 and nip05 records for this pubkey.
// login: verify if there's matching nip02 and nip05 records.

@sshmatrix
Copy link

sshmatrix commented Mar 21, 2023

Dear @fiatjaf @barkyq,

We would like to report major updates to our NIP-XX proposal and again seek your feedback on the following:

  1. We have added significant amount of explanatory text to the NIP-XX proposal including some useful diagrams and flowcharts. For instance, the Schnorr key generation algorithm from ECDSA/Ethereum keys is outlined below:

  1. The codebase for the underlying NIP-XX implementation is also complete and we have requested second review of that as well here.

  2. The client for the NIP-XX implementation is also ready at Dostr: Ethereum-flavoured Nostr; please feel free to give it a swing.

  • We'd like to request NIP identifier 60 or 111 for NIP-XX, i.e. NIP-60 or NIP-111.

We'd love to hear back from you with your thoughts and comments on this PR!

Thank you!

@fiatjaf
Copy link
Member

fiatjaf commented Mar 21, 2023

Is this method of generating keys a standard among altcoins?

@sshmatrix
Copy link

sshmatrix commented Mar 21, 2023

Is this method of generating keys a standard among altcoins?

No, I wouldn't say so. But it is standard and well-known in general Cryptography. I think @0xc0de4c0ffee might be more aware of previous implementations of it, if any, in cryptosphere. I hope he will comment here on it.

@0xc0de4c0ffee
Copy link
Author

0xc0de4c0ffee commented Mar 21, 2023

Is this method of generating keys a standard among altcoins?

It's fully chain agnostic sign-in-with-x CAIP122 used to generate deterministic keys.

There are few Ethereum dapps using straight sha256(half_signature) to derive app-specific private keys from signatures. We're adding chain and address identifiers (CAIP02+CAIP10) format, NIP05/NIP02 user info (+optional password) with HKDF to generate deterministic keys.

Example from umbra.js stealth payment codes.

https://github.com/ScopeLift/umbra-protocol/blob/master/umbra-js/src/classes/Umbra.ts#L549

Screenshot 2023-03-21 at 21 50 22

We're signing stuffs from both window.nostr & window.ethereum.. for basic e2ee msgs and stealth payments, so we don't want to reuse or mix ETH keys with Nostr keys.

** err updated typo

@0xc0de4c0ffee 0xc0de4c0ffee changed the title NIP-XX: Nostr-specific Private Key from Deterministic Wallet Signature (Sign-in-With-X) NIP-111: Nostr-specific Private Keys from Deterministic Wallet Signatures (Sign-in-With-X) Mar 30, 2023
@sshmatrix
Copy link

Hi @fiatjaf,

Hope you are doing good. Dostr client is nearing its official launch (Mar 31). In reality, the client is already online at:

for earlybirds. There won't be any drastic changes to the client from now on other than cosmetic fine-tuning of the UI/UX.

With respect to the NIP:

  • We have changed the numbering of the NIP in PR to NIP-111 as its our "favourite" number.
  • More importantly, we have finalised the format of the message to eth-Sign to generate Nostr keys.

This NIP is in its final boss form from our side 😊

@digi-monkey
Copy link

digi-monkey commented Apr 4, 2023

@sshmatrix are you still refactoring the signing message? I like this idea and I am implementing at digi-monkey/flycat-web#82, I will hope the message can be finalized and not changed again since I am going to merge the PR and rollout this feature

111.md Outdated Show resolved Hide resolved
- Connected Ethereum wallet Signer **MUST** be EIP-191 and RFC-6979 compatible.
- The `message` **MUST** be string formatted as
```
`Log into Nostr client as '${username}'\n\nIMPORTANT: Please verify the integrity and authenticity of connected Nostr client before signing this message\n\nSIGNED BY: ${caip10}`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused, is this "inspired by" Sign-In With Ethereum/CAIP-122 or is it meant to be conformant to one or both of those two specs? If the latter was intended, the message should probably conform the CAIP-122/EIP-4361 ABNF, which could include the entire message (with \ns removed) in the statement field, use the salt value for nonce, and info as first entry in the resources array (not really sure what to do with inputKey, as I'm unclear on its exact function on a cursory read-through). If it's not too late to go in that direction, I think it might have some benefits, such as being displayed to metamask users in the familiar, locked-down "Sign-In With Ethereum" modal rather than as a generic "personal_sign" modal... which only displays when presented with a personal_sign message matching the ABNF :D
image

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And apologies for not reading this sooner! Exciting work, in any case, supportive of the general direction and thankful to see CAIP-10 being used as the "export format" for addresses of signers 💪

Copy link

@sshmatrix sshmatrix Apr 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @bumblefudge! We intended to conform to CAIP-122 as much as possible but we had to strip all the variable quantities from the message format to retain deterministic nature of key derivation. In some sense, this implementation is not really "Sign-In" but more of "Ephemeral KeyGen and then Sign-In", and hence the necessary deviation from the standard. Our implementation requires that the verifiable signature is static.

Copy link

@bumblefudge bumblefudge Apr 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the problem with an ABNF-based syntax is that you can't deviate and still get interop with other SIWX libraries or take advantage of SIWX support built into Metamask! Can I suggest hard-coding conformant dummy values into the spec and template rather than removing the key/value pairs that you don't need, so that the message can still conform to the ABNF and get displayed to the users as a SIWX message? I would note that ephemeral keygen is already baked into the SIWX standard, and is being used for that exact usecase by most implementers (the generated ephemeral public key is usually included as a value in the Resources array, although this isn't really mentioned explicitly in the specification itself!)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused, is this "inspired by" Sign-In With Ethereum/CAIP-122 or is it meant to be conformant to one or both of those two specs? If the latter was intended, the message should probably conform the CAIP-122/EIP-4361 ABNF, which could include the entire message (with \n removed) in the statement field, use the salt value for nonce, and info as first entry in the resources array (not really sure what to do with inputKey, as I'm unclear on its exact function on a cursory read-through).

@bumblefudge 🙏
Should we remove CAIP122 ref as we can't pass full strict ABNF? or req all as new CAIP?

We actually started as basic signature request before upgrading to full CAIP122/ERC4361 format but had to remove all extradata for deterministic keys as they are too strict to fit in all.. So it's now back to generic "personal_sign" modal for chain agnostic "sign-in-with-x" in Nostr context, internally it's using deterministic signature from wallet to generate "app specific deterministic keys" across all Nostr clients.

We could fill in all ABNF required formats with deterministic/fixed values but we can't pass URI validation in wallets, and "${service} wants you to sign in with <chain> account: \n<addr>\n...must:have" text message for ALL web2+3 D/Apps is too strict for our deterministic keygen, web3+dapps are not supposed to have single URLs/apps as entry point..

"Important: .....\n\n" is supposed to be "Warning! ..." statement, it's not key: value. All other extradata is wrapped in SIGNED BY : ${CAIP10}, & our msg is simple 3 blocks <title>\n\n<statement>\n\n<key:value>...

I would note that ephemeral keygen is already baked into the SIWX standard, and is being used for that exact usecase by most implementers (the generated ephemeral public key is usually included as a value in the Resources array, although this isn't really mentioned explicitly in the specification itself!)

Can you add some ref codes/links for that?

Copy link

@sshmatrix sshmatrix Apr 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could fill in all ABNF required formats with deterministic/fixed values but we can't pass URI validation in wallets, and "${service} wants you to sign in with <chain> account: \n<addr>\n...must:have" text message for ALL web2+3 D/Apps is too strict for our deterministic keygen, web3+dapps are not supposed to have single URLs/apps as entry point..

URI is definitely a bigger issue and it hadn't come to my mind before. While timestamp and nonce can be replaced with placeholders, URI is used by Metamask for its community-audited safe dApp list. This is problematic for dApps or services without a unique entry point. CAIP122 is too strict for all use-cases and pretty much a death sentence for the deterministic use-case.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't the uri be:
https://github.com/nostr-protocol/nips/blob/master/111.md
? It doesn't need to be on the domain

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't the uri be: https://github.com/nostr-protocol/nips/blob/master/111.md ? It doesn't need to be on the domain

That's not a bad idea honestly but it will make users highly suspicious and wary of a service with 1/1/1970 in timestamp and a GitHub link in URI. I personally won't sign and subscribe to such a service at first glance. I believe there is room for a new CAIP detailing a separate signature format standard for deterministic use-cases.

Copy link
Author

@0xc0de4c0ffee 0xc0de4c0ffee Apr 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't the uri be: https://github.com/nostr-protocol/nips/blob/master/111.md ? It doesn't need to be on the domain

When we connect MM at https://app.dostr.eth.limo it'll check and add "WARNING" if connected domain isn't matching domain URL. It's good feature for web2 & web3 apps with single point of entry but for our use case we're adding extra user info & pw with static signature request as basic security.

It all works as plaintext "sign in with xyz on Nostr" signature request so we're missing all SWIx interface/features.

  • it's better if we explain why we're doing this, our keygen design is inspired by Umbra cash & old Whisper(ssh) newKeypair/ newIdentity function. We use window.ethereum to sign eth tx/permits then use deterministic keys in window.nostr to send that over Nostr relays for off-chain features like stealth payments, DeFi, NFT markets, alt-mempool for AA, xyz services, bundlers/bots, games...

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally I think if we don't need timestamp, adding a placeholder for it is not a good idea.

@sshmatrix
Copy link

sshmatrix commented Apr 6, 2023

@sshmatrix are you still refactoring the signing message? I like this idea and I am implementing at digi-monkey/flycat-web#82, I will hope the message can be finalized and not changed again since I am going to merge the PR and rollout this feature

Hi @digi-monkey, the message contained in the present draft is "final". It won't be changed, we hope. We have adopted the same on Dostr client and frozen it. There was a typo in the draft however and we have fixed it. It didn't affect the implementation apart from a mislabeling of caip10 variable in the statement.

@bumblefudge Implementing CAIP-122 as is won't work in this case, since we need the final key output to be deterministic. Adding the nonce and timestamp will add variable salt to the key derivation function resulting in variable keys. Deterministic key generation requires deterministic message and this led to the current choice of message to sign.

@bumblefudge
Copy link

timestamp can be zero (i.e. midnight 1/1/1970) and nonce can be any string. in fact, CASA member @skgbafa demo'd a usage of SIWE for deterministic key generation at a CASA event, so I see no reason a more conformant version couldn't be used for that use-case.

apologies that I missed this notification and wasn't able to make these suggestions before final status! if SIWX conformance is of use, maybe it's worth a superceding spec or an extension spec that puts the same inputs into a conformant message value, so that individual implementations can choose to implement that variant instead if taking advantage of wallet special-handling of SIWX messages is worth the extra work to them?

@sshmatrix
Copy link

sshmatrix commented Apr 6, 2023

apologies that I missed this notification and wasn't able to make these suggestions before final status! if SIWX conformance is of use, maybe it's worth a superceding spec or an extension spec that puts the same inputs into a conformant message value, so that individual implementations can choose to implement that variant instead if taking advantage of wallet special-handling of SIWX messages is worth the extra work to them?

I was thinking precisely the same thing! Perhaps it is worth an extension spec that provides deterministic usage of SIWx. We have CAIP-111 in the works (pretty much ready) and will open a PR soon. I will have a discussion with my co-dev about message formatting and report back. Thanks for your comments! Much appreciated

@digi-monkey You might want to hold your horses there 🤭

@bumblefudge
Copy link

bumblefudge commented Apr 6, 2023

Oh! Actually I meant an extension NIP, but hey, maybe an extension-CAIP* might make just as much sense. I would also add that if the NIPs elaborate enough complexity and cohesion that there are nostr-enabled wallets, signing methods, etc, it might some day make sense to create a Nostr namespace, where you can define a profile for CAIP-10 (syntax of a URN scheme for CAIP-10 encodings of nostr accounts/pubkeys), CAIP-122 (if a Nostr app which held a private key wanted to "sign in with nostr" to another service), etc etc.

* = note, it wouldn't be CAIP-111, unfortunately, because CAIPs are numbered to the PR# that creates them, so if you opened a PR today it would be CAIP-222 --- which is a funny coincidence, hurry up before someone else nabs that number!

@sshmatrix
Copy link

sshmatrix commented Apr 10, 2023

Oh! Actually I meant an extension NIP, but hey, maybe an extension-CAIP* might make just as much sense...

* = note, it wouldn't be CAIP-111, unfortunately, because CAIPs are numbered to the PR# that...

Hi @bumblefudge, linking the answer to your questions here: #268 (comment). GitHub notifications are unreliable on multiple threads

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants