Skip to content

Commit

Permalink
feat(core): add nostr
Browse files Browse the repository at this point in the history
  • Loading branch information
ibz committed Dec 11, 2024
1 parent 748a19a commit feabb03
Show file tree
Hide file tree
Showing 24 changed files with 1,723 additions and 252 deletions.
51 changes: 51 additions & 0 deletions common/protob/messages-nostr.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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";

option (include_in_bitcoin_only) = true;

/**
* Request: Ask the device for the Nostr public key
* @start
* @next NostrMessageSignature
*/
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
}

/**
* Request: Ask device to sign event
* @start
* @next NostrEventSignature
* @next Failure
*/
message NostrSignEvent {
repeated uint32 address_n = 1; // used to derive the key
optional uint32 created_at = 2;
optional uint32 kind = 3;
repeated string tags = 4;
optional string content = 5;
}

/**
* 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 @@ -77,7 +77,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 @@ -105,6 +104,12 @@ enum MessageType {
MessageType_OwnershipProof = 50 [(bitcoin_only) = true, (wire_out) = true];
MessageType_AuthorizeCoinJoin = 51 [(bitcoin_only) = true, (wire_in) = true];

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

// Crypto
MessageType_CipherKeyValue = 23 [(bitcoin_only) = true, (wire_in) = true];
MessageType_CipheredKeyValue = 48 [(bitcoin_only) = true, (wire_out) = 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.
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.

3 changes: 3 additions & 0 deletions core/src/apps/bitcoin/keychain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


def validate_path_against_script_type(
coin: coininfo.CoinInfo,
Expand Down
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

CURVE = "secp256k1"
SLIP44_ID = 1237
PATTERN = PATTERN_BIP44
20 changes: 20 additions & 0 deletions core/src/apps/nostr/get_pubkey.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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:
from trezor.messages import NostrPubkey

address_n = msg.address_n

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

return NostrPubkey(pubkey=pk)
35 changes: 35 additions & 0 deletions core/src/apps/nostr/sign_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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.crypto.curve import secp256k1
from trezor.crypto.hashlib import sha256
from trezor.messages import NostrEventSignature

address_n = msg.address_n
created_at = msg.created_at
kind = msg.kind
tags = msg.tags
content = msg.content

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

serialized_event = (
f'[0,"{hexlify(pk).decode()}",{created_at},{kind},{tags},"{content}"]'
)
event_id = sha256(serialized_event).digest()
signature = secp256k1.sign(sk, event_id)[-64:]

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

# 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.

68 changes: 68 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.

1 change: 1 addition & 0 deletions core/src/trezor/wire/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
if __debug__:
log.exception(__name__, exc)
finally:
Expand Down
3 changes: 2 additions & 1 deletion legacy/firmware/protob/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple SdPro
Solana StellarClaimClaimableBalanceOp \
ChangeLanguage TranslationDataRequest TranslationDataAck \
SetBrightness DebugLinkOptigaSetSecMax \
BenchmarkListNames BenchmarkRun BenchmarkNames BenchmarkResult
BenchmarkListNames BenchmarkRun BenchmarkNames BenchmarkResult \
NostrGetPubkey NostrPubkey NostrSignEvent NostrEventSignature

ifeq ($(BITCOIN_ONLY), 1)
SKIPPED_MESSAGES += Ethereum NEM Stellar
Expand Down
82 changes: 82 additions & 0 deletions python/src/trezorlib/cli/nostr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# This file is part of the Trezor project.
#
# Copyright (C) 2012-2025 SatoshiLabs and contributors
#
# This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the License along with this library.
# If not, see <https://www.gnu.org/licenses/lgpl-3.0.html>.

import json
from typing import TYPE_CHECKING, Dict

import click

from .. import nostr, tools
from . import with_client

if TYPE_CHECKING:
from ..client import TrezorClient


@click.group(name="nostr")
def cli() -> None:
pass


@cli.command()
@click.option("-n", "--address", default="m/44'/1237'/0'/0/0", help="BIP-32 path")
@with_client
def get_pubkey(
client: "TrezorClient",
address: str,
) -> Dict[str, str]:
"""Derive the pubkey from the seed."""

address_n = tools.parse_path(address)

res = nostr.get_pubkey(
client,
address_n,
)

return {
"pubkey": res.pubkey.hex(),
}


@cli.command()
@click.option("-n", "--address", default="m/44'/1237'/0'/0/0", help="BIP-32 path")
@click.argument("event")
@with_client
def sign_event(
client: "TrezorClient",
address: str,
event: str,
) -> Dict[str, str]:
"""Sign an event using address of given path."""

event_json = json.loads(event)

address_n = tools.parse_path(address)

res = nostr.sign_event(
client,
address_n,
event,
)

event_json["id"] = res.id.hex()
event_json["pubkey"] = res.pubkey.hex()
event_json["sig"] = res.signature.hex()

return {
"signed_event": json.dumps(event_json),
}
2 changes: 2 additions & 0 deletions python/src/trezorlib/cli/trezorctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
firmware,
monero,
nem,
nostr,
ripple,
settings,
solana,
Expand Down Expand Up @@ -409,6 +410,7 @@ def wait_for_emulator(obj: TrezorConnection, timeout: float) -> None:
cli.add_command(fido.cli)
cli.add_command(monero.cli)
cli.add_command(nem.cli)
cli.add_command(nostr.cli)
cli.add_command(ripple.cli)
cli.add_command(settings.cli)
cli.add_command(solana.cli)
Expand Down
4 changes: 3 additions & 1 deletion python/src/trezorlib/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
f'Cannot encode class "{type(msg).__name__}" without wire type'
)

buf = io.BytesIO()
protobuf.dump_message(buf, msg)
Expand Down
Loading

0 comments on commit feabb03

Please sign in to comment.