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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions common/protob/messages-nostr.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
syntax = "proto2";
package hw.trezor.messages.nostr;

// Sugar for easier handling in Java
option java_package = "com.satoshilabs.trezor.lib.protobuf";
option java_outer_classname = "TrezorMessageNostr";

import "options.proto";

/**
* Request: Ask the device for the Nostr public key
* @start
* @next NostrPubkey
*/
message NostrGetPubkey {
repeated uint32 address_n = 1; // used to derive the key
}

/**
* Response: Nostr pubkey
* @end
*/
message NostrPubkey {
required bytes pubkey = 1; // pubkey derived from the seed
}

/**
* @embed
*/
message NostrTag {
// Nostr tags consist of at least one string (the key)
// followed by an arbitrary number of strings,
// the first of which (if present) is called "value".
// See NIP-01: https://github.com/nostr-protocol/nips/blob/master/01.md#tags
required string key = 1;
optional string value = 2;
repeated string extra = 3;
}

/**
* Request: Ask device to sign an event
* @start
* @next NostrEventSignature
* @next Failure
*/
message NostrSignEvent {
repeated uint32 address_n = 1; // used to derive the key

// Nostr event fields, except the ones that are calculated by the signer
// See NIP-01: https://github.com/nostr-protocol/nips/blob/master/01.md

required uint32 created_at = 2; // Event created_at: unix timestamp in seconds
required uint32 kind = 3; // Event kind: integer between 0 and 65535
repeated NostrTag tags = 4; // Event tags
required string content = 5; // Event content: arbitrary string
}

