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 15 commits into
base: main
Choose a base branch
from
Open

Introduce Nostr #4436

wants to merge 15 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.

@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 Show resolved Hide resolved
@ibz ibz changed the title Add Nostr Introduce Nostr Dec 19, 2024
@@ -0,0 +1 @@
Add Nostr support.
Copy link
Contributor

Choose a reason for hiding this comment

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

pls mark it somehow so that we know that it's debug-only

@@ -0,0 +1,5 @@
from apps.common.paths import PATTERN_BIP44
Copy link
Contributor

Choose a reason for hiding this comment

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

first entry here should be something like

if not __debug__:
    from trezor.utils import halt
    halt("disabled in production mode")

see apps/benchmark/__init__.py



@auto_keychain(__name__)
async def get_pubkey(msg: NostrGetPubkey, keychain: Keychain) -> NostrPubkey:
Copy link
Contributor

Choose a reason for hiding this comment

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

aside: there's no showing the Nostr key on screen, will that be added in the future? if not, this is useless because you can get the same pubkey via GetPublicKey

@@ -95,6 +95,7 @@ async def handle_session(iface: WireInterface) -> None:
except Exception as exc:
# Log and ignore. The session handler can only exit explicitly in the
# following finally block.
do_not_restart = True
Copy link
Contributor

Choose a reason for hiding this comment

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

please remove this change.

(if there's a reason you want this, please make a separate issue and a separate PR, this is nothing to do with Nostr)

@@ -62,7 +62,9 @@ def encode(self, msg: protobuf.MessageType) -> Tuple[int, bytes]:
"""
wire_type = self.class_to_type_override.get(type(msg), msg.MESSAGE_WIRE_TYPE)
if wire_type is None:
raise ValueError("Cannot encode class without wire type")
raise ValueError(
Copy link
Contributor

Choose a reason for hiding this comment

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

make a separate commit for this one



@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

response = nostr.sign_event(
client,
event=json.dumps(
{"created_at": created_at, "kind": kind, "tags": tags, "content": content}
Copy link
Contributor

Choose a reason for hiding this comment

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

instead, make a static event definition with a fixed time etc. on top level. we want the tests to be deterministic if we at all can

n: "Address",
event: AnyStr,
) -> "MessageType":
event_json = json.loads(event)
Copy link
Contributor

Choose a reason for hiding this comment

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

if you want a structured dict in, pass it in as a dict, not as a string. Let the caller do their own json.loads if they want to. for bonus points, use TypedDict.
(wondering if we should instead introduce a dataclass for the parameters, or perhaps use the protobuf message directly? given that we're returning the corresponding protobuf instance too)



@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


address_n = tools.parse_path(PATH_TEMPLATE.format(account))

res = nostr.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.

just return nostr.get_pubkey, which should be changed to return bytes per the other comment

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

Successfully merging this pull request may close these issues.

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