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

Introduce Nostr #4436

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open

Introduce Nostr #4436

wants to merge 21 commits into from

Conversation

ibz
Copy link
Contributor

@ibz ibz commented Dec 10, 2024

This PR adds Nostr support to the Trezor firmware, trezorlib and trezorctl.

Firmware

The Trezor firmware accepts the NostrGetPubkey and NostrSignEvent messages, as defined in common/protob/messages-nostr.proto.

To avoid adding complexity to the firmware, it does not parse the event JSON. The event needs to be parsed beforehand and its individual fields passed via the protobuf message. The return value contains the pubkey, event ID and signature, which need to be added back to the JSON by the caller before forwarding the event to relays.

UI

A simple UI exists to confirm the event being signed on your Trezor's screen. It will be improved later, but for now it just shows all the fields of the event being signed:

Trezor Safe 5

Main screen shows the event kind and the content:

image

From the menu you can view the created_at and the tags:

image
image

Trezor Safe 3

image
image

Trezor Model T

image
image

trezorctl

trezorctl accepts the nostr get-pubkey and nostr sign-event commands which correspond to the protobuf messages described above.

Note: by passing --account (which defaults to 0 - the "first" account) to trezorctl nostr get-pubkey or trezorctl nostr sign-event you can generate a virtually infinite amount of Nostr keypairs from your Trezor - all derived from your seed phrase using the m/44'/1237'/<account>'/0/0 derivation path, as defined by NIP-06.

get-pubkey

Example:

ibz@localhost:~/dev/trezor-firmware$ trezorctl nostr get-pubkey

Output:

pubkey: 2dc0dd32b499f7bd428156334c3431893c18e6016cf1bf00187d76513368dba7

sign-event

trezorctl nostr sign-event will parse the event JSON, send the NostrSignEvent to the Trezor device and re-assemble the event JSON with the id, pubkey and sig obtained from the Trezor.

Example:

ibz@localhost:~/dev/trezor-firmware$ trezorctl nostr sign-event "{\"kind\": 1, \"created_at\": 1733917737, \"tags\": [[\"e\", \"5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36\", \"wss://nostr.example.com\"], [\"p\", \"f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca\"], [\"a\", \"30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd\", \"wss://nostr.example.com\"], [\"alt\", \"reply\"]], \"content\": \"Hello world\"}"

Output:

signed_event: {"kind": 1, "created_at": 1733917737, "tags": [["e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://nostr.example.com"], ["p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca"], ["a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://nostr.example.com"], ["alt", "reply"]], "content": "Hello world", "id": "c2ea8f08093caebf6d73bfe4edbb7e8825bac8eb4308ab634e23161db18b78f8", "pubkey": "2dc0dd32b499f7bd428156334c3431893c18e6016cf1bf00187d76513368dba7", "sig": "4176eaa45730a617f7393ba53df009278504911b3b30d35e125cb1c0f0d1e92102f468705eaed7d3ae5bb3bb4267d9411f0184a7ab6c451110553c2fca97b39c"}

@ibz ibz self-assigned this Dec 10, 2024
@ibz ibz linked an issue Dec 10, 2024 that may be closed by this pull request
Copy link

github-actions bot commented Dec 10, 2024

core UI changes device test click test persistence test
T2T1 Model T test(screens) main(screens) test(screens) main(screens) test(screens) main(screens)
T3B1 Safe 3 test(screens) main(screens) test(screens) main(screens) test(screens) main(screens)
T3T1 Safe 5 test(screens) main(screens) test(screens) main(screens) test(screens) main(screens)
All main(screens)

@ibz ibz force-pushed the ibz/nostr branch 3 times, most recently from de9d4e4 to bd13fed Compare December 11, 2024 11:44
@ibz ibz changed the title feat(core): add nostr event signing Add Nostr Dec 11, 2024
@ibz ibz force-pushed the ibz/nostr branch 2 times, most recently from 9ef8c6a to feabb03 Compare December 11, 2024 14:40
@prusnak
Copy link
Member

prusnak commented Dec 11, 2024

We should check that the address_n provided to NostrGetPubkey and NostrSignEvent matches m/44'/1237'/<account>'/0/0 and fail if it does not.

Also I prefer changing the CLI interface from using --address to using --account where default account is 0 (see below).

Otherwise great work! 🎉

