mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-02-27 14:52:10 +00:00
feat(core): introduce Nostr
[no changelog]
This commit is contained in:
parent
323107e6a8
commit
f013340a25
64
common/protob/messages-nostr.proto
Normal file
64
common/protob/messages-nostr.proto
Normal file
@ -0,0 +1,64 @@
|
||||
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";
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
@ -324,6 +324,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];
|
||||
|
1
core/.changelog.d/4160.added
Normal file
1
core/.changelog.d/4160.added
Normal file
@ -0,0 +1 @@
|
||||
Add Nostr support (in debug mode only!).
|
@ -662,6 +662,10 @@ if FROZEN:
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nem/*/*.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/NEM*.py'))
|
||||
|
||||
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'))
|
||||
|
||||
|
@ -721,6 +721,10 @@ if FROZEN:
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/nem/*/*.py'))
|
||||
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/NEM*.py'))
|
||||
|
||||
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'))
|
||||
|
||||
|
@ -15,6 +15,7 @@ ALTCOIN_PREFIXES = (
|
||||
"fido",
|
||||
"monero",
|
||||
"nem",
|
||||
"nostr",
|
||||
"ripple",
|
||||
"solana",
|
||||
"stellar",
|
||||
|
6
core/src/all_modules.py
generated
6
core/src/all_modules.py
generated
@ -415,6 +415,12 @@ apps.misc.get_firmware_hash
|
||||
import apps.misc.get_firmware_hash
|
||||
apps.misc.sign_identity
|
||||
import apps.misc.sign_identity
|
||||
apps.nostr
|
||||
import apps.nostr
|
||||
apps.nostr.get_pubkey
|
||||
import apps.nostr.get_pubkey
|
||||
apps.nostr.sign_event
|
||||
import apps.nostr.sign_event
|
||||
apps.workflow_handlers
|
||||
import apps.workflow_handlers
|
||||
|
||||
|
9
core/src/apps/nostr/__init__.py
Normal file
9
core/src/apps/nostr/__init__.py
Normal file
@ -0,0 +1,9 @@
|
||||
from apps.common.paths import PATTERN_BIP44
|
||||
|
||||
CURVE = "secp256k1"
|
||||
SLIP44_ID = 1237
|
||||
|
||||
# Note: we are not satisfied with using this path even though it is defined in NIP-06.
|
||||
# See this issue for details: https://github.com/nostr-protocol/nips/issues/1774
|
||||
# TODO: we need to create a new NIP using a different derivation path and use that here!
|
||||
PATTERN = PATTERN_BIP44
|
24
core/src/apps/nostr/get_pubkey.py
Normal file
24
core/src/apps/nostr/get_pubkey.py
Normal file
@ -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:
|
||||
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)
|
48
core/src/apps/nostr/sign_event.py
Normal file
48
core/src/apps/nostr/sign_event.py
Normal file
@ -0,0 +1,48 @@
|
||||
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
|
||||
|
||||
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:]
|
||||
|
||||
# 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}"]'
|
||||
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,
|
||||
)
|
@ -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"
|
||||
|
4
core/src/trezor/enums/MessageType.py
generated
4
core/src/trezor/enums/MessageType.py
generated
@ -250,3 +250,7 @@ if not utils.BITCOIN_ONLY:
|
||||
SolanaAddress = 903
|
||||
SolanaSignTx = 904
|
||||
SolanaTxSignature = 905
|
||||
NostrGetPubkey = 2001
|
||||
NostrPubkey = 2002
|
||||
NostrSignEvent = 2003
|
||||
NostrEventSignature = 2004
|
||||
|
4
core/src/trezor/enums/__init__.py
generated
4
core/src/trezor/enums/__init__.py
generated
@ -591,6 +591,10 @@ if TYPE_CHECKING:
|
||||
SolanaAddress = 903
|
||||
SolanaSignTx = 904
|
||||
SolanaTxSignature = 905
|
||||
NostrGetPubkey = 2001
|
||||
NostrPubkey = 2002
|
||||
NostrSignEvent = 2003
|
||||
NostrEventSignature = 2004
|
||||
BenchmarkListNames = 9100
|
||||
BenchmarkNames = 9101
|
||||
BenchmarkRun = 9102
|
||||
|
86
core/src/trezor/messages.py
generated
86
core/src/trezor/messages.py
generated
@ -5216,6 +5216,92 @@ if TYPE_CHECKING:
|
||||
def is_type_of(cls, msg: Any) -> TypeGuard["NEMCosignatoryModification"]:
|
||||
return isinstance(msg, cls)
|
||||
|
||||
class NostrGetPubkey(protobuf.MessageType):
|
||||
address_n: "list[int]"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
address_n: "list[int] | None" = None,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def is_type_of(cls, msg: Any) -> TypeGuard["NostrGetPubkey"]:
|
||||
return isinstance(msg, cls)
|
||||
|
||||
class NostrPubkey(protobuf.MessageType):
|
||||
pubkey: "bytes"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
pubkey: "bytes",
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def is_type_of(cls, msg: Any) -> TypeGuard["NostrPubkey"]:
|
||||
return isinstance(msg, cls)
|
||||
|
||||
class NostrTag(protobuf.MessageType):
|
||||
key: "str"
|
||||
value: "str | None"
|
||||
extra: "list[str]"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
key: "str",
|
||||
extra: "list[str] | None" = None,
|
||||
value: "str | None" = None,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def is_type_of(cls, msg: Any) -> TypeGuard["NostrTag"]:
|
||||
return isinstance(msg, cls)
|
||||
|
||||
class NostrSignEvent(protobuf.MessageType):
|
||||
address_n: "list[int]"
|
||||
created_at: "int"
|
||||
kind: "int"
|
||||
tags: "list[NostrTag]"
|
||||
content: "str"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
created_at: "int",
|
||||
kind: "int",
|
||||
content: "str",
|
||||
address_n: "list[int] | None" = None,
|
||||
tags: "list[NostrTag] | None" = None,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def is_type_of(cls, msg: Any) -> TypeGuard["NostrSignEvent"]:
|
||||
return isinstance(msg, cls)
|
||||
|
||||
class NostrEventSignature(protobuf.MessageType):
|
||||
pubkey: "bytes"
|
||||
id: "bytes"
|
||||
signature: "bytes"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
pubkey: "bytes",
|
||||
id: "bytes",
|
||||
signature: "bytes",
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def is_type_of(cls, msg: Any) -> TypeGuard["NostrEventSignature"]:
|
||||
return isinstance(msg, cls)
|
||||
|
||||
class RippleGetAddress(protobuf.MessageType):
|
||||
address_n: "list[int]"
|
||||
show_display: "bool | None"
|
||||
|
@ -12,7 +12,8 @@ SKIPPED_MESSAGES := Binance Cardano DebugMonero Eos Monero Ontology Ripple SdPro
|
||||
Solana StellarClaimClaimableBalanceOp \
|
||||
ChangeLanguage TranslationDataRequest TranslationDataAck \
|
||||
SetBrightness DebugLinkOptigaSetSecMax EntropyCheckReady EntropyCheckContinue \
|
||||
BenchmarkListNames BenchmarkRun BenchmarkNames BenchmarkResult
|
||||
BenchmarkListNames BenchmarkRun BenchmarkNames BenchmarkResult \
|
||||
NostrGetPubkey NostrPubkey NostrSignEvent NostrEventSignature
|
||||
|
||||
ifeq ($(BITCOIN_ONLY), 1)
|
||||
SKIPPED_MESSAGES += Ethereum NEM Stellar
|
||||
|
93
python/src/trezorlib/cli/nostr.py
Normal file
93
python/src/trezorlib/cli/nostr.py
Normal file
@ -0,0 +1,93 @@
|
||||
# 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>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import typing as t
|
||||
|
||||
import click
|
||||
|
||||
from .. import messages, nostr, tools
|
||||
from . import with_client
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from ..client import TrezorClient
|
||||
|
||||
|
||||
PATH_TEMPLATE = "m/44h/1237h/{}h/0/0"
|
||||
|
||||
|
||||
@click.group(name="nostr")
|
||||
def cli() -> None:
|
||||
pass
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("-a", "--account", default=0, help="Account index")
|
||||
@with_client
|
||||
def get_pubkey(
|
||||
client: "TrezorClient",
|
||||
account: int,
|
||||
) -> str:
|
||||
"""Return the pubkey derived by the given path."""
|
||||
|
||||
address_n = tools.parse_path(PATH_TEMPLATE.format(account))
|
||||
|
||||
return nostr.get_pubkey(
|
||||
client,
|
||||
address_n,
|
||||
).hex()
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("-a", "--account", default=0, help="Account index")
|
||||
@click.argument("event")
|
||||
@with_client
|
||||
def sign_event(
|
||||
client: "TrezorClient",
|
||||
account: int,
|
||||
event: str,
|
||||
) -> dict[str, str]:
|
||||
"""Sign an event using the key derived by the given path."""
|
||||
|
||||
event_json = json.loads(event)
|
||||
|
||||
address_n = tools.parse_path(PATH_TEMPLATE.format(account))
|
||||
|
||||
res = nostr.sign_event(
|
||||
client,
|
||||
messages.NostrSignEvent(
|
||||
address_n=address_n,
|
||||
created_at=event_json["created_at"],
|
||||
kind=event_json["kind"],
|
||||
tags=[
|
||||
messages.NostrTag(
|
||||
key=t[0], value=t[1] if len(t) > 1 else None, extra=t[2:]
|
||||
)
|
||||
for t in event_json["tags"]
|
||||
],
|
||||
content=event_json["content"],
|
||||
),
|
||||
)
|
||||
|
||||
event_json["id"] = res.id.hex()
|
||||
event_json["pubkey"] = res.pubkey.hex()
|
||||
event_json["sig"] = res.signature.hex()
|
||||
|
||||
return {
|
||||
"signed_event": event_json,
|
||||
}
|
@ -44,6 +44,7 @@ from . import (
|
||||
firmware,
|
||||
monero,
|
||||
nem,
|
||||
nostr,
|
||||
ripple,
|
||||
settings,
|
||||
solana,
|
||||
@ -409,6 +410,7 @@ cli.add_command(ethereum.cli)
|
||||
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)
|
||||
|
98
python/src/trezorlib/messages.py
generated
98
python/src/trezorlib/messages.py
generated
@ -644,6 +644,10 @@ class MessageType(IntEnum):
|
||||
SolanaAddress = 903
|
||||
SolanaSignTx = 904
|
||||
SolanaTxSignature = 905
|
||||
NostrGetPubkey = 2001
|
||||
NostrPubkey = 2002
|
||||
NostrSignEvent = 2003
|
||||
NostrEventSignature = 2004
|
||||
BenchmarkListNames = 9100
|
||||
BenchmarkNames = 9101
|
||||
BenchmarkRun = 9102
|
||||
@ -6775,6 +6779,100 @@ class NEMCosignatoryModification(protobuf.MessageType):
|
||||
self.public_key = public_key
|
||||
|
||||
|
||||
class NostrGetPubkey(protobuf.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 2001
|
||||
FIELDS = {
|
||||
1: protobuf.Field("address_n", "uint32", repeated=True, required=False, default=None),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
address_n: Optional[Sequence["int"]] = None,
|
||||
) -> None:
|
||||
self.address_n: Sequence["int"] = address_n if address_n is not None else []
|
||||
|
||||
|
||||
class NostrPubkey(protobuf.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 2002
|
||||
FIELDS = {
|
||||
1: protobuf.Field("pubkey", "bytes", repeated=False, required=True),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
pubkey: "bytes",
|
||||
) -> None:
|
||||
self.pubkey = pubkey
|
||||
|
||||
|
||||
class NostrTag(protobuf.MessageType):
|
||||
MESSAGE_WIRE_TYPE = None
|
||||
FIELDS = {
|
||||
1: protobuf.Field("key", "string", repeated=False, required=True),
|
||||
2: protobuf.Field("value", "string", repeated=False, required=False, default=None),
|
||||
3: protobuf.Field("extra", "string", repeated=True, required=False, default=None),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
key: "str",
|
||||
extra: Optional[Sequence["str"]] = None,
|
||||
value: Optional["str"] = None,
|
||||
) -> None:
|
||||
self.extra: Sequence["str"] = extra if extra is not None else []
|
||||
self.key = key
|
||||
self.value = value
|
||||
|
||||
|
||||
class NostrSignEvent(protobuf.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 2003
|
||||
FIELDS = {
|
||||
1: protobuf.Field("address_n", "uint32", repeated=True, required=False, default=None),
|
||||
2: protobuf.Field("created_at", "uint32", repeated=False, required=True),
|
||||
3: protobuf.Field("kind", "uint32", repeated=False, required=True),
|
||||
4: protobuf.Field("tags", "NostrTag", repeated=True, required=False, default=None),
|
||||
5: protobuf.Field("content", "string", repeated=False, required=True),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
created_at: "int",
|
||||
kind: "int",
|
||||
content: "str",
|
||||
address_n: Optional[Sequence["int"]] = None,
|
||||
tags: Optional[Sequence["NostrTag"]] = None,
|
||||
) -> None:
|
||||
self.address_n: Sequence["int"] = address_n if address_n is not None else []
|
||||
self.tags: Sequence["NostrTag"] = tags if tags is not None else []
|
||||
self.created_at = created_at
|
||||
self.kind = kind
|
||||
self.content = content
|
||||
|
||||
|
||||
class NostrEventSignature(protobuf.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 2004
|
||||
FIELDS = {
|
||||
1: protobuf.Field("pubkey", "bytes", repeated=False, required=True),
|
||||
2: protobuf.Field("id", "bytes", repeated=False, required=True),
|
||||
3: protobuf.Field("signature", "bytes", repeated=False, required=True),
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
pubkey: "bytes",
|
||||
id: "bytes",
|
||||
signature: "bytes",
|
||||
) -> None:
|
||||
self.pubkey = pubkey
|
||||
self.id = id
|
||||
self.signature = signature
|
||||
|
||||
|
||||
class RippleGetAddress(protobuf.MessageType):
|
||||
MESSAGE_WIRE_TYPE = 400
|
||||
FIELDS = {
|
||||
|
40
python/src/trezorlib/nostr.py
Normal file
40
python/src/trezorlib/nostr.py
Normal file
@ -0,0 +1,40 @@
|
||||
# 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>.
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from . import messages
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .client import TrezorClient
|
||||
from .tools import Address
|
||||
|
||||
|
||||
def get_pubkey(client: "TrezorClient", n: "Address") -> bytes:
|
||||
return client.call(
|
||||
messages.NostrGetPubkey(
|
||||
address_n=n,
|
||||
),
|
||||
expect=messages.NostrPubkey,
|
||||
).pubkey
|
||||
|
||||
|
||||
def sign_event(
|
||||
client: "TrezorClient",
|
||||
sign_event: messages.NostrSignEvent,
|
||||
) -> messages.NostrEventSignature:
|
||||
return client.call(sign_event, expect=messages.NostrEventSignature)
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Generates the `trezor_message_impl!` macro calls for the `src/messages/mod.rs` file.
|
||||
# Generates the `trezor_message_impl!` macro calls for the `src/messages/generated.rs` file.
|
||||
|
||||
from os import path
|
||||
|
||||
@ -24,6 +24,7 @@ FEATURES = {
|
||||
"EOS": "eos",
|
||||
"Monero": "monero",
|
||||
"NEM": "nem",
|
||||
"Nostr": "nostr",
|
||||
"Ripple": "ripple",
|
||||
"Solana": "solana",
|
||||
"Stellar": "stellar",
|
||||
|
8
rust/trezor-client/src/messages/generated.rs
generated
8
rust/trezor-client/src/messages/generated.rs
generated
@ -237,6 +237,14 @@ trezor_message_impl! {
|
||||
NEMDecryptedMessage => MessageType_NEMDecryptedMessage,
|
||||
}
|
||||
|
||||
#[cfg(feature = "nostr")]
|
||||
trezor_message_impl! {
|
||||
NostrGetPubkey => MessageType_NostrGetPubkey,
|
||||
NostrPubkey => MessageType_NostrPubkey,
|
||||
NostrSignEvent => MessageType_NostrSignEvent,
|
||||
NostrEventSignature => MessageType_NostrEventSignature,
|
||||
}
|
||||
|
||||
#[cfg(feature = "ripple")]
|
||||
trezor_message_impl! {
|
||||
RippleGetAddress => MessageType_RippleGetAddress,
|
||||
|
56
rust/trezor-client/src/protos/generated/messages.rs
generated
56
rust/trezor-client/src/protos/generated/messages.rs
generated
@ -514,6 +514,14 @@ pub enum MessageType {
|
||||
MessageType_SolanaSignTx = 904,
|
||||
// @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_SolanaTxSignature)
|
||||
MessageType_SolanaTxSignature = 905,
|
||||
// @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_NostrGetPubkey)
|
||||
MessageType_NostrGetPubkey = 2001,
|
||||
// @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_NostrPubkey)
|
||||
MessageType_NostrPubkey = 2002,
|
||||
// @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_NostrSignEvent)
|
||||
MessageType_NostrSignEvent = 2003,
|
||||
// @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_NostrEventSignature)
|
||||
MessageType_NostrEventSignature = 2004,
|
||||
// @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_BenchmarkListNames)
|
||||
MessageType_BenchmarkListNames = 9100,
|
||||
// @@protoc_insertion_point(enum_value:hw.trezor.messages.MessageType.MessageType_BenchmarkNames)
|
||||
@ -776,6 +784,10 @@ impl ::protobuf::Enum for MessageType {
|
||||
903 => ::std::option::Option::Some(MessageType::MessageType_SolanaAddress),
|
||||
904 => ::std::option::Option::Some(MessageType::MessageType_SolanaSignTx),
|
||||
905 => ::std::option::Option::Some(MessageType::MessageType_SolanaTxSignature),
|
||||
2001 => ::std::option::Option::Some(MessageType::MessageType_NostrGetPubkey),
|
||||
2002 => ::std::option::Option::Some(MessageType::MessageType_NostrPubkey),
|
||||
2003 => ::std::option::Option::Some(MessageType::MessageType_NostrSignEvent),
|
||||
2004 => ::std::option::Option::Some(MessageType::MessageType_NostrEventSignature),
|
||||
9100 => ::std::option::Option::Some(MessageType::MessageType_BenchmarkListNames),
|
||||
9101 => ::std::option::Option::Some(MessageType::MessageType_BenchmarkNames),
|
||||
9102 => ::std::option::Option::Some(MessageType::MessageType_BenchmarkRun),
|
||||
@ -1029,6 +1041,10 @@ impl ::protobuf::Enum for MessageType {
|
||||
"MessageType_SolanaAddress" => ::std::option::Option::Some(MessageType::MessageType_SolanaAddress),
|
||||
"MessageType_SolanaSignTx" => ::std::option::Option::Some(MessageType::MessageType_SolanaSignTx),
|
||||
"MessageType_SolanaTxSignature" => ::std::option::Option::Some(MessageType::MessageType_SolanaTxSignature),
|
||||
"MessageType_NostrGetPubkey" => ::std::option::Option::Some(MessageType::MessageType_NostrGetPubkey),
|
||||
"MessageType_NostrPubkey" => ::std::option::Option::Some(MessageType::MessageType_NostrPubkey),
|
||||
"MessageType_NostrSignEvent" => ::std::option::Option::Some(MessageType::MessageType_NostrSignEvent),
|
||||
"MessageType_NostrEventSignature" => ::std::option::Option::Some(MessageType::MessageType_NostrEventSignature),
|
||||
"MessageType_BenchmarkListNames" => ::std::option::Option::Some(MessageType::MessageType_BenchmarkListNames),
|
||||
"MessageType_BenchmarkNames" => ::std::option::Option::Some(MessageType::MessageType_BenchmarkNames),
|
||||
"MessageType_BenchmarkRun" => ::std::option::Option::Some(MessageType::MessageType_BenchmarkRun),
|
||||
@ -1281,6 +1297,10 @@ impl ::protobuf::Enum for MessageType {
|
||||
MessageType::MessageType_SolanaAddress,
|
||||
MessageType::MessageType_SolanaSignTx,
|
||||
MessageType::MessageType_SolanaTxSignature,
|
||||
MessageType::MessageType_NostrGetPubkey,
|
||||
MessageType::MessageType_NostrPubkey,
|
||||
MessageType::MessageType_NostrSignEvent,
|
||||
MessageType::MessageType_NostrEventSignature,
|
||||
MessageType::MessageType_BenchmarkListNames,
|
||||
MessageType::MessageType_BenchmarkNames,
|
||||
MessageType::MessageType_BenchmarkRun,
|
||||
@ -1539,10 +1559,14 @@ impl ::protobuf::EnumFull for MessageType {
|
||||
MessageType::MessageType_SolanaAddress => 240,
|
||||
MessageType::MessageType_SolanaSignTx => 241,
|
||||
MessageType::MessageType_SolanaTxSignature => 242,
|
||||
MessageType::MessageType_BenchmarkListNames => 243,
|
||||
MessageType::MessageType_BenchmarkNames => 244,
|
||||
MessageType::MessageType_BenchmarkRun => 245,
|
||||
MessageType::MessageType_BenchmarkResult => 246,
|
||||
MessageType::MessageType_NostrGetPubkey => 243,
|
||||
MessageType::MessageType_NostrPubkey => 244,
|
||||
MessageType::MessageType_NostrSignEvent => 245,
|
||||
MessageType::MessageType_NostrEventSignature => 246,
|
||||
MessageType::MessageType_BenchmarkListNames => 247,
|
||||
MessageType::MessageType_BenchmarkNames => 248,
|
||||
MessageType::MessageType_BenchmarkRun => 249,
|
||||
MessageType::MessageType_BenchmarkResult => 250,
|
||||
};
|
||||
Self::enum_descriptor().value_by_index(index)
|
||||
}
|
||||
@ -1561,7 +1585,7 @@ impl MessageType {
|
||||
}
|
||||
|
||||
static file_descriptor_proto_data: &'static [u8] = b"\
|
||||
\n\x0emessages.proto\x12\x12hw.trezor.messages\x1a\roptions.proto*\xe8U\
|
||||
\n\x0emessages.proto\x12\x12hw.trezor.messages\x1a\roptions.proto*\x86W\
|
||||
\n\x0bMessageType\x12(\n\x16MessageType_Initialize\x10\0\x1a\x0c\x80\xa6\
|
||||
\x1d\x01\xb0\xb5\x18\x01\x90\xb5\x18\x01\x12\x1e\n\x10MessageType_Ping\
|
||||
\x10\x01\x1a\x08\x80\xa6\x1d\x01\x90\xb5\x18\x01\x12%\n\x13MessageType_S\
|
||||
@ -1838,15 +1862,19 @@ static file_descriptor_proto_data: &'static [u8] = b"\
|
||||
\x18\x01\x12$\n\x19MessageType_SolanaAddress\x10\x87\x07\x1a\x04\x98\xb5\
|
||||
\x18\x01\x12#\n\x18MessageType_SolanaSignTx\x10\x88\x07\x1a\x04\x90\xb5\
|
||||
\x18\x01\x12(\n\x1dMessageType_SolanaTxSignature\x10\x89\x07\x1a\x04\x98\
|
||||
\xb5\x18\x01\x12)\n\x1eMessageType_BenchmarkListNames\x10\x8cG\x1a\x04\
|
||||
\x80\xa6\x1d\x01\x12%\n\x1aMessageType_BenchmarkNames\x10\x8dG\x1a\x04\
|
||||
\x80\xa6\x1d\x01\x12#\n\x18MessageType_BenchmarkRun\x10\x8eG\x1a\x04\x80\
|
||||
\xa6\x1d\x01\x12&\n\x1bMessageType_BenchmarkResult\x10\x8fG\x1a\x04\x80\
|
||||
\xa6\x1d\x01\x1a\x04\xc8\xf3\x18\x01\"\x04\x08Z\x10\\\"\x04\x08G\x10J\"\
|
||||
\x04\x08r\x10z\"\x06\x08\xdb\x01\x10\xdb\x01\"\x06\x08\xe0\x01\x10\xe0\
|
||||
\x01\"\x06\x08\xac\x02\x10\xb0\x02\"\x06\x08\xb5\x02\x10\xb8\x02\"\x06\
|
||||
\x08\xe8\x07\x10\xcb\x08B8\n#com.satoshilabs.trezor.lib.protobufB\rTrezo\
|
||||
rMessage\x80\xa6\x1d\x01\
|
||||
\xb5\x18\x01\x12%\n\x1aMessageType_NostrGetPubkey\x10\xd1\x0f\x1a\x04\
|
||||
\x90\xb5\x18\x01\x12\"\n\x17MessageType_NostrPubkey\x10\xd2\x0f\x1a\x04\
|
||||
\x98\xb5\x18\x01\x12%\n\x1aMessageType_NostrSignEvent\x10\xd3\x0f\x1a\
|
||||
\x04\x90\xb5\x18\x01\x12*\n\x1fMessageType_NostrEventSignature\x10\xd4\
|
||||
\x0f\x1a\x04\x98\xb5\x18\x01\x12)\n\x1eMessageType_BenchmarkListNames\
|
||||
\x10\x8cG\x1a\x04\x80\xa6\x1d\x01\x12%\n\x1aMessageType_BenchmarkNames\
|
||||
\x10\x8dG\x1a\x04\x80\xa6\x1d\x01\x12#\n\x18MessageType_BenchmarkRun\x10\
|
||||
\x8eG\x1a\x04\x80\xa6\x1d\x01\x12&\n\x1bMessageType_BenchmarkResult\x10\
|
||||
\x8fG\x1a\x04\x80\xa6\x1d\x01\x1a\x04\xc8\xf3\x18\x01\"\x04\x08Z\x10\\\"\
|
||||
\x04\x08G\x10J\"\x04\x08r\x10z\"\x06\x08\xdb\x01\x10\xdb\x01\"\x06\x08\
|
||||
\xe0\x01\x10\xe0\x01\"\x06\x08\xac\x02\x10\xb0\x02\"\x06\x08\xb5\x02\x10\
|
||||
\xb8\x02\"\x06\x08\xe8\x07\x10\xcb\x08B8\n#com.satoshilabs.trezor.lib.pr\
|
||||
otobufB\rTrezorMessage\x80\xa6\x1d\x01\
|
||||
";
|
||||
|
||||
/// `FileDescriptorProto` object which was a source for this generated file
|
||||
|
1155
rust/trezor-client/src/protos/generated/messages_nostr.rs
generated
Normal file
1155
rust/trezor-client/src/protos/generated/messages_nostr.rs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -33,6 +33,7 @@ mod generated {
|
||||
"eos" => messages_eos
|
||||
"monero" => messages_monero
|
||||
"nem" => messages_nem
|
||||
"nostr" => messages_nostr
|
||||
"ripple" => messages_ripple
|
||||
"solana" => messages_solana
|
||||
"stellar" => messages_stellar
|
||||
|
0
tests/device_tests/nostr/__init__.py
Normal file
0
tests/device_tests/nostr/__init__.py
Normal file
129
tests/device_tests/nostr/test_nostr.py
Normal file
129
tests/device_tests/nostr/test_nostr.py
Normal file
@ -0,0 +1,129 @@
|
||||
# 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 hashlib import sha256
|
||||
|
||||
import pytest
|
||||
from ecdsa import SECP256k1, VerifyingKey
|
||||
from six import b
|
||||
|
||||
from trezorlib import messages, nostr
|
||||
from trezorlib.tools import parse_path
|
||||
|
||||
pytestmark = [pytest.mark.altcoin, pytest.mark.models("core")]
|
||||
|
||||
# test data from NIP-06: https://github.com/nostr-protocol/nips/blob/master/06.md
|
||||
|
||||
LEAD_MONKEY_MNEMONIC = (
|
||||
"leader monkey parrot ring guide accident before fence cannon height naive bean"
|
||||
)
|
||||
LEAD_MONKEY_PUBKEY_HEX = (
|
||||
"17162c921dc4d2518f9a101db33695df1afb56ab82f5ff3e5da6eec3ca5cd917"
|
||||
)
|
||||
|
||||
WHAT_BLEAK_MNEMONIC = "what bleak badge arrange retreat wolf trade produce cricket blur garlic valid proud rude strong choose busy staff weather area salt hollow arm fade"
|
||||
WHAT_BLEAK_PUBKEY_HEX = (
|
||||
"d41b22899549e1f3d335a31002cfd382174006e166d3e658e3a5eecdb6463573"
|
||||
)
|
||||
|
||||
|
||||
pytestmark_lead_monkey = pytest.mark.setup_client(mnemonic=LEAD_MONKEY_MNEMONIC)
|
||||
pytestmark_what_bleak = pytest.mark.setup_client(mnemonic=WHAT_BLEAK_MNEMONIC)
|
||||
|
||||
VECTORS = [
|
||||
pytest.param(LEAD_MONKEY_PUBKEY_HEX, marks=pytestmark_lead_monkey),
|
||||
pytest.param(WHAT_BLEAK_PUBKEY_HEX, marks=pytestmark_what_bleak),
|
||||
]
|
||||
|
||||
TEST_EVENT = {
|
||||
"created_at": 1737396950,
|
||||
"kind": 1,
|
||||
"tags": [
|
||||
[
|
||||
"e",
|
||||
"5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36",
|
||||
"wss://nostr.example.com",
|
||||
],
|
||||
["p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca"],
|
||||
[
|
||||
"a",
|
||||
"30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd",
|
||||
"wss://nostr.example.com",
|
||||
],
|
||||
["alt", "reply"],
|
||||
],
|
||||
"content": "Hello, world",
|
||||
}
|
||||
|
||||
SIGN_TEST_EVENT = messages.NostrSignEvent(
|
||||
address_n=parse_path("m/44h/1237h/0h/0/0"),
|
||||
created_at=TEST_EVENT["created_at"],
|
||||
kind=TEST_EVENT["kind"],
|
||||
content=TEST_EVENT["content"],
|
||||
tags=[
|
||||
messages.NostrTag(key=t[0], value=t[1] if len(t) > 1 else None, extra=t[2:])
|
||||
for t in TEST_EVENT["tags"]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pubkey_hex", VECTORS)
|
||||
def test_get_pubkey(client, pubkey_hex):
|
||||
response = nostr.get_pubkey(
|
||||
client,
|
||||
n=parse_path("m/44h/1237h/0h/0/0"),
|
||||
)
|
||||
|
||||
assert response == bytes.fromhex(pubkey_hex)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pubkey_hex", VECTORS)
|
||||
def test_sign_event(client, pubkey_hex):
|
||||
response = nostr.sign_event(client, SIGN_TEST_EVENT)
|
||||
|
||||
assert response.pubkey == bytes.fromhex(pubkey_hex)
|
||||
|
||||
expected_id = (
|
||||
sha256(
|
||||
json.dumps(
|
||||
[
|
||||
0,
|
||||
pubkey_hex,
|
||||
TEST_EVENT["created_at"],
|
||||
TEST_EVENT["kind"],
|
||||
TEST_EVENT["tags"],
|
||||
TEST_EVENT["content"],
|
||||
],
|
||||
separators=(",", ":"),
|
||||
).encode()
|
||||
)
|
||||
.digest()
|
||||
.hex()
|
||||
)
|
||||
|
||||
assert response.id == expected_id
|
||||
|
||||
vk = VerifyingKey.from_string(
|
||||
b("\x03") + bytes.fromhex(pubkey_hex),
|
||||
curve=SECP256k1,
|
||||
# this is a pretty silly way to tell VerifyingKey
|
||||
# that we do not want the message to be hashed
|
||||
# when verifying the signature!
|
||||
hashfunc=lambda x: type("h", (), {"digest": lambda: x}),
|
||||
)
|
||||
|
||||
assert vk.verify(bytes.fromhex(response.signature), bytes.fromhex(response.id))
|
Loading…
Reference in New Issue
Block a user