Skip to content

Commit

Permalink
feat(core): add nostr event signing
Browse files Browse the repository at this point in the history
  • Loading branch information
ibz committed Dec 10, 2024
1 parent 748a19a commit 4f32cb3
Show file tree
Hide file tree
Showing 21 changed files with 1,289 additions and 367 deletions.
35 changes: 35 additions & 0 deletions common/protob/messages-nostr.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
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 device to sign event
* @start
* @next NostrMessageSignature
* @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
}
3 changes: 3 additions & 0 deletions common/protob/messages.proto
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ enum MessageType {
MessageType_OwnershipProof = 50 [(bitcoin_only) = true, (wire_out) = true];
MessageType_AuthorizeCoinJoin = 51 [(bitcoin_only) = true, (wire_in) = true];

MessageType_NostrSignEvent = 2001 [(bitcoin_only) = true, (wire_in) = true];
MessageType_NostrEventSignature = 2002 [(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
4 changes: 4 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.

2 changes: 1 addition & 1 deletion core/src/apps/bitcoin/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@


BITCOIN_NAMES = ("Bitcoin", "Regtest", "Testnet")

NOSTR_NAMES = ("Nostr",)

class SigHashType(IntEnum):
"""Enumeration type listing the supported signature hash types.
Expand Down
2 changes: 2 additions & 0 deletions core/src/apps/bitcoin/keychain.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ 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
4 changes: 4 additions & 0 deletions core/src/apps/bitcoin/sign_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,16 @@ async def sign_message(
digest = message_digest(coin, message)
signature = secp256k1.sign(seckey, digest)

print("XXXXXXXXX", script_type)

if script_type == InputScriptType.SPENDADDRESS:
script_type_info = 0
elif script_type == InputScriptType.SPENDP2SHWITNESS:
script_type_info = 4
elif script_type == InputScriptType.SPENDWITNESS:
script_type_info = 8
elif script_type == InputScriptType.SPENDTAPROOT:
script_type_info = 12 # ??
else:
raise wire.ProcessError("Unsupported script type")

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
34 changes: 34 additions & 0 deletions core/src/apps/nostr/sign_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import TYPE_CHECKING

from apps.common.keychain import auto_keychain

from ..bitcoin.common import encode_bech32_address

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 trezor.crypto.curve import bip340
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()
sk = node.private_key()

serialized_event = f"[0,\"{pk}\",\"{created_at}\",{kind},{tags},\"{content}\"]"
event_id = sha256(serialized_event).digest()
signature = bip340.sign(sk, event_id)

return NostrEventSignature(pubkey=pk, id=event_id, signature=signature)
4 changes: 4 additions & 0 deletions core/src/apps/workflow_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ 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.NostrSignEvent:
return "apps.nostr.sign_event"

if not utils.BITCOIN_ONLY:
if msg_type == MessageType.SetU2FCounter:
return "apps.management.set_u2f_counter"
Expand Down
2 changes: 2 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.

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

40 changes: 40 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
59 changes: 59 additions & 0 deletions python/src/trezorlib/cli/nostr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# This file is part of the Trezor project.
#
# Copyright (C) 2012-2024 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>.

from typing import TYPE_CHECKING, Dict

import click
import json

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", required=True, 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
2 changes: 1 addition & 1 deletion python/src/trezorlib/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ 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
48 changes: 48 additions & 0 deletions python/src/trezorlib/messages.py

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

Loading

0 comments on commit 4f32cb3

Please sign in to comment.