diff --git a/Cargo.lock b/Cargo.lock index 8762c6310..392bea3cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1183,6 +1183,7 @@ dependencies = [ "thiserror", "time", "tokio 1.34.0", + "tokio-stream", "tonic 0.8.3", "tonic-build 0.8.4", "tower 0.4.13", diff --git a/libs/gl-client-py/glclient/greenlight_pb2.py b/libs/gl-client-py/glclient/greenlight_pb2.py index b2696f17f..3c4ec052c 100644 --- a/libs/gl-client-py/glclient/greenlight_pb2.py +++ b/libs/gl-client-py/glclient/greenlight_pb2.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: greenlight.proto +# Protobuf Python Version: 4.25.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool diff --git a/libs/gl-client-py/glclient/scheduler_pb2.py b/libs/gl-client-py/glclient/scheduler_pb2.py index 8c0e52074..4b1e6b618 100644 --- a/libs/gl-client-py/glclient/scheduler_pb2.py +++ b/libs/gl-client-py/glclient/scheduler_pb2.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: scheduler.proto +# Protobuf Python Version: 4.25.0 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -14,17 +15,17 @@ from . import greenlight_pb2 as greenlight__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fscheduler.proto\x12\tscheduler\x1a\x10greenlight.proto\"M\n\x10\x43hallengeRequest\x12(\n\x05scope\x18\x01 \x01(\x0e\x32\x19.scheduler.ChallengeScope\x12\x0f\n\x07node_id\x18\x02 \x01(\x0c\"&\n\x11\x43hallengeResponse\x12\x11\n\tchallenge\x18\x01 \x01(\x0c\"\xea\x01\n\x13RegistrationRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\x0c\x12\x11\n\tbip32_key\x18\x02 \x01(\x0c\x12\x0f\n\x07network\x18\x04 \x01(\t\x12\x11\n\tchallenge\x18\x05 \x01(\x0c\x12\x11\n\tsignature\x18\x06 \x01(\x0c\x12\x14\n\x0csigner_proto\x18\x07 \x01(\t\x12\x10\n\x08init_msg\x18\x08 \x01(\x0c\x12\x0b\n\x03\x63sr\x18\t \x01(\x0c\x12\x13\n\x0binvite_code\x18\n \x01(\t\x12.\n\x0bstartupmsgs\x18\x03 \x03(\x0b\x32\x19.scheduler.StartupMessage\"[\n\x14RegistrationResponse\x12\x13\n\x0b\x64\x65vice_cert\x18\x01 \x01(\t\x12\x12\n\ndevice_key\x18\x02 \x01(\t\x12\x0c\n\x04rune\x18\x03 \x01(\t\x12\x0c\n\x04\x61uth\x18\x04 \x01(\x0c\"\"\n\x0fScheduleRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\x0c\"0\n\x0fNodeInfoRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\x0c\x12\x0c\n\x04wait\x18\x02 \x01(\x08\"5\n\x10NodeInfoResponse\x12\x0f\n\x07node_id\x18\x01 \x01(\x0c\x12\x10\n\x08grpc_uri\x18\x02 \x01(\t\"U\n\x0fRecoveryRequest\x12\x11\n\tchallenge\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x0f\n\x07node_id\x18\x03 \x01(\x0c\x12\x0b\n\x03\x63sr\x18\t \x01(\x0c\"W\n\x10RecoveryResponse\x12\x13\n\x0b\x64\x65vice_cert\x18\x01 \x01(\t\x12\x12\n\ndevice_key\x18\x02 \x01(\t\x12\x0c\n\x04rune\x18\x03 \x01(\t\x12\x0c\n\x04\x61uth\x18\x04 \x01(\x0c\"m\n\x0eUpgradeRequest\x12\x16\n\x0esigner_version\x18\x01 \x01(\t\x12\x13\n\x07initmsg\x18\x02 \x01(\x0c\x42\x02\x18\x01\x12.\n\x0bstartupmsgs\x18\x03 \x03(\x0b\x32\x19.scheduler.StartupMessage\"&\n\x0fUpgradeResponse\x12\x13\n\x0bold_version\x18\x01 \x01(\t\"3\n\x0eStartupMessage\x12\x0f\n\x07request\x18\x01 \x01(\x0c\x12\x10\n\x08response\x18\x02 \x01(\x0c\"\x18\n\x16ListInviteCodesRequest\"J\n\x17ListInviteCodesResponse\x12/\n\x10invite_code_list\x18\x01 \x03(\x0b\x32\x15.scheduler.InviteCode\"/\n\nInviteCode\x12\x0c\n\x04\x63ode\x18\x01 \x01(\t\x12\x13\n\x0bis_redeemed\x18\x02 \x01(\x08\"\x13\n\x11\x45xportNodeRequest\"!\n\x12\x45xportNodeResponse\x12\x0b\n\x03url\x18\x01 \x01(\t\"\\\n\x0fSignerRejection\x12\x0b\n\x03msg\x18\x01 \x01(\t\x12\'\n\x07request\x18\x02 \x01(\x0b\x32\x16.greenlight.HsmRequest\x12\x13\n\x0bgit_version\x18\x03 \x01(\t\"g\n\x11PairDeviceRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0b\n\x03\x63sr\x18\x02 \x01(\x0c\x12\x13\n\x0b\x64\x65vice_name\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x65sc\x18\x04 \x01(\t\x12\x0e\n\x06restrs\x18\x05 \x01(\t\"m\n\x12PairDeviceResponse\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65vice_cert\x18\x02 \x01(\t\x12\x12\n\ndevice_key\x18\x03 \x01(\t\x12\x0c\n\x04rune\x18\x04 \x01(\t\x12\x0c\n\x04\x61uth\x18\x05 \x01(\x0c\"\x19\n\tPairingQr\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\t\"+\n\x15GetPairingDataRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\"l\n\x16GetPairingDataResponse\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0b\n\x03\x63sr\x18\x02 \x01(\x0c\x12\x13\n\x0b\x64\x65vice_name\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x65sc\x18\x04 \x01(\t\x12\x0e\n\x06restrs\x18\x05 \x01(\t\"\x8f\x01\n\x15\x41pprovePairingRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x0f\n\x07node_id\x18\x03 \x01(\x0c\x12\x13\n\x0b\x64\x65vice_name\x18\x04 \x01(\t\x12\x0e\n\x06restrs\x18\x05 \x01(\t\x12\x0b\n\x03sig\x18\x06 \x01(\x0c\x12\x0c\n\x04rune\x18\x07 \x01(\t\"\x07\n\x05\x45mpty*+\n\x0e\x43hallengeScope\x12\x0c\n\x08REGISTER\x10\x00\x12\x0b\n\x07RECOVER\x10\x01\x32\xf0\x04\n\tScheduler\x12M\n\x08Register\x12\x1e.scheduler.RegistrationRequest\x1a\x1f.scheduler.RegistrationResponse\"\x00\x12\x44\n\x07Recover\x12\x1a.scheduler.RecoveryRequest\x1a\x1b.scheduler.RecoveryResponse\"\x00\x12K\n\x0cGetChallenge\x12\x1b.scheduler.ChallengeRequest\x1a\x1c.scheduler.ChallengeResponse\"\x00\x12\x45\n\x08Schedule\x12\x1a.scheduler.ScheduleRequest\x1a\x1b.scheduler.NodeInfoResponse\"\x00\x12H\n\x0bGetNodeInfo\x12\x1a.scheduler.NodeInfoRequest\x1a\x1b.scheduler.NodeInfoResponse\"\x00\x12G\n\x0cMaybeUpgrade\x12\x19.scheduler.UpgradeRequest\x1a\x1a.scheduler.UpgradeResponse\"\x00\x12Z\n\x0fListInviteCodes\x12!.scheduler.ListInviteCodesRequest\x1a\".scheduler.ListInviteCodesResponse\"\x00\x12K\n\nExportNode\x12\x1c.scheduler.ExportNodeRequest\x1a\x1d.scheduler.ExportNodeResponse\"\x00\x32Q\n\x05\x44\x65\x62ug\x12H\n\x15ReportSignerRejection\x12\x1a.scheduler.SignerRejection\x1a\x11.greenlight.Empty\"\x00\x32\xf7\x01\n\x07Pairing\x12K\n\nPairDevice\x12\x1c.scheduler.PairDeviceRequest\x1a\x1d.scheduler.PairDeviceResponse\"\x00\x12W\n\x0eGetPairingData\x12 .scheduler.GetPairingDataRequest\x1a!.scheduler.GetPairingDataResponse\"\x00\x12\x46\n\x0e\x41pprovePairing\x12 .scheduler.ApprovePairingRequest\x1a\x10.scheduler.Empty\"\x00\x62\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0fscheduler.proto\x12\tscheduler\x1a\x10greenlight.proto\"M\n\x10\x43hallengeRequest\x12(\n\x05scope\x18\x01 \x01(\x0e\x32\x19.scheduler.ChallengeScope\x12\x0f\n\x07node_id\x18\x02 \x01(\x0c\"&\n\x11\x43hallengeResponse\x12\x11\n\tchallenge\x18\x01 \x01(\x0c\"\xea\x01\n\x13RegistrationRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\x0c\x12\x11\n\tbip32_key\x18\x02 \x01(\x0c\x12\x0f\n\x07network\x18\x04 \x01(\t\x12\x11\n\tchallenge\x18\x05 \x01(\x0c\x12\x11\n\tsignature\x18\x06 \x01(\x0c\x12\x14\n\x0csigner_proto\x18\x07 \x01(\t\x12\x10\n\x08init_msg\x18\x08 \x01(\x0c\x12\x0b\n\x03\x63sr\x18\t \x01(\x0c\x12\x13\n\x0binvite_code\x18\n \x01(\t\x12.\n\x0bstartupmsgs\x18\x03 \x03(\x0b\x32\x19.scheduler.StartupMessage\"[\n\x14RegistrationResponse\x12\x13\n\x0b\x64\x65vice_cert\x18\x01 \x01(\t\x12\x12\n\ndevice_key\x18\x02 \x01(\t\x12\x0c\n\x04rune\x18\x03 \x01(\t\x12\x0c\n\x04\x61uth\x18\x04 \x01(\x0c\"\"\n\x0fScheduleRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\x0c\"0\n\x0fNodeInfoRequest\x12\x0f\n\x07node_id\x18\x01 \x01(\x0c\x12\x0c\n\x04wait\x18\x02 \x01(\x08\"5\n\x10NodeInfoResponse\x12\x0f\n\x07node_id\x18\x01 \x01(\x0c\x12\x10\n\x08grpc_uri\x18\x02 \x01(\t\"U\n\x0fRecoveryRequest\x12\x11\n\tchallenge\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\x12\x0f\n\x07node_id\x18\x03 \x01(\x0c\x12\x0b\n\x03\x63sr\x18\t \x01(\x0c\"W\n\x10RecoveryResponse\x12\x13\n\x0b\x64\x65vice_cert\x18\x01 \x01(\t\x12\x12\n\ndevice_key\x18\x02 \x01(\t\x12\x0c\n\x04rune\x18\x03 \x01(\t\x12\x0c\n\x04\x61uth\x18\x04 \x01(\x0c\"m\n\x0eUpgradeRequest\x12\x16\n\x0esigner_version\x18\x01 \x01(\t\x12\x13\n\x07initmsg\x18\x02 \x01(\x0c\x42\x02\x18\x01\x12.\n\x0bstartupmsgs\x18\x03 \x03(\x0b\x32\x19.scheduler.StartupMessage\"&\n\x0fUpgradeResponse\x12\x13\n\x0bold_version\x18\x01 \x01(\t\"3\n\x0eStartupMessage\x12\x0f\n\x07request\x18\x01 \x01(\x0c\x12\x10\n\x08response\x18\x02 \x01(\x0c\"\x18\n\x16ListInviteCodesRequest\"J\n\x17ListInviteCodesResponse\x12/\n\x10invite_code_list\x18\x01 \x03(\x0b\x32\x15.scheduler.InviteCode\"/\n\nInviteCode\x12\x0c\n\x04\x63ode\x18\x01 \x01(\t\x12\x13\n\x0bis_redeemed\x18\x02 \x01(\x08\"\x13\n\x11\x45xportNodeRequest\"!\n\x12\x45xportNodeResponse\x12\x0b\n\x03url\x18\x01 \x01(\t\"\\\n\x0fSignerRejection\x12\x0b\n\x03msg\x18\x01 \x01(\t\x12\'\n\x07request\x18\x02 \x01(\x0b\x32\x16.greenlight.HsmRequest\x12\x13\n\x0bgit_version\x18\x03 \x01(\t\"g\n\x11PairDeviceRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0b\n\x03\x63sr\x18\x02 \x01(\x0c\x12\x13\n\x0b\x64\x65vice_name\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x65sc\x18\x04 \x01(\t\x12\x0e\n\x06restrs\x18\x05 \x01(\t\"m\n\x12PairDeviceResponse\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65vice_cert\x18\x02 \x01(\t\x12\x12\n\ndevice_key\x18\x03 \x01(\t\x12\x0c\n\x04rune\x18\x04 \x01(\t\x12\x0c\n\x04\x61uth\x18\x05 \x01(\x0c\"\x19\n\tPairingQr\x12\x0c\n\x04\x64\x61ta\x18\x01 \x01(\t\"+\n\x15GetPairingDataRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\"l\n\x16GetPairingDataResponse\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0b\n\x03\x63sr\x18\x02 \x01(\x0c\x12\x13\n\x0b\x64\x65vice_name\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x65sc\x18\x04 \x01(\t\x12\x0e\n\x06restrs\x18\x05 \x01(\t\"\x9f\x01\n\x15\x41pprovePairingRequest\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\x12\x0f\n\x07node_id\x18\x03 \x01(\x0c\x12\x13\n\x0b\x64\x65vice_name\x18\x04 \x01(\t\x12\x0e\n\x06restrs\x18\x05 \x01(\t\x12\x0b\n\x03sig\x18\x06 \x01(\x0c\x12\x0e\n\x06pubkey\x18\x07 \x01(\x0c\x12\x0c\n\x04rune\x18\x08 \x01(\t\"K\n\x16\x41pprovePairingResponse\x12\x12\n\nsession_id\x18\x01 \x01(\t\x12\x0f\n\x07node_id\x18\x02 \x01(\x0c\x12\x0c\n\x04rune\x18\x03 \x01(\t\"\x07\n\x05\x45mpty\"k\n\rSignerRequest\x12\x12\n\nrequest_id\x18\x01 \x01(\r\x12;\n\x0f\x61pprove_pairing\x18\x02 \x01(\x0b\x32 .scheduler.ApprovePairingRequestH\x00\x42\t\n\x07request\"\x91\x01\n\x0eSignerResponse\x12\x12\n\nrequest_id\x18\x01 \x01(\r\x12!\n\x05\x65mpty\x18\x02 \x01(\x0b\x32\x10.scheduler.EmptyH\x00\x12<\n\x0f\x61pprove_pairing\x18\x03 \x01(\x0b\x32!.scheduler.ApprovePairingResponseH\x00\x42\n\n\x08response*+\n\x0e\x43hallengeScope\x12\x0c\n\x08REGISTER\x10\x00\x12\x0b\n\x07RECOVER\x10\x01\x32\xc3\x05\n\tScheduler\x12M\n\x08Register\x12\x1e.scheduler.RegistrationRequest\x1a\x1f.scheduler.RegistrationResponse\"\x00\x12\x44\n\x07Recover\x12\x1a.scheduler.RecoveryRequest\x1a\x1b.scheduler.RecoveryResponse\"\x00\x12K\n\x0cGetChallenge\x12\x1b.scheduler.ChallengeRequest\x1a\x1c.scheduler.ChallengeResponse\"\x00\x12\x45\n\x08Schedule\x12\x1a.scheduler.ScheduleRequest\x1a\x1b.scheduler.NodeInfoResponse\"\x00\x12H\n\x0bGetNodeInfo\x12\x1a.scheduler.NodeInfoRequest\x1a\x1b.scheduler.NodeInfoResponse\"\x00\x12G\n\x0cMaybeUpgrade\x12\x19.scheduler.UpgradeRequest\x1a\x1a.scheduler.UpgradeResponse\"\x00\x12Z\n\x0fListInviteCodes\x12!.scheduler.ListInviteCodesRequest\x1a\".scheduler.ListInviteCodesResponse\"\x00\x12K\n\nExportNode\x12\x1c.scheduler.ExportNodeRequest\x1a\x1d.scheduler.ExportNodeResponse\"\x00\x12Q\n\x14SignerRequestsStream\x12\x19.scheduler.SignerResponse\x1a\x18.scheduler.SignerRequest\"\x00(\x01\x30\x01\x32Q\n\x05\x44\x65\x62ug\x12H\n\x15ReportSignerRejection\x12\x1a.scheduler.SignerRejection\x1a\x11.greenlight.Empty\"\x00\x32\xf7\x01\n\x07Pairing\x12K\n\nPairDevice\x12\x1c.scheduler.PairDeviceRequest\x1a\x1d.scheduler.PairDeviceResponse\"\x00\x12W\n\x0eGetPairingData\x12 .scheduler.GetPairingDataRequest\x1a!.scheduler.GetPairingDataResponse\"\x00\x12\x46\n\x0e\x41pprovePairing\x12 .scheduler.ApprovePairingRequest\x1a\x10.scheduler.Empty\"\x00\x62\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'scheduler_pb2', _globals) if _descriptor._USE_C_DESCRIPTORS == False: DESCRIPTOR._options = None - _UPGRADEREQUEST.fields_by_name['initmsg']._options = None - _UPGRADEREQUEST.fields_by_name['initmsg']._serialized_options = b'\030\001' - _globals['_CHALLENGESCOPE']._serialized_start=1872 - _globals['_CHALLENGESCOPE']._serialized_end=1915 + _globals['_UPGRADEREQUEST'].fields_by_name['initmsg']._options = None + _globals['_UPGRADEREQUEST'].fields_by_name['initmsg']._serialized_options = b'\030\001' + _globals['_CHALLENGESCOPE']._serialized_start=2222 + _globals['_CHALLENGESCOPE']._serialized_end=2265 _globals['_CHALLENGEREQUEST']._serialized_start=48 _globals['_CHALLENGEREQUEST']._serialized_end=125 _globals['_CHALLENGERESPONSE']._serialized_start=127 @@ -72,13 +73,19 @@ _globals['_GETPAIRINGDATARESPONSE']._serialized_start=1607 _globals['_GETPAIRINGDATARESPONSE']._serialized_end=1715 _globals['_APPROVEPAIRINGREQUEST']._serialized_start=1718 - _globals['_APPROVEPAIRINGREQUEST']._serialized_end=1861 - _globals['_EMPTY']._serialized_start=1863 - _globals['_EMPTY']._serialized_end=1870 - _globals['_SCHEDULER']._serialized_start=1918 - _globals['_SCHEDULER']._serialized_end=2542 - _globals['_DEBUG']._serialized_start=2544 - _globals['_DEBUG']._serialized_end=2625 - _globals['_PAIRING']._serialized_start=2628 - _globals['_PAIRING']._serialized_end=2875 + _globals['_APPROVEPAIRINGREQUEST']._serialized_end=1877 + _globals['_APPROVEPAIRINGRESPONSE']._serialized_start=1879 + _globals['_APPROVEPAIRINGRESPONSE']._serialized_end=1954 + _globals['_EMPTY']._serialized_start=1956 + _globals['_EMPTY']._serialized_end=1963 + _globals['_SIGNERREQUEST']._serialized_start=1965 + _globals['_SIGNERREQUEST']._serialized_end=2072 + _globals['_SIGNERRESPONSE']._serialized_start=2075 + _globals['_SIGNERRESPONSE']._serialized_end=2220 + _globals['_SCHEDULER']._serialized_start=2268 + _globals['_SCHEDULER']._serialized_end=2975 + _globals['_DEBUG']._serialized_start=2977 + _globals['_DEBUG']._serialized_end=3058 + _globals['_PAIRING']._serialized_start=3061 + _globals['_PAIRING']._serialized_end=3308 # @@protoc_insertion_point(module_scope) diff --git a/libs/gl-client-py/glclient/scheduler_pb2.pyi b/libs/gl-client-py/glclient/scheduler_pb2.pyi index 1248142b2..4d1399eaa 100644 --- a/libs/gl-client-py/glclient/scheduler_pb2.pyi +++ b/libs/gl-client-py/glclient/scheduler_pb2.pyi @@ -652,6 +652,7 @@ class ApprovePairingRequest(google.protobuf.message.Message): DEVICE_NAME_FIELD_NUMBER: builtins.int RESTRS_FIELD_NUMBER: builtins.int SIG_FIELD_NUMBER: builtins.int + PUBKEY_FIELD_NUMBER: builtins.int RUNE_FIELD_NUMBER: builtins.int session_id: builtins.str timestamp: builtins.int @@ -666,9 +667,13 @@ class ApprovePairingRequest(google.protobuf.message.Message): subjects CN field: CN=/users//. """ restrs: builtins.str - """The restrictions need a the pubkey set.""" + """The restrictions need a pubkey set.""" sig: builtins.bytes """The signature of the above to ensure data integrity.""" + pubkey: builtins.bytes + """The public key corresponding to the private key that was used + to sign the request and that is part of the rune; + """ rune: builtins.str """The rune of the old device with a pubkey field corresponding to the signature above. Used to authorize the approval request. @@ -682,12 +687,34 @@ class ApprovePairingRequest(google.protobuf.message.Message): device_name: builtins.str = ..., restrs: builtins.str = ..., sig: builtins.bytes = ..., + pubkey: builtins.bytes = ..., rune: builtins.str = ..., ) -> None: ... - def ClearField(self, field_name: typing_extensions.Literal["device_name", b"device_name", "node_id", b"node_id", "restrs", b"restrs", "rune", b"rune", "session_id", b"session_id", "sig", b"sig", "timestamp", b"timestamp"]) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["device_name", b"device_name", "node_id", b"node_id", "pubkey", b"pubkey", "restrs", b"restrs", "rune", b"rune", "session_id", b"session_id", "sig", b"sig", "timestamp", b"timestamp"]) -> None: ... global___ApprovePairingRequest = ApprovePairingRequest +@typing_extensions.final +class ApprovePairingResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + SESSION_ID_FIELD_NUMBER: builtins.int + NODE_ID_FIELD_NUMBER: builtins.int + RUNE_FIELD_NUMBER: builtins.int + session_id: builtins.str + node_id: builtins.bytes + rune: builtins.str + def __init__( + self, + *, + session_id: builtins.str = ..., + node_id: builtins.bytes = ..., + rune: builtins.str = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal["node_id", b"node_id", "rune", b"rune", "session_id", b"session_id"]) -> None: ... + +global___ApprovePairingResponse = ApprovePairingResponse + @typing_extensions.final class Empty(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor @@ -697,3 +724,49 @@ class Empty(google.protobuf.message.Message): ) -> None: ... global___Empty = Empty + +@typing_extensions.final +class SignerRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + REQUEST_ID_FIELD_NUMBER: builtins.int + APPROVE_PAIRING_FIELD_NUMBER: builtins.int + request_id: builtins.int + @property + def approve_pairing(self) -> global___ApprovePairingRequest: ... + def __init__( + self, + *, + request_id: builtins.int = ..., + approve_pairing: global___ApprovePairingRequest | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["approve_pairing", b"approve_pairing", "request", b"request"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["approve_pairing", b"approve_pairing", "request", b"request", "request_id", b"request_id"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["request", b"request"]) -> typing_extensions.Literal["approve_pairing"] | None: ... + +global___SignerRequest = SignerRequest + +@typing_extensions.final +class SignerResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + REQUEST_ID_FIELD_NUMBER: builtins.int + EMPTY_FIELD_NUMBER: builtins.int + APPROVE_PAIRING_FIELD_NUMBER: builtins.int + request_id: builtins.int + @property + def empty(self) -> global___Empty: ... + @property + def approve_pairing(self) -> global___ApprovePairingResponse: ... + def __init__( + self, + *, + request_id: builtins.int = ..., + empty: global___Empty | None = ..., + approve_pairing: global___ApprovePairingResponse | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["approve_pairing", b"approve_pairing", "empty", b"empty", "response", b"response"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["approve_pairing", b"approve_pairing", "empty", b"empty", "request_id", b"request_id", "response", b"response"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["response", b"response"]) -> typing_extensions.Literal["empty", "approve_pairing"] | None: ... + +global___SignerResponse = SignerResponse diff --git a/libs/gl-client-py/glclient/scheduler_pb2_grpc.py b/libs/gl-client-py/glclient/scheduler_pb2_grpc.py index f7083c94a..81884e7dd 100644 --- a/libs/gl-client-py/glclient/scheduler_pb2_grpc.py +++ b/libs/gl-client-py/glclient/scheduler_pb2_grpc.py @@ -88,6 +88,11 @@ def __init__(self, channel): request_serializer=scheduler__pb2.ExportNodeRequest.SerializeToString, response_deserializer=scheduler__pb2.ExportNodeResponse.FromString, ) + self.SignerRequestsStream = channel.stream_stream( + '/scheduler.Scheduler/SignerRequestsStream', + request_serializer=scheduler__pb2.SignerResponse.SerializeToString, + response_deserializer=scheduler__pb2.SignerRequest.FromString, + ) class SchedulerServicer(object): @@ -270,6 +275,19 @@ def ExportNode(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def SignerRequestsStream(self, request_iterator, context): + """Attaches a Signer to via a bidirectional stream to the + scheduler. This is a communication channel between greenlight + and the signing device that is used for requests that are not + part of the node api. + + The stream is used to hand out the ApprovePairingRequests that + the signer answers with a ApprovePairingResponse. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def add_SchedulerServicer_to_server(servicer, server): rpc_method_handlers = { @@ -313,6 +331,11 @@ def add_SchedulerServicer_to_server(servicer, server): request_deserializer=scheduler__pb2.ExportNodeRequest.FromString, response_serializer=scheduler__pb2.ExportNodeResponse.SerializeToString, ), + 'SignerRequestsStream': grpc.stream_stream_rpc_method_handler( + servicer.SignerRequestsStream, + request_deserializer=scheduler__pb2.SignerResponse.FromString, + response_serializer=scheduler__pb2.SignerRequest.SerializeToString, + ), } generic_handler = grpc.method_handlers_generic_handler( 'scheduler.Scheduler', rpc_method_handlers) @@ -492,6 +515,23 @@ def ExportNode(request, options, channel_credentials, insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + @staticmethod + def SignerRequestsStream(request_iterator, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.stream_stream(request_iterator, target, '/scheduler.Scheduler/SignerRequestsStream', + scheduler__pb2.SignerResponse.SerializeToString, + scheduler__pb2.SignerRequest.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + class DebugStub(object): """A service to collect debugging information from clients. diff --git a/libs/gl-client-py/tests/test_pairing.py b/libs/gl-client-py/tests/test_pairing.py index af0703901..63084aa25 100644 --- a/libs/gl-client-py/tests/test_pairing.py +++ b/libs/gl-client-py/tests/test_pairing.py @@ -1,8 +1,12 @@ +import time from gltesting.fixtures import * from glclient import NewDevicePairingClient, AttestationDevicePairingClient from test_scheduler import tls, signer, sclient def test_pairing_session(scheduler, nobody_id, sclient, signer, tls): + # Run the signer in the background + signer.run_in_thread() + name = "new_device" desc = "my description" restrs = "method^list" @@ -17,6 +21,7 @@ def test_pairing_session(scheduler, nobody_id, sclient, signer, tls): # register an "attestation device" res = sclient.register(signer) + sclient.schedule() ac = AttestationDevicePairingClient(auth=res.auth) # check for pairing data. @@ -33,7 +38,7 @@ def test_pairing_session(scheduler, nobody_id, sclient, signer, tls): # and with our rune. ac.approve_pairing( m.session_id, - sclient.node_id, + signer.node_id(), m.device_name, m.restrs ) @@ -46,6 +51,9 @@ def test_pairing_session(scheduler, nobody_id, sclient, signer, tls): # assert(m.rune) fixme: enable once we pass back a rune during the tests. assert(m.auth) + signer.shutdown() + # FIXME: add a blocking shutdown call that waits for the signer to shutdown. + time.sleep(2) def test_paring_data_validation(scheduler, sclient, signer): """A simple test to ensure that data validation works as intended. diff --git a/libs/gl-client/Cargo.toml b/libs/gl-client/Cargo.toml index 21d0b6c8b..b583b19d1 100644 --- a/libs/gl-client/Cargo.toml +++ b/libs/gl-client/Cargo.toml @@ -2,10 +2,7 @@ name = "gl-client" version = "0.1.9" edition = "2018" -authors = [ - "Christian Decker", - "The Greenlight Team" -] +authors = ["Christian Decker", "The Greenlight Team"] description = "Client library for Greenlight, and basis for language bindings." repository = "https://github.com/Blockstream/greenlight" license = "MIT" @@ -33,19 +30,23 @@ picky-asn1-der = "0.4" pin-project = "1.1.3" prost = "0.11" prost-derive = "0.11" -reqwest = {version="^0.11", features=["json", "rustls-tls-native-roots"], default-features = false} +reqwest = { version = "^0.11", features = [ + "json", + "rustls-tls-native-roots", +], default-features = false } ring = "~0.16.20" runeauth = "0.1" rustls-pemfile = "1.0.3" sha256 = "1.1.4" tokio = { version = "1", features = ["full"] } +tokio-stream = "0.1" tonic = { version = "^0.8", features = ["tls", "transport"] } tower = { version = "0.4" } -rcgen = { version = "0.10.0", features = ["pem", "x509-parser"]} +rcgen = { version = "0.10.0", features = ["pem", "x509-parser"] } tempfile = "3.3.0" url = "2.4.0" bitcoin = "^0" -serde = { version = "1", features = [ "derive" ] } +serde = { version = "1", features = ["derive"] } vls-core = { workspace = true } vls-persist = { workspace = true } vls-protocol-signer = { workspace = true } @@ -62,9 +63,9 @@ futures = "0.3.28" async-trait = "0.1.72" rand = "0.8.5" -uuid = {version = "1.4.0", features=["serde"]} +uuid = { version = "1.4.0", features = ["serde"] } time = { version = "0.3", features = ["macros"] } [build-dependencies] tonic-build = "^0.8" -serde = { version = "1", features = [ "derive" ] } +serde = { version = "1", features = ["derive"] } diff --git a/libs/gl-client/src/pairing/attestation_device.rs b/libs/gl-client/src/pairing/attestation_device.rs index 24c6dee94..762e387a9 100644 --- a/libs/gl-client/src/pairing/attestation_device.rs +++ b/libs/gl-client/src/pairing/attestation_device.rs @@ -14,7 +14,7 @@ use picky::{pem::Pem, x509::Csr}; use picky_asn1_x509::{PublicKey, SubjectPublicKeyInfo}; use ring::{ rand, - signature::{self, EcdsaKeyPair}, + signature::{self, EcdsaKeyPair, KeyPair}, }; use runeauth::Rune; use rustls_pemfile as pemfile; @@ -161,6 +161,7 @@ impl Client { device_name: device_name.to_string(), restrs: restrs.to_string(), sig: sig, + pubkey: kp.public_key().as_ref().to_vec(), rune: self.rune.to_base64(), }) .await? diff --git a/libs/gl-client/src/signer/mod.rs b/libs/gl-client/src/signer/mod.rs index e75d86947..fd44cc7b3 100644 --- a/libs/gl-client/src/signer/mod.rs +++ b/libs/gl-client/src/signer/mod.rs @@ -1,4 +1,8 @@ -use crate::pb::scheduler::{scheduler_client::SchedulerClient, NodeInfoRequest, UpgradeRequest}; +use crate::pb::scheduler::{ + self, scheduler_client::SchedulerClient, NodeInfoRequest, UpgradeRequest, +}; +use crate::pb::scheduler::{ApprovePairingRequest, ApprovePairingResponse, SignerResponse}; +use crate::pb::PendingRequest; /// The core signer system. It runs in a dedicated thread or using the /// caller thread, streaming incoming requests, verifying them, /// signing if ok, and then shipping the response to the node. @@ -18,6 +22,7 @@ use lightning_signer::node::NodeServices; use lightning_signer::policy::filter::FilterRule; use lightning_signer::util::crypto_utils; use log::{debug, info, trace, warn}; +use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_FIXED}; use runeauth::{Condition, MapChecker, Restriction, Rune, RuneError}; use std::collections::HashMap; use std::convert::TryFrom; @@ -26,6 +31,7 @@ use std::sync::Arc; use std::sync::Mutex; use tokio::sync::{broadcast, mpsc}; use tokio::time::{sleep, Duration}; +use tokio_stream::wrappers::ReceiverStream; use tonic::transport::{Endpoint, Uri}; use tonic::{Code, Request}; use vls_protocol::msgs::{DeBolt, HsmdInitReplyV4}; @@ -246,7 +252,6 @@ impl Signer { requests: Vec, ) -> Vec> { // Filter out requests lacking a required field. They are unverifiable anyway. - use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_FIXED}; // Todo: partition results to provide more detailed errors. requests .into_iter() @@ -626,7 +631,7 @@ impl Signer { &scheduler_uri ); - let channel = Endpoint::from_shared(scheduler_uri)? + let channel = Endpoint::from_shared(scheduler_uri.clone())? .tls_config(self.tls.inner.clone())? .tcp_keepalive(Some(crate::TCP_KEEPALIVE)) .http2_keep_alive_interval(crate::TCP_KEEPALIVE) @@ -671,9 +676,10 @@ impl Signer { let shutdown = self.shutdown_connector(shutdown, tx.clone()); let node_runner = self.run_forever_node(tx.subscribe(), scheduler.clone()); + let scheduler_runner = self.run_forever_scheduler(tx.subscribe(), scheduler.clone()); - let _ = tokio::join!(node_runner, shutdown); - todo!() + let _ = tokio::join!(node_runner, scheduler_runner, shutdown); + Ok(()) } async fn shutdown_connector( @@ -745,6 +751,150 @@ impl Signer { Ok(()) } + async fn run_forever_scheduler( + &self, + mut shutdown: broadcast::Receiver<()>, + mut scheduler: SchedulerClient, + ) -> Result<(), anyhow::Error> { + loop { + let (sender, rx) = mpsc::channel(1); + let outbound = ReceiverStream::new(rx); + let inbound_future = scheduler.signer_requests_stream(outbound); + + let mut stream = tokio::select! { + biased; + _ = shutdown.recv() => { + debug!("Received the signal to exit the signer loop"); + return Ok(()); + } + stream = inbound_future => match stream { + Ok(s) => s.into_inner(), + Err(e) => { + debug!("Failed to start stream: {}", e); + sleep(Duration::from_secs(5)).await; + continue; + }, + }, + }; + + debug!("Starting to stream signer requests from scheduler"); + + loop { + tokio::select! { + biased; + _ = shutdown.recv() => { + debug!("Received the signal to exit the signer loop"); + return Ok(()); + }, + msg = stream.message() => match msg { + Ok(Some(msg)) => { + let req_id = msg.request_id; + debug!("Processing scheduler request {}", req_id); + match msg.request { + Some(scheduler::signer_request::Request::ApprovePairing(req)) => self.process_pairing_approval(req_id, req, sender.clone()).await, + None => { + debug!("Received an empty signing request"); + } + }; + }, + Ok(None) => { + debug!("End of stream, this should not happen by the server"); + break; + }, + Err(e) => { + debug!("Got an error from the scheduler {}", e); + break; + }, + }, + }; + } + } + } + + async fn process_pairing_approval( + &self, + req_id: u32, + req: ApprovePairingRequest, + stream: mpsc::Sender, + ) -> () { + let mut data = vec![]; + data.put(req.session_id.as_bytes()); + data.put_u64(req.timestamp); + data.put(&req.node_id[..]); + data.put(req.device_name.as_bytes()); + data.put(req.restrs.as_bytes()); + + // Check that the signature matches + let pk = UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, req.pubkey.clone()); + if pk.verify(&data, &req.sig).is_err() { + debug!("Got an invalid signature processing pairing approval"); + return; + } + + // Decode rune as we expect it to be bytes in the pending request. + let rune = match general_purpose::URL_SAFE.decode(req.rune.clone()) { + Ok(r) => r, + Err(e) => { + debug!("Could not decode rune processing pairing approval {}", e); + return; + } + }; + + // Check that the rune matches + match self.verify_rune(PendingRequest { + request: vec![], + uri: "/cln.Node/ApprovePairing".to_string(), + signature: req.sig, + pubkey: req.pubkey, + timestamp: req.timestamp, + rune, + }) { + Ok(_) => (), + Err(e) => { + debug!( + "Got an invalid rune {} processing pairing approval {}", + req.rune, e + ); + return; + } + }; + + let restrs: Vec> = req + .restrs + .split('&') + .map(|s| s.split('|').collect::>()) + .collect(); + + // Create the rune that approves pairing + let rune = match self.create_rune(None, restrs) { + Ok(r) => r, + Err(e) => { + debug!("Could not create rune during pairing approval {}", e); + return; + } + }; + + match stream + .send(SignerResponse { + request_id: req_id, + response: Some(scheduler::signer_response::Response::ApprovePairing( + ApprovePairingResponse { + session_id: req.session_id, + node_id: req.node_id, + rune: rune, + }, + )), + }) + .await + { + Ok(_) => (), + Err(e) => debug!( + "Could not respond to stream during pairing approval {:?}", + e + ), + }; + } + // TODO See comment on `sign_device_key`. pub fn sign_challenge(&self, challenge: Vec) -> Result, anyhow::Error> { if challenge.len() != 32 { @@ -1117,8 +1267,8 @@ mod tests { let new_rune = signer .create_rune(Some(rune), vec![vec!["method^get"]]) .unwrap(); - let rs = Rune::from_base64(&new_rune).unwrap().to_string(); - assert!(rs.contains("0-gl0&pubkey=000000&method^get")) + let stream = Rune::from_base64(&new_rune).unwrap().to_string(); + assert!(stream.contains("0-gl0&pubkey=000000&method^get")) } #[test] diff --git a/libs/gl-testing/gltesting/scheduler.py b/libs/gl-testing/gltesting/scheduler.py index 17d3ccd2d..0944dac55 100644 --- a/libs/gl-testing/gltesting/scheduler.py +++ b/libs/gl-testing/gltesting/scheduler.py @@ -10,10 +10,12 @@ from dataclasses import dataclass from pathlib import Path from threading import Condition -from typing import List, Optional, Dict, Any +from typing import List, Optional, Dict, Any, Generator import purerpc import anyio +import asyncio +from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream from glclient import greenlight_pb2 as greenlightpb from glclient import scheduler_pb2 as schedpb from pyln.client import LightningRpc @@ -98,7 +100,9 @@ def __init__( self.invite_codes: List[str] = [] self.received_invite_code = None self.debugger = DebugServicer() - self.pairings = PairingServicer() + self.pairing_tx_in, self.pairing_rx_in = anyio.create_memory_object_stream() + self.pairing_tx_out, self.paring_rx_out = anyio.create_memory_object_stream() + self.pairings = PairingServicer(stream_out=self.pairing_tx_in, stream_in=self.paring_rx_out) if node_directory is not None: self.node_directory = node_directory @@ -361,6 +365,26 @@ async def ListInviteCodes(self, req) -> schedpb.ListInviteCodesResponse: codes = [schedpb.InviteCode(**c) for c in self.invite_codes] return schedpb.ListInviteCodesResponse(invite_code_list=codes) + async def _read_from_signer(self, req): + async for message in req: + await self.pairing_tx_out.send(message) + + async def SignerRequestsStream(self, request): + print(f"Signer attached to schedulers signer_request_stream") + asyncio.create_task(self._read_from_signer(request)) + + async with self.pairing_rx_in: + async for data in self.pairing_rx_in: + yield schedpb.SignerRequest(request_id=1, approve_pairing=schedpb.ApprovePairingRequest( + session_id=data.session_id, + timestamp=data.timestamp, + node_id=data.node_id, + device_name=data.device_name, + restrs=data.restrs, + sig=data.sig, + pubkey=data.pubkey, + rune=data.rune)) + class DebugServicer(schedgrpc.DebugServicer): """Collects and analyzes rejected signer requests.""" @@ -374,11 +398,12 @@ async def ReportSignerRejection(self, report): class PairingServicer(schedgrpc.PairingServicer): """Mocks a pairing backend for local testing""" - def __init__(self): + def __init__(self, stream_out: MemoryObjectSendStream[Any]=None, stream_in: MemoryObjectReceiveStream[Any]=None): self.sessions: Dict[int, Dict[str, str | bytes]] = {} - self.send_stream, self.recv_stream = anyio.create_memory_object_stream() + self.stream_out = stream_out + self.stream_in = stream_in - async def recv_once(self, stream: anyio.streams.memory.MemoryObjectReceiveStream[Any]): + async def recv_once(self, stream: MemoryObjectReceiveStream[Any]): async with stream: data = await stream.receive() return data @@ -392,15 +417,14 @@ async def PairDevice(self, req: schedpb.PairDeviceRequest): } self.sessions[req.session_id] = data - # Wait for the Approval of an old device. - await self.recv_once(self.recv_stream) + # Wait for the Approval from the signer. + await self.recv_once(self.stream_in) device_cert = certs.gencert_from_csr(req.csr, recover=False, pairing=True) return schedpb.PairDeviceResponse( session_id=req.session_id, device_cert=device_cert) - async def GetPairingData(self, req: schedpb.GetPairingDataRequest): data = self.sessions[req.session_id] return schedpb.GetPairingDataResponse( @@ -412,10 +436,8 @@ async def GetPairingData(self, req: schedpb.GetPairingDataRequest): ) async def ApprovePairing(self, req): - async with self.send_stream as send_stream: - await send_stream.send(req) - + await self.stream_out.send(req) return schedpb.Empty() - + Scheduler = AsyncScheduler diff --git a/libs/gl-testing/gltesting/scheduler_grpc.py b/libs/gl-testing/gltesting/scheduler_grpc.py index 824fcbce3..8198dcd75 100644 --- a/libs/gl-testing/gltesting/scheduler_grpc.py +++ b/libs/gl-testing/gltesting/scheduler_grpc.py @@ -27,6 +27,9 @@ async def ListInviteCodes(self, input_message): async def ExportNode(self, input_message): raise NotImplementedError() + + async def SignerRequestsStream(self, input_message): + raise NotImplementedError @property def service(self) -> purerpc.Service: @@ -105,6 +108,15 @@ def service(self) -> purerpc.Service: scheduler__pb2.ExportNodeResponse, ) ) + service_obj.add_method( + "SignerRequestsStream", + self.SignerRequestsStream, + purerpc.RPCSignature( + purerpc.Cardinality.STREAM_STREAM, + scheduler__pb2.SignerResponse, + scheduler__pb2.SignerRequest, + ) + ) return service_obj @@ -178,6 +190,14 @@ def __init__(self, channel): scheduler__pb2.ExportNodeResponse, ) ) + self.SignerRequestsStream = self._client.get_method_stub( + "SignerRequestsStream", + purerpc.RPCSignature( + purerpc.Cardinality.STREAM_STEAM, + scheduler__pb2.SignerResponse, + scheduler__pb2.SignerRequest, + ) + ) class DebugServicer(purerpc.Servicer): @@ -226,7 +246,7 @@ async def GetPairingData(self, input_message): async def ApproveSession(self, input_message): raise NotImplementedError() - + @property def service(self) -> purerpc.Service: service_obj = purerpc.Service( diff --git a/libs/proto/scheduler.proto b/libs/proto/scheduler.proto index 8f99d4723..ebbad7f9a 100644 --- a/libs/proto/scheduler.proto +++ b/libs/proto/scheduler.proto @@ -148,6 +148,15 @@ service Scheduler { // being replayed) and loss of funds (see CLN Backups // documentation for more information) rpc ExportNode(ExportNodeRequest) returns (ExportNodeResponse) {} + + // Attaches a Signer to via a bidirectional stream to the + // scheduler. This is a communication channel between greenlight + // and the signing device that is used for requests that are not + // part of the node api. + // + // The stream is used to hand out the ApprovePairingRequests that + // the signer answers with a ApprovePairingResponse. + rpc SignerRequestsStream(stream SignerResponse) returns (stream SignerRequest) {}; }; // A service to collect debugging information from clients. @@ -467,15 +476,40 @@ message ApprovePairingRequest { // subjects CN field: CN=/users//. string device_name = 4; - // The restrictions need a the pubkey set. + // The restrictions need a pubkey set. string restrs = 5; // The signature of the above to ensure data integrity. - bytes sig = 6; + bytes sig = 6; + + // The public key corresponding to the private key that was used + // to sign the request and that is part of the rune; + bytes pubkey = 7; // The rune of the old device with a pubkey field corresponding to // the signature above. Used to authorize the approval request. - string rune = 7; + string rune = 8; +} + +message ApprovePairingResponse { + string session_id = 1; + bytes node_id = 2; + string rune =3; +} + +message Empty {} + +message SignerRequest { + uint32 request_id = 1; + oneof request { + ApprovePairingRequest approve_pairing = 2; + } } -message Empty {} \ No newline at end of file +message SignerResponse { + uint32 request_id = 1; + oneof response { + Empty empty = 2; + ApprovePairingResponse approve_pairing = 3; + } +} \ No newline at end of file