common/protob/messages.proto Outdated Show resolved Hide resolved
python/src/trezorlib/cli/nostr.py Outdated Show resolved Hide resolved
python/src/trezorlib/cli/nostr.py Outdated Show resolved Hide resolved
python/src/trezorlib/cli/nostr.py Outdated Show resolved Hide resolved
python/src/trezorlib/cli/nostr.py Outdated Show resolved Hide resolved
tests/device_tests/nostr/test_nostr.py Outdated Show resolved Hide resolved
tests/device_tests/nostr/test_nostr.py Outdated Show resolved Hide resolved
tests/device_tests/nostr/test_nostr.py Outdated Show resolved Hide resolved
@@ -91,6 +91,9 @@ class MsgWithAddressScriptType(Protocol):
# SLIP-44 coin type for all Testnet coins
SLIP44_TESTNET = const(1)

# SLIP-44 "coin type" for Nostr
SLIP44_NOSTR = const(1237)
Copy link
Member

Choose a reason for hiding this comment

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

It seems this is not used anywhere.

content = msg.content

node = keychain.derive(address_n)
pk = node.public_key()[-32:]
Copy link
Member

@prusnak prusnak Dec 12, 2024

Choose a reason for hiding this comment

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

Can we do this? Don't we care about the public key parity? Or does nostr use x-only keys?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can we do this? Don't we care about the public key parity? Or does nostr use x-only keys?

From what I understand, Nostr uses X-only keys, indeed.

Copy link
Member

Choose a reason for hiding this comment

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

But it is not guaranteed that keychain.derive returns an x-only key, right?
I think we need to convert the key if a non x-only key is returned.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Isn't conversion to x-only basically dropping the first byte if the key is 33 bytes long? Which is the same as taking the last 32 bytes always?

@ibz ibz force-pushed the ibz/nostr branch 3 times, most recently from d66d0af to 8bc5409 Compare December 18, 2024 10:19
@ibz
Copy link
Contributor Author

ibz commented Dec 18, 2024

We should check that the address_n provided to NostrGetPubkey and NostrSignEvent matches m/44'/1237'/<account>'/0/0 and fail if it does not.

Yup! dc1a257#diff-3dd8aea4110592e07746c9d3233736246c7083e001c9d29db7c1b03f7423f93bR19

@ibz ibz marked this pull request as ready for review December 18, 2024 11:24
@ibz ibz requested a review from matejcik as a code owner December 18, 2024 11:24
@ibz ibz requested a review from martykan December 19, 2024 10:29
Copy link
Contributor

@obrusvit obrusvit left a comment

Choose a reason for hiding this comment

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

Some points for discussion.

common/protob/messages-nostr.proto Outdated Show resolved Hide resolved
common/protob/messages-nostr.proto Outdated Show resolved Hide resolved
core/src/apps/nostr/sign_event.py Show resolved Hide resolved
core/src/apps/nostr/sign_event.py Outdated Show resolved Hide resolved
@ibz ibz changed the title Add Nostr Introduce Nostr Dec 19, 2024
core/.changelog.d/4160.added Outdated Show resolved Hide resolved
core/src/apps/nostr/__init__.py Show resolved Hide resolved
core/src/apps/nostr/get_pubkey.py Show resolved Hide resolved
core/src/trezor/wire/__init__.py Outdated Show resolved Hide resolved
python/src/trezorlib/mapping.py Show resolved Hide resolved


@pytest.mark.setup_client(mnemonic=LEAD_MONKEY_MNEMONIC)
def test_sign_event_lead_monkey(client: Client):
Copy link
Contributor

Choose a reason for hiding this comment

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

there's a better way to write these tests:

LEAD_MONKEY = pytest.mark.setup_client(...)
WHAT_BLEAK = pytest.mark.setup_client(...)

VECTORS = (  # pubkey_hex
    pytest.param("cafecacebeef", mark=LEAD_MONKEY),
    pytest.param(...)
)

@pytest.mark.parametrize("pubkey_hex", VECTORS)
def test_get_pubkey(client, pubkey_hex):
   ...

dtto for sign_event except i'd prefer also hardcoding the signature bytes, in addition to verifying

Copy link
Contributor Author

Choose a reason for hiding this comment

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

7f493ea#diff-1a61ba923388ca946c730e9cec2ca549a97e355ff7a53673617057d39f95f0f5R44

The signature cannot be hardcoded because every time an event is signed it gets a different signature. That is the reason to use verify rather than comparing to a known signature.

Copy link
Contributor

Choose a reason for hiding this comment

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

The signature cannot be hardcoded because every time an event is signed it gets a different signature.

what is the source of this nondeterminism?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a feature of ECDSA - a random value is included. This is to prevent deriving of the private key if you have many signatures that you can analyze.

Copy link
Contributor

Choose a reason for hiding this comment

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

that's what RFC 6979 is for -- i was under the impression that this is built into ecdsa signing on library level...?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right... I think that in theory we should be allowed to use the deterministic signatures, but from what I noticed Nostr signatures are usually of the non-deterministic kind. Do you think it should be otherwise? I guess the library can do both, if needed, but isn't the non-deterministic version better?

Copy link
Contributor

Choose a reason for hiding this comment

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

isn't the non-deterministic version better?

i'm not aware of any argument why it would be.
in our particular case, there's another issue on top: if the nonce is not deterministic, attacks like Dark Skippy can use the signatures to smuggle out secret bits without the user being able to detect it. (with rfc6979 at least you can verify the signature if you also have the seed on the host side)

so please change to deterministic signatures, unless you have a spec explicitly saying you shouldn't do that. (and if so let's see it and talk to R&D about this)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think RFC 6979 would work actually because we need Schnorr signatures. So I used BIP-340 to sign and I got deterministic signatures now. 45d4ee0

tests/device_tests/nostr/test_nostr.py Outdated Show resolved Hide resolved
python/src/trezorlib/nostr.py Outdated Show resolved Hide resolved


@expect(messages.NostrPubkey)
def get_pubkey(
Copy link
Contributor

Choose a reason for hiding this comment

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

this function should return just bytes of the public key.

perhaps it's a good idea to rebase on #4490

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually I don't think it should be bytes but rather the hex-encoded bytes, which is human readable and the (most) standard way to encode Nostr keys. Another encoding commonly used is NPUB - and I should probably add an option to return that encoding if desired, but at the most fundamental level it is the hex encoding, which is what all code dealing with Nostr is using. bytes seems strange, because it looks bad, can't even be copy-pasted, and is of no use...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

bytes seems strange, because it looks bad, can't even be copy-pasted, and is of no use...

I mean you're deep inside a library, nobody is supposed to be copy-pasting the return value of this.
If there is a canonical representation of the pubkey as readable string, the NostrPubkey message should send that from Trezor. If there isn't (you apparently have two choices, npub / hex), and you're returning raw bytes, the library function should return raw bytes.

Consumers of trezorlib.nostr.get_pubkey are expected to do their own human readable conversion if they need to; specifically, if encoding to npub is simple, feel free to implement that in cli/nostr.py and add an option to the user-exposed function

Copy link
Contributor

Choose a reason for hiding this comment

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

(fwiw if you return bytes from a click-exposed function, there's some sort of conversion machinery that will automagically print it as hex for the user)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If there is a canonical representation of the pubkey as readable string, the NostrPubkey message should send that from Trezor

hex is the standard way to represent keys in code interacting with Nostr events. npub is a more "human friendly" standard, but it always has to be converted back to hex after being received from user input. So I think hex is the most appropriate standard to use here.

Copy link
Contributor

Choose a reason for hiding this comment

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

okay, if that's the case, please modify the Trezor side to return the hex string

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. It's all str now: a95dc04 !

python/src/trezorlib/cli/nostr.py Outdated Show resolved Hide resolved
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.

import json
from typing import TYPE_CHECKING, Dict
Copy link
Contributor

Choose a reason for hiding this comment

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

instead:

from __future__ import annotations

# ...

import typing as t

# ...

if t.TYPE_CHECKING:
# ...

# ...
def foo() -> dict[str, str]:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

).pubkey.hex()


@expect(messages.NostrEventSignature)
Copy link
Contributor

Choose a reason for hiding this comment

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

per #4490 please drop the @expect and replace with client.call(..., expect=NostrEventSignature)
dtto for get_pubkey

Copy link
Contributor Author

Choose a reason for hiding this comment

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

def test_sign_event(client, pubkey_hex):
response = nostr.sign_event(
client,
messages.NostrSignEvent(
Copy link
Contributor

Choose a reason for hiding this comment

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

would it make sense to move this construction to top level directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: 🏃‍♀️ In progress
Development

Successfully merging this pull request may close these issues.

Add app/feature to sign Nostr messages using m/44'/1237'/* keys.
4 participants