/**
* Response: Computed event ID and signature
* @end
*/
message NostrEventSignature {
required bytes pubkey = 1; // pubkey used to sign the event
required bytes id = 2; // ID of the event
required bytes signature = 3; // signature of the event
}
7 changes: 6 additions & 1 deletion common/protob/messages.proto
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ enum MessageType {
MessageType_NextU2FCounter = 81 [(wire_out) = true];

// Deprecated messages, kept for protobuf compatibility.
// Both are marked wire_out so that we don't need to implement incoming handler for legacy
MessageType_Deprecated_PassphraseStateRequest = 77 [deprecated = true];
MessageType_Deprecated_PassphraseStateAck = 78 [deprecated = true];

Expand Down Expand Up @@ -324,6 +323,12 @@ enum MessageType {
// THP
reserved 1000 to 1099; // See messages-thp.proto

// Nostr
MessageType_NostrGetPubkey = 2001 [(wire_in) = true];
MessageType_NostrPubkey = 2002 [(wire_out) = true];
MessageType_NostrSignEvent = 2003 [(wire_in) = true];
MessageType_NostrEventSignature = 2004 [(wire_out) = true];

// Benchmark
MessageType_BenchmarkListNames = 9100 [(bitcoin_only) = true];
MessageType_BenchmarkNames = 9101 [(bitcoin_only) = true];
Expand Down
1 change: 1 addition & 0 deletions core/.changelog.d/4160.added
Original file line number Diff line number Diff line change
@@ -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

5 changes: 5 additions & 0 deletions core/SConscript.firmware
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,11 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nem/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/NEM*.py'))

if PYOPT == '0':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nostr/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nostr/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Nostr*.py'))

SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/ripple/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Ripple*.py'))

Expand Down
5 changes: 5 additions & 0 deletions core/SConscript.unix
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,11 @@ if FROZEN:
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nem/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/NEM*.py'))

if PYOPT == '0':
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nostr/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nostr/*/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Nostr*.py'))

SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/ripple/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Ripple*.py'))

Expand Down
1 change: 1 addition & 0 deletions core/embed/rust/librust_qstr.h
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ static void _librust_qstrs(void) {
MP_QSTR_modify_fee__transaction_fee;
MP_QSTR_more_info_callback;
MP_QSTR_multiple_pages_texts;
MP_QSTR_nostr__event_kind_template;
MP_QSTR_notification;
MP_QSTR_notification_level;
MP_QSTR_page_count;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ALTCOIN_PREFIXES = (
"fido",
"monero",
"nem",
"nostr",
"ripple",
"solana",
"stellar",
Expand Down
1 change: 1 addition & 0 deletions core/mocks/trezortranslate_keys.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,7 @@ class TR:
nem__under_namespace: str = "under namespace"
nem__unencrypted: str = "Unencrypted:"
nem__unknown_mosaic: str = "Unknown mosaic!"
nostr__event_kind_template: str = "Event kind: {0}"
passphrase__access_wallet: str = "Access passphrase wallet?"
passphrase__always_on_device: str = "Always enter your passphrase on Trezor?"
passphrase__continue_with_empty_passphrase: str = "Continue with empty passphrase?"
Expand Down
6 changes: 6 additions & 0 deletions core/src/all_modules.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions core/src/apps/nostr/__init__.py
Original file line number Diff line number Diff line change
@@ -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


CURVE = "secp256k1"
SLIP44_ID = 1237
PATTERN = PATTERN_BIP44
24 changes: 24 additions & 0 deletions core/src/apps/nostr/get_pubkey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import TYPE_CHECKING

from apps.common.keychain import auto_keychain

if TYPE_CHECKING:
from trezor.messages import NostrGetPubkey, NostrPubkey

from apps.common.keychain import Keychain


@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

from trezor.messages import NostrPubkey

from apps.common import paths

address_n = msg.address_n

await paths.validate_path(keychain, address_n)

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

return NostrPubkey(pubkey=pk)
60 changes: 60 additions & 0 deletions core/src/apps/nostr/sign_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from typing import TYPE_CHECKING

from apps.common.keychain import auto_keychain

if TYPE_CHECKING:
from trezor.messages import NostrEventSignature, NostrSignEvent

from apps.common.keychain import Keychain


@auto_keychain(__name__)
async def sign_event(msg: NostrSignEvent, keychain: Keychain) -> NostrEventSignature:
from ubinascii import hexlify

from trezor import TR
from trezor.crypto.curve import secp256k1
from trezor.crypto.hashlib import sha256
from trezor.messages import NostrEventSignature
from trezor.ui.layouts import confirm_value

from apps.common import paths

address_n = msg.address_n
created_at = msg.created_at
kind = msg.kind
tags = [[t.key] + ([t.value] if t.value else []) + t.extra for t in msg.tags]
content = msg.content

await paths.validate_path(keychain, address_n)

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.

sk = node.private_key()

title = TR.nostr__event_kind_template.format(kind)

# confirm_value on TR only accepts one single info item
obrusvit marked this conversation as resolved.
Show resolved Hide resolved
# which is why we concatenate all of them here.
# This is not great, but it gets the job done for now.
tags_str = f"created_at: {created_at}"
for t in tags:
tags_str += f"\n\n{t[0]}: " + (f" {' '.join(t[1:])}" if len(t) > 1 else "")

await confirm_value(
title, content, "", "nostr_sign_event", info_items=[("", tags_str)]
)

# The event ID is obtained by serializing the event in a specific way:
# "[0,pubkey,created_at,kind,tags,content]"
# See NIP-01: https://github.com/nostr-protocol/nips/blob/master/01.md
serialized_tags = ",".join(
["[" + ",".join(f'"{t}"' for t in tag) + "]" for tag in tags]
)
serialized_event = f'[0,"{hexlify(pk).decode()}",{created_at},{kind},[{serialized_tags}],"{content}"]'
obrusvit marked this conversation as resolved.
Show resolved Hide resolved
event_id = sha256(serialized_event).digest()

# The event signature is basically the signature of the event ID computed above
signature = secp256k1.sign(sk, event_id)[-64:]

return NostrEventSignature(pubkey=pk, id=event_id, signature=signature)
10 changes: 10 additions & 0 deletions core/src/apps/workflow_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ def _find_message_handler_module(msg_type: int) -> str:
if msg_type == MessageType.GetFirmwareHash:
return "apps.misc.get_firmware_hash"

# When promoting the Nostr app to production-level
# and removing the "if" guard don't forget to also remove
# the corresponding guards (PYOPT == '0') in Sconscript.*
if __debug__:
# nostr
if msg_type == MessageType.NostrGetPubkey:
return "apps.nostr.get_pubkey"
if msg_type == MessageType.NostrSignEvent:
return "apps.nostr.sign_event"

if not utils.BITCOIN_ONLY:
if msg_type == MessageType.SetU2FCounter:
return "apps.management.set_u2f_counter"
Expand Down
4 changes: 4 additions & 0 deletions core/src/trezor/enums/MessageType.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions core/src/trezor/enums/__init__.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

86 changes: 86 additions & 0 deletions core/src/trezor/messages.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading