flwr 1.22.0__py3-none-any.whl → 1.24.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (301) hide show
  1. flwr/__init__.py +16 -5
  2. flwr/app/error.py +2 -2
  3. flwr/app/exception.py +3 -3
  4. flwr/cli/app.py +34 -1
  5. flwr/cli/app_cmd/__init__.py +23 -0
  6. flwr/cli/app_cmd/publish.py +285 -0
  7. flwr/cli/app_cmd/review.py +252 -0
  8. flwr/cli/auth_plugin/__init__.py +15 -6
  9. flwr/cli/auth_plugin/auth_plugin.py +94 -0
  10. flwr/cli/auth_plugin/noop_auth_plugin.py +101 -0
  11. flwr/cli/auth_plugin/oidc_cli_plugin.py +46 -32
  12. flwr/cli/build.py +166 -53
  13. flwr/cli/{cli_user_auth_interceptor.py → cli_account_auth_interceptor.py} +29 -11
  14. flwr/cli/config_utils.py +101 -13
  15. flwr/cli/federation/__init__.py +24 -0
  16. flwr/cli/federation/ls.py +140 -0
  17. flwr/cli/federation/show.py +317 -0
  18. flwr/cli/install.py +91 -13
  19. flwr/cli/log.py +54 -11
  20. flwr/cli/login/login.py +41 -27
  21. flwr/cli/ls.py +177 -133
  22. flwr/cli/new/new.py +175 -40
  23. flwr/cli/new/templates/app/code/task.pytorch.py.tpl +1 -0
  24. flwr/cli/new/templates/app/pyproject.baseline.toml.tpl +1 -1
  25. flwr/cli/new/templates/app/pyproject.flowertune.toml.tpl +1 -1
  26. flwr/cli/new/templates/app/pyproject.huggingface.toml.tpl +1 -1
  27. flwr/cli/new/templates/app/pyproject.jax.toml.tpl +1 -1
  28. flwr/cli/new/templates/app/pyproject.mlx.toml.tpl +1 -1
  29. flwr/cli/new/templates/app/pyproject.numpy.toml.tpl +1 -1
  30. flwr/cli/new/templates/app/pyproject.pytorch.toml.tpl +3 -3
  31. flwr/cli/new/templates/app/pyproject.pytorch_legacy_api.toml.tpl +1 -1
  32. flwr/cli/new/templates/app/pyproject.sklearn.toml.tpl +1 -1
  33. flwr/cli/new/templates/app/pyproject.tensorflow.toml.tpl +2 -2
  34. flwr/cli/new/templates/app/pyproject.xgboost.toml.tpl +1 -1
  35. flwr/cli/pull.py +12 -7
  36. flwr/cli/run/run.py +82 -31
  37. flwr/cli/run_utils.py +130 -0
  38. flwr/cli/stop.py +27 -9
  39. flwr/cli/supernode/__init__.py +25 -0
  40. flwr/cli/supernode/ls.py +268 -0
  41. flwr/cli/supernode/register.py +190 -0
  42. flwr/cli/supernode/unregister.py +140 -0
  43. flwr/cli/utils.py +464 -81
  44. flwr/client/__init__.py +2 -1
  45. flwr/client/dpfedavg_numpy_client.py +4 -1
  46. flwr/client/grpc_adapter_client/connection.py +12 -15
  47. flwr/client/grpc_rere_client/connection.py +68 -41
  48. flwr/client/grpc_rere_client/grpc_adapter.py +34 -14
  49. flwr/client/grpc_rere_client/{client_interceptor.py → node_auth_client_interceptor.py} +5 -7
  50. flwr/client/message_handler/message_handler.py +2 -2
  51. flwr/client/mod/secure_aggregation/secaggplus_mod.py +10 -8
  52. flwr/client/numpy_client.py +1 -1
  53. flwr/client/rest_client/connection.py +94 -51
  54. flwr/client/run_info_store.py +4 -5
  55. flwr/client/typing.py +1 -1
  56. flwr/clientapp/__init__.py +1 -2
  57. flwr/{client → clientapp}/client_app.py +9 -10
  58. flwr/clientapp/mod/centraldp_mods.py +16 -17
  59. flwr/clientapp/mod/localdp_mod.py +8 -9
  60. flwr/clientapp/typing.py +1 -1
  61. flwr/{client/clientapp → clientapp}/utils.py +4 -4
  62. flwr/common/address.py +1 -2
  63. flwr/common/args.py +3 -4
  64. flwr/common/config.py +13 -16
  65. flwr/common/constant.py +56 -13
  66. flwr/common/differential_privacy.py +3 -4
  67. flwr/common/event_log_plugin/event_log_plugin.py +3 -4
  68. flwr/common/exit/exit.py +15 -2
  69. flwr/common/exit/exit_code.py +39 -10
  70. flwr/common/exit/exit_handler.py +6 -2
  71. flwr/common/exit/signal_handler.py +5 -5
  72. flwr/common/grpc.py +6 -6
  73. flwr/common/inflatable_protobuf_utils.py +1 -1
  74. flwr/common/inflatable_utils.py +48 -31
  75. flwr/common/logger.py +19 -19
  76. flwr/common/message.py +4 -4
  77. flwr/common/object_ref.py +7 -7
  78. flwr/common/record/array.py +6 -6
  79. flwr/common/record/arrayrecord.py +18 -21
  80. flwr/common/record/configrecord.py +3 -3
  81. flwr/common/record/recorddict.py +5 -5
  82. flwr/common/record/typeddict.py +9 -2
  83. flwr/common/recorddict_compat.py +7 -10
  84. flwr/common/retry_invoker.py +20 -20
  85. flwr/common/secure_aggregation/crypto/symmetric_encryption.py +1 -89
  86. flwr/common/secure_aggregation/ndarrays_arithmetic.py +3 -3
  87. flwr/common/serde.py +9 -6
  88. flwr/common/serde_utils.py +2 -2
  89. flwr/common/telemetry.py +9 -5
  90. flwr/common/typing.py +59 -43
  91. flwr/compat/client/app.py +39 -38
  92. flwr/compat/client/grpc_client/connection.py +13 -13
  93. flwr/compat/server/app.py +5 -6
  94. flwr/proto/appio_pb2.py +13 -3
  95. flwr/proto/appio_pb2.pyi +134 -65
  96. flwr/proto/appio_pb2_grpc.py +20 -0
  97. flwr/proto/appio_pb2_grpc.pyi +27 -0
  98. flwr/proto/clientappio_pb2.py +17 -7
  99. flwr/proto/clientappio_pb2.pyi +15 -0
  100. flwr/proto/clientappio_pb2_grpc.py +206 -40
  101. flwr/proto/clientappio_pb2_grpc.pyi +168 -53
  102. flwr/proto/control_pb2.py +72 -40
  103. flwr/proto/control_pb2.pyi +319 -87
  104. flwr/proto/control_pb2_grpc.py +339 -28
  105. flwr/proto/control_pb2_grpc.pyi +209 -37
  106. flwr/proto/error_pb2.py +13 -3
  107. flwr/proto/error_pb2.pyi +24 -6
  108. flwr/proto/error_pb2_grpc.py +20 -0
  109. flwr/proto/error_pb2_grpc.pyi +27 -0
  110. flwr/proto/fab_pb2.py +24 -10
  111. flwr/proto/fab_pb2.pyi +68 -20
  112. flwr/proto/fab_pb2_grpc.py +20 -0
  113. flwr/proto/fab_pb2_grpc.pyi +27 -0
  114. flwr/proto/federation_pb2.py +38 -0
  115. flwr/proto/federation_pb2.pyi +56 -0
  116. flwr/proto/federation_pb2_grpc.py +24 -0
  117. flwr/proto/federation_pb2_grpc.pyi +31 -0
  118. flwr/proto/fleet_pb2.py +45 -27
  119. flwr/proto/fleet_pb2.pyi +186 -70
  120. flwr/proto/fleet_pb2_grpc.py +277 -66
  121. flwr/proto/fleet_pb2_grpc.pyi +201 -55
  122. flwr/proto/grpcadapter_pb2.py +14 -4
  123. flwr/proto/grpcadapter_pb2.pyi +38 -16
  124. flwr/proto/grpcadapter_pb2_grpc.py +35 -4
  125. flwr/proto/grpcadapter_pb2_grpc.pyi +38 -7
  126. flwr/proto/heartbeat_pb2.py +17 -7
  127. flwr/proto/heartbeat_pb2.pyi +51 -22
  128. flwr/proto/heartbeat_pb2_grpc.py +20 -0
  129. flwr/proto/heartbeat_pb2_grpc.pyi +27 -0
  130. flwr/proto/log_pb2.py +13 -3
  131. flwr/proto/log_pb2.pyi +34 -11
  132. flwr/proto/log_pb2_grpc.py +20 -0
  133. flwr/proto/log_pb2_grpc.pyi +27 -0
  134. flwr/proto/message_pb2.py +15 -5
  135. flwr/proto/message_pb2.pyi +154 -86
  136. flwr/proto/message_pb2_grpc.py +20 -0
  137. flwr/proto/message_pb2_grpc.pyi +27 -0
  138. flwr/proto/node_pb2.py +16 -4
  139. flwr/proto/node_pb2.pyi +77 -4
  140. flwr/proto/node_pb2_grpc.py +20 -0
  141. flwr/proto/node_pb2_grpc.pyi +27 -0
  142. flwr/proto/recorddict_pb2.py +13 -3
  143. flwr/proto/recorddict_pb2.pyi +184 -107
  144. flwr/proto/recorddict_pb2_grpc.py +20 -0
  145. flwr/proto/recorddict_pb2_grpc.pyi +27 -0
  146. flwr/proto/run_pb2.py +40 -31
  147. flwr/proto/run_pb2.pyi +149 -84
  148. flwr/proto/run_pb2_grpc.py +20 -0
  149. flwr/proto/run_pb2_grpc.pyi +27 -0
  150. flwr/proto/serverappio_pb2.py +13 -3
  151. flwr/proto/serverappio_pb2.pyi +32 -8
  152. flwr/proto/serverappio_pb2_grpc.py +246 -65
  153. flwr/proto/serverappio_pb2_grpc.pyi +221 -85
  154. flwr/proto/simulationio_pb2.py +16 -8
  155. flwr/proto/simulationio_pb2.pyi +15 -0
  156. flwr/proto/simulationio_pb2_grpc.py +162 -41
  157. flwr/proto/simulationio_pb2_grpc.pyi +149 -55
  158. flwr/proto/transport_pb2.py +20 -10
  159. flwr/proto/transport_pb2.pyi +249 -160
  160. flwr/proto/transport_pb2_grpc.py +35 -4
  161. flwr/proto/transport_pb2_grpc.pyi +38 -8
  162. flwr/server/app.py +173 -127
  163. flwr/server/client_manager.py +4 -5
  164. flwr/server/client_proxy.py +10 -11
  165. flwr/server/compat/app.py +4 -5
  166. flwr/server/compat/app_utils.py +2 -1
  167. flwr/server/compat/grid_client_proxy.py +10 -12
  168. flwr/server/compat/legacy_context.py +3 -4
  169. flwr/server/fleet_event_log_interceptor.py +2 -1
  170. flwr/server/grid/grid.py +2 -3
  171. flwr/server/grid/grpc_grid.py +10 -8
  172. flwr/server/grid/inmemory_grid.py +4 -4
  173. flwr/server/run_serverapp.py +2 -3
  174. flwr/server/server.py +34 -39
  175. flwr/server/server_app.py +7 -8
  176. flwr/server/server_config.py +1 -2
  177. flwr/server/serverapp/app.py +34 -28
  178. flwr/server/serverapp_components.py +4 -5
  179. flwr/server/strategy/aggregate.py +9 -8
  180. flwr/server/strategy/bulyan.py +13 -11
  181. flwr/server/strategy/dp_adaptive_clipping.py +16 -20
  182. flwr/server/strategy/dp_fixed_clipping.py +12 -17
  183. flwr/server/strategy/dpfedavg_adaptive.py +3 -4
  184. flwr/server/strategy/dpfedavg_fixed.py +6 -10
  185. flwr/server/strategy/fault_tolerant_fedavg.py +14 -13
  186. flwr/server/strategy/fedadagrad.py +18 -14
  187. flwr/server/strategy/fedadam.py +16 -14
  188. flwr/server/strategy/fedavg.py +16 -17
  189. flwr/server/strategy/fedavg_android.py +15 -15
  190. flwr/server/strategy/fedavgm.py +21 -18
  191. flwr/server/strategy/fedmedian.py +2 -3
  192. flwr/server/strategy/fedopt.py +11 -10
  193. flwr/server/strategy/fedprox.py +10 -9
  194. flwr/server/strategy/fedtrimmedavg.py +12 -11
  195. flwr/server/strategy/fedxgb_bagging.py +13 -11
  196. flwr/server/strategy/fedxgb_cyclic.py +6 -6
  197. flwr/server/strategy/fedxgb_nn_avg.py +4 -4
  198. flwr/server/strategy/fedyogi.py +16 -14
  199. flwr/server/strategy/krum.py +12 -11
  200. flwr/server/strategy/qfedavg.py +16 -15
  201. flwr/server/strategy/strategy.py +6 -9
  202. flwr/server/superlink/fleet/grpc_adapter/grpc_adapter_servicer.py +19 -8
  203. flwr/server/superlink/fleet/grpc_bidi/flower_service_servicer.py +1 -2
  204. flwr/server/superlink/fleet/grpc_bidi/grpc_bridge.py +3 -4
  205. flwr/server/superlink/fleet/grpc_bidi/grpc_client_proxy.py +10 -12
  206. flwr/server/superlink/fleet/grpc_bidi/grpc_server.py +1 -3
  207. flwr/server/superlink/fleet/grpc_rere/fleet_servicer.py +136 -42
  208. flwr/server/superlink/fleet/grpc_rere/{server_interceptor.py → node_auth_server_interceptor.py} +28 -51
  209. flwr/server/superlink/fleet/message_handler/message_handler.py +100 -49
  210. flwr/server/superlink/fleet/rest_rere/rest_api.py +54 -33
  211. flwr/server/superlink/fleet/vce/backend/backend.py +2 -2
  212. flwr/server/superlink/fleet/vce/backend/raybackend.py +6 -6
  213. flwr/server/superlink/fleet/vce/vce_api.py +32 -13
  214. flwr/server/superlink/linkstate/in_memory_linkstate.py +266 -207
  215. flwr/server/superlink/linkstate/linkstate.py +161 -62
  216. flwr/server/superlink/linkstate/linkstate_factory.py +24 -6
  217. flwr/server/superlink/linkstate/sqlite_linkstate.py +698 -638
  218. flwr/server/superlink/linkstate/utils.py +9 -60
  219. flwr/server/superlink/serverappio/serverappio_grpc.py +1 -2
  220. flwr/server/superlink/serverappio/serverappio_servicer.py +28 -23
  221. flwr/server/superlink/simulation/simulationio_grpc.py +1 -2
  222. flwr/server/superlink/simulation/simulationio_servicer.py +19 -14
  223. flwr/server/superlink/utils.py +4 -6
  224. flwr/server/typing.py +1 -1
  225. flwr/server/utils/tensorboard.py +15 -8
  226. flwr/server/utils/validator.py +2 -3
  227. flwr/server/workflow/default_workflows.py +5 -5
  228. flwr/server/workflow/secure_aggregation/secagg_workflow.py +2 -4
  229. flwr/server/workflow/secure_aggregation/secaggplus_workflow.py +12 -10
  230. flwr/serverapp/strategy/bulyan.py +16 -15
  231. flwr/serverapp/strategy/dp_adaptive_clipping.py +12 -11
  232. flwr/serverapp/strategy/dp_fixed_clipping.py +11 -14
  233. flwr/serverapp/strategy/fedadagrad.py +10 -11
  234. flwr/serverapp/strategy/fedadam.py +10 -11
  235. flwr/serverapp/strategy/fedavg.py +9 -10
  236. flwr/serverapp/strategy/fedavgm.py +17 -16
  237. flwr/serverapp/strategy/fedmedian.py +2 -2
  238. flwr/serverapp/strategy/fedopt.py +10 -11
  239. flwr/serverapp/strategy/fedprox.py +7 -8
  240. flwr/serverapp/strategy/fedtrimmedavg.py +9 -9
  241. flwr/serverapp/strategy/fedxgb_bagging.py +3 -3
  242. flwr/serverapp/strategy/fedxgb_cyclic.py +9 -9
  243. flwr/serverapp/strategy/fedyogi.py +9 -11
  244. flwr/serverapp/strategy/krum.py +7 -7
  245. flwr/serverapp/strategy/multikrum.py +9 -9
  246. flwr/serverapp/strategy/qfedavg.py +17 -16
  247. flwr/serverapp/strategy/strategy.py +6 -9
  248. flwr/serverapp/strategy/strategy_utils.py +7 -8
  249. flwr/simulation/app.py +46 -42
  250. flwr/simulation/legacy_app.py +12 -12
  251. flwr/simulation/ray_transport/ray_actor.py +11 -12
  252. flwr/simulation/ray_transport/ray_client_proxy.py +12 -13
  253. flwr/simulation/run_simulation.py +44 -43
  254. flwr/simulation/simulationio_connection.py +4 -4
  255. flwr/supercore/cli/flower_superexec.py +3 -4
  256. flwr/supercore/constant.py +52 -0
  257. flwr/supercore/corestate/corestate.py +24 -3
  258. flwr/supercore/corestate/in_memory_corestate.py +138 -0
  259. flwr/supercore/corestate/sqlite_corestate.py +157 -0
  260. flwr/supercore/ffs/disk_ffs.py +1 -2
  261. flwr/supercore/ffs/ffs.py +1 -2
  262. flwr/supercore/ffs/ffs_factory.py +1 -2
  263. flwr/{common → supercore}/heartbeat.py +20 -25
  264. flwr/supercore/object_store/in_memory_object_store.py +1 -6
  265. flwr/supercore/object_store/object_store.py +1 -2
  266. flwr/supercore/object_store/object_store_factory.py +27 -8
  267. flwr/supercore/object_store/sqlite_object_store.py +253 -0
  268. flwr/{client/clientapp → supercore/primitives}/__init__.py +1 -1
  269. flwr/supercore/primitives/asymmetric.py +117 -0
  270. flwr/supercore/primitives/asymmetric_ed25519.py +175 -0
  271. flwr/supercore/sqlite_mixin.py +159 -0
  272. flwr/supercore/superexec/plugin/base_exec_plugin.py +1 -2
  273. flwr/supercore/superexec/plugin/exec_plugin.py +3 -3
  274. flwr/supercore/superexec/run_superexec.py +9 -13
  275. flwr/supercore/utils.py +20 -0
  276. flwr/superlink/artifact_provider/artifact_provider.py +1 -2
  277. flwr/{common → superlink}/auth_plugin/__init__.py +6 -6
  278. flwr/superlink/auth_plugin/auth_plugin.py +88 -0
  279. flwr/superlink/auth_plugin/noop_auth_plugin.py +84 -0
  280. flwr/superlink/federation/__init__.py +24 -0
  281. flwr/superlink/federation/federation_manager.py +64 -0
  282. flwr/superlink/federation/noop_federation_manager.py +71 -0
  283. flwr/superlink/servicer/control/{control_user_auth_interceptor.py → control_account_auth_interceptor.py} +41 -32
  284. flwr/superlink/servicer/control/control_event_log_interceptor.py +7 -7
  285. flwr/superlink/servicer/control/control_grpc.py +18 -17
  286. flwr/superlink/servicer/control/control_license_interceptor.py +3 -3
  287. flwr/superlink/servicer/control/control_servicer.py +239 -63
  288. flwr/supernode/cli/flower_supernode.py +74 -26
  289. flwr/supernode/nodestate/in_memory_nodestate.py +60 -49
  290. flwr/supernode/nodestate/nodestate.py +7 -8
  291. flwr/supernode/nodestate/nodestate_factory.py +7 -4
  292. flwr/supernode/runtime/run_clientapp.py +43 -24
  293. flwr/supernode/servicer/clientappio/clientappio_servicer.py +40 -10
  294. flwr/supernode/start_client_internal.py +175 -51
  295. {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/METADATA +8 -8
  296. flwr-1.24.0.dist-info/RECORD +454 -0
  297. flwr/common/auth_plugin/auth_plugin.py +0 -149
  298. flwr/supercore/object_store/utils.py +0 -43
  299. flwr-1.22.0.dist-info/RECORD +0 -428
  300. {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/WHEEL +0 -0
  301. {flwr-1.22.0.dist-info → flwr-1.24.0.dist-info}/entry_points.txt +0 -0
@@ -17,20 +17,23 @@
17
17
 
18
18
  import hashlib
19
19
  import time
20
- from collections.abc import Generator
20
+ from collections.abc import Generator, Sequence
21
21
  from logging import ERROR, INFO
22
- from typing import Any, Optional, cast
22
+ from typing import Any, cast
23
23
 
24
24
  import grpc
25
25
 
26
26
  from flwr.cli.config_utils import get_fab_metadata
27
27
  from flwr.common import Context, RecordDict, now
28
- from flwr.common.auth_plugin import ControlAuthPlugin
29
28
  from flwr.common.constant import (
30
29
  FAB_MAX_SIZE,
30
+ HEARTBEAT_DEFAULT_INTERVAL,
31
31
  LOG_STREAM_INTERVAL,
32
+ NO_ACCOUNT_AUTH_MESSAGE,
32
33
  NO_ARTIFACT_PROVIDER_MESSAGE,
33
- NO_USER_AUTH_MESSAGE,
34
+ NODE_NOT_FOUND_MESSAGE,
35
+ PUBLIC_KEY_ALREADY_IN_USE_MESSAGE,
36
+ PUBLIC_KEY_NOT_VALID,
34
37
  PULL_UNFINISHED_RUN_MESSAGE,
35
38
  RUN_ID_NOT_FOUND_MESSAGE,
36
39
  Status,
@@ -49,23 +52,37 @@ from flwr.proto.control_pb2 import ( # pylint: disable=E0611
49
52
  GetAuthTokensResponse,
50
53
  GetLoginDetailsRequest,
51
54
  GetLoginDetailsResponse,
55
+ ListFederationsRequest,
56
+ ListFederationsResponse,
57
+ ListNodesRequest,
58
+ ListNodesResponse,
52
59
  ListRunsRequest,
53
60
  ListRunsResponse,
54
61
  PullArtifactsRequest,
55
62
  PullArtifactsResponse,
63
+ RegisterNodeRequest,
64
+ RegisterNodeResponse,
65
+ ShowFederationRequest,
66
+ ShowFederationResponse,
56
67
  StartRunRequest,
57
68
  StartRunResponse,
58
69
  StopRunRequest,
59
70
  StopRunResponse,
60
71
  StreamLogsRequest,
61
72
  StreamLogsResponse,
73
+ UnregisterNodeRequest,
74
+ UnregisterNodeResponse,
62
75
  )
76
+ from flwr.proto.federation_pb2 import Federation # pylint: disable=E0611
77
+ from flwr.proto.node_pb2 import NodeInfo # pylint: disable=E0611
63
78
  from flwr.server.superlink.linkstate import LinkState, LinkStateFactory
64
79
  from flwr.supercore.ffs import FfsFactory
65
80
  from flwr.supercore.object_store import ObjectStore, ObjectStoreFactory
81
+ from flwr.supercore.primitives.asymmetric import bytes_to_public_key, uses_nist_ec_curve
66
82
  from flwr.superlink.artifact_provider import ArtifactProvider
83
+ from flwr.superlink.auth_plugin import ControlAuthnPlugin
67
84
 
68
- from .control_user_auth_interceptor import shared_account_info
85
+ from .control_account_auth_interceptor import get_current_account_info
69
86
 
70
87
 
71
88
  class ControlServicer(control_pb2_grpc.ControlServicer):
@@ -77,14 +94,14 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
77
94
  ffs_factory: FfsFactory,
78
95
  objectstore_factory: ObjectStoreFactory,
79
96
  is_simulation: bool,
80
- auth_plugin: Optional[ControlAuthPlugin] = None,
81
- artifact_provider: Optional[ArtifactProvider] = None,
97
+ authn_plugin: ControlAuthnPlugin,
98
+ artifact_provider: ArtifactProvider | None = None,
82
99
  ) -> None:
83
100
  self.linkstate_factory = linkstate_factory
84
101
  self.ffs_factory = ffs_factory
85
102
  self.objectstore_factory = objectstore_factory
86
103
  self.is_simulation = is_simulation
87
- self.auth_plugin = auth_plugin
104
+ self.authn_plugin = authn_plugin
88
105
  self.artifact_provider = artifact_provider
89
106
 
90
107
  def StartRun( # pylint: disable=too-many-locals
@@ -103,7 +120,8 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
103
120
  )
104
121
  return StartRunResponse()
105
122
 
106
- flwr_aid = shared_account_info.get().flwr_aid if self.auth_plugin else None
123
+ flwr_aid = get_current_account_info().flwr_aid
124
+ flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
107
125
  override_config = user_config_from_proto(request.override_config)
108
126
  federation_options = config_record_from_proto(request.federation_options)
109
127
  fab_file = request.fab.content
@@ -115,8 +133,25 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
115
133
  "Federation options doesn't contain key `num-supernodes`."
116
134
  )
117
135
 
136
+ # Check (1) federation exists and (2) the flwr_aid is a member
137
+ federation = request.federation
138
+
139
+ if not state.federation_manager.exists(federation):
140
+ raise ValueError(f"Federation '{federation}' does not exist.")
141
+
142
+ if not state.federation_manager.has_member(flwr_aid, federation):
143
+ raise ValueError(
144
+ f"Account with ID '{flwr_aid}' is not a member of the "
145
+ f"federation '{federation}'. Please log in with another account "
146
+ "or request access to this federation."
147
+ )
148
+
118
149
  # Create run
119
- fab = Fab(hashlib.sha256(fab_file).hexdigest(), fab_file)
150
+ fab = Fab(
151
+ hashlib.sha256(fab_file).hexdigest(),
152
+ fab_file,
153
+ dict(request.fab.verifications),
154
+ )
120
155
  fab_hash = ffs.put(fab.content, {})
121
156
  if fab_hash != fab.hash_str:
122
157
  raise RuntimeError(
@@ -129,6 +164,7 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
129
164
  fab_version,
130
165
  fab_hash,
131
166
  override_config,
167
+ request.federation,
132
168
  federation_options,
133
169
  flwr_aid,
134
170
  )
@@ -157,7 +193,10 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
157
193
  # pylint: disable-next=broad-except
158
194
  except Exception as e:
159
195
  log(ERROR, "Could not start run: %s", str(e))
160
- return StartRunResponse()
196
+ context.abort(
197
+ grpc.StatusCode.FAILED_PRECONDITION,
198
+ str(e),
199
+ )
161
200
 
162
201
  log(INFO, "Created run %s", str(run_id))
163
202
  return StartRunResponse(run_id=run_id)
@@ -177,12 +216,9 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
177
216
  if not run:
178
217
  context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
179
218
 
180
- # If user auth is enabled, check if `flwr_aid` matches the run's `flwr_aid`
181
- if self.auth_plugin:
182
- flwr_aid = shared_account_info.get().flwr_aid
183
- _check_flwr_aid_in_run(
184
- flwr_aid=flwr_aid, run=cast(Run, run), context=context
185
- )
219
+ # Check if `flwr_aid` matches the run's `flwr_aid`
220
+ flwr_aid = get_current_account_info().flwr_aid
221
+ _check_flwr_aid_in_run(flwr_aid=flwr_aid, run=cast(Run, run), context=context)
186
222
 
187
223
  after_timestamp = request.after_timestamp + 1e-6
188
224
  while context.is_active():
@@ -218,20 +254,11 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
218
254
 
219
255
  # Build a set of run IDs for `flwr ls --runs`
220
256
  if not request.HasField("run_id"):
221
- if self.auth_plugin:
222
- # If no `run_id` is specified and user auth is enabled,
223
- # return run IDs for the authenticated user
224
- flwr_aid = shared_account_info.get().flwr_aid
225
- if flwr_aid is None:
226
- context.abort(
227
- grpc.StatusCode.PERMISSION_DENIED,
228
- "️⛔️ User authentication is enabled, but `flwr_aid` is None",
229
- )
230
- run_ids = state.get_run_ids(flwr_aid=flwr_aid)
231
- else:
232
- # If no `run_id` is specified and no user auth is enabled,
233
- # return all run IDs
234
- run_ids = state.get_run_ids(None)
257
+ # If no `run_id` is specified and account auth is enabled,
258
+ # return run IDs for the authenticated account
259
+ flwr_aid = get_current_account_info().flwr_aid
260
+ _check_flwr_aid_exists(flwr_aid, context)
261
+ run_ids = state.get_run_ids(flwr_aid=flwr_aid)
235
262
  # Build a set of run IDs for `flwr ls --run-id <run_id>`
236
263
  else:
237
264
  # Retrieve run ID and run
@@ -241,13 +268,11 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
241
268
  # Exit if `run_id` not found
242
269
  if not run:
243
270
  context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
271
+ raise grpc.RpcError() # This line is unreachable
244
272
 
245
- # If user auth is enabled, check if `flwr_aid` matches the run's `flwr_aid`
246
- if self.auth_plugin:
247
- flwr_aid = shared_account_info.get().flwr_aid
248
- _check_flwr_aid_in_run(
249
- flwr_aid=flwr_aid, run=cast(Run, run), context=context
250
- )
273
+ # Check if `flwr_aid` matches the run's `flwr_aid`
274
+ flwr_aid = get_current_account_info().flwr_aid
275
+ _check_flwr_aid_in_run(flwr_aid=flwr_aid, run=run, context=context)
251
276
 
252
277
  run_ids = {run_id}
253
278
 
@@ -269,13 +294,11 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
269
294
  # Exit if `run_id` not found
270
295
  if not run:
271
296
  context.abort(grpc.StatusCode.NOT_FOUND, RUN_ID_NOT_FOUND_MESSAGE)
297
+ raise grpc.RpcError() # This line is unreachable
272
298
 
273
- # If user auth is enabled, check if `flwr_aid` matches the run's `flwr_aid`
274
- if self.auth_plugin:
275
- flwr_aid = shared_account_info.get().flwr_aid
276
- _check_flwr_aid_in_run(
277
- flwr_aid=flwr_aid, run=cast(Run, run), context=context
278
- )
299
+ # Check if `flwr_aid` matches the run's `flwr_aid`
300
+ flwr_aid = get_current_account_info().flwr_aid
301
+ _check_flwr_aid_in_run(flwr_aid=flwr_aid, run=run, context=context)
279
302
 
280
303
  run_status = state.get_run_status({run_id})[run_id]
281
304
  if run_status.status == Status.FINISHED:
@@ -284,11 +307,15 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
284
307
  f"Run ID {run_id} is already finished",
285
308
  )
286
309
 
310
+ # Update run status to finished:stopped
287
311
  update_success = state.update_run_status(
288
312
  run_id=run_id,
289
313
  new_status=RunStatus(Status.FINISHED, SubStatus.STOPPED, ""),
290
314
  )
291
315
 
316
+ # Delete the token associated with the run to stop further operations
317
+ state.delete_token(run_id)
318
+
292
319
  if update_success:
293
320
  message_ids: set[str] = state.get_message_ids_from_run_id(run_id)
294
321
 
@@ -305,22 +332,22 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
305
332
  ) -> GetLoginDetailsResponse:
306
333
  """Start login."""
307
334
  log(INFO, "ControlServicer.GetLoginDetails")
308
- if self.auth_plugin is None:
335
+ if self.authn_plugin is None:
309
336
  context.abort(
310
337
  grpc.StatusCode.UNIMPLEMENTED,
311
- NO_USER_AUTH_MESSAGE,
338
+ NO_ACCOUNT_AUTH_MESSAGE,
312
339
  )
313
340
  raise grpc.RpcError() # This line is unreachable
314
341
 
315
342
  # Get login details
316
- details = self.auth_plugin.get_login_details()
343
+ details = self.authn_plugin.get_login_details()
317
344
 
318
345
  # Return empty response if details is None
319
346
  if details is None:
320
347
  return GetLoginDetailsResponse()
321
348
 
322
349
  return GetLoginDetailsResponse(
323
- auth_type=details.auth_type,
350
+ authn_type=details.authn_type,
324
351
  device_code=details.device_code,
325
352
  verification_uri_complete=details.verification_uri_complete,
326
353
  expires_in=details.expires_in,
@@ -332,15 +359,15 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
332
359
  ) -> GetAuthTokensResponse:
333
360
  """Get auth token."""
334
361
  log(INFO, "ControlServicer.GetAuthTokens")
335
- if self.auth_plugin is None:
362
+ if self.authn_plugin is None:
336
363
  context.abort(
337
364
  grpc.StatusCode.UNIMPLEMENTED,
338
- NO_USER_AUTH_MESSAGE,
365
+ NO_ACCOUNT_AUTH_MESSAGE,
339
366
  )
340
367
  raise grpc.RpcError() # This line is unreachable
341
368
 
342
369
  # Get auth tokens
343
- credentials = self.auth_plugin.get_auth_tokens(request.device_code)
370
+ credentials = self.authn_plugin.get_auth_tokens(request.device_code)
344
371
 
345
372
  # Return empty response if credentials is None
346
373
  if credentials is None:
@@ -383,15 +410,160 @@ class ControlServicer(control_pb2_grpc.ControlServicer):
383
410
  grpc.StatusCode.FAILED_PRECONDITION, PULL_UNFINISHED_RUN_MESSAGE
384
411
  )
385
412
 
386
- # Check if `flwr_aid` matches the run's `flwr_aid` when user auth is enabled
387
- if self.auth_plugin:
388
- flwr_aid = shared_account_info.get().flwr_aid
389
- _check_flwr_aid_in_run(flwr_aid=flwr_aid, run=run, context=context)
413
+ # Check if `flwr_aid` matches the run's `flwr_aid`
414
+ flwr_aid = get_current_account_info().flwr_aid
415
+ _check_flwr_aid_in_run(flwr_aid=flwr_aid, run=run, context=context)
390
416
 
391
417
  # Call artifact provider
392
418
  download_url = self.artifact_provider.get_url(run_id)
393
419
  return PullArtifactsResponse(url=download_url)
394
420
 
421
+ def RegisterNode(
422
+ self, request: RegisterNodeRequest, context: grpc.ServicerContext
423
+ ) -> RegisterNodeResponse:
424
+ """Add a SuperNode."""
425
+ log(INFO, "ControlServicer.RegisterNode")
426
+
427
+ # Verify public key
428
+ try:
429
+ # Attempt to deserialize public key
430
+ pub_key = bytes_to_public_key(request.public_key)
431
+ # Check if it's a NIST EC curve public key
432
+ if not uses_nist_ec_curve(pub_key):
433
+ err_msg = "The provided public key is not a NIST EC curve public key."
434
+ log(ERROR, "%s", err_msg)
435
+ raise ValueError(err_msg)
436
+ except (ValueError, AttributeError) as err:
437
+ log(ERROR, "%s", err)
438
+ context.abort(grpc.StatusCode.FAILED_PRECONDITION, PUBLIC_KEY_NOT_VALID)
439
+
440
+ # Init link state
441
+ state = self.linkstate_factory.state()
442
+ node_id = 0
443
+
444
+ flwr_aid = get_current_account_info().flwr_aid
445
+ flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
446
+ # Account name exists if `flwr_aid` exists
447
+ account_name = cast(str, get_current_account_info().account_name)
448
+ try:
449
+ node_id = state.create_node(
450
+ owner_aid=flwr_aid,
451
+ owner_name=account_name,
452
+ public_key=request.public_key,
453
+ heartbeat_interval=HEARTBEAT_DEFAULT_INTERVAL,
454
+ )
455
+
456
+ except ValueError:
457
+ # Public key already in use
458
+ log(ERROR, PUBLIC_KEY_ALREADY_IN_USE_MESSAGE)
459
+ context.abort(
460
+ grpc.StatusCode.FAILED_PRECONDITION, PUBLIC_KEY_ALREADY_IN_USE_MESSAGE
461
+ )
462
+ log(INFO, "[ControlServicer.RegisterNode] Created node_id=%s", node_id)
463
+
464
+ return RegisterNodeResponse(node_id=node_id)
465
+
466
+ def UnregisterNode(
467
+ self, request: UnregisterNodeRequest, context: grpc.ServicerContext
468
+ ) -> UnregisterNodeResponse:
469
+ """Remove a SuperNode."""
470
+ log(INFO, "ControlServicer.UnregisterNode")
471
+
472
+ # Init link state
473
+ state = self.linkstate_factory.state()
474
+
475
+ flwr_aid = get_current_account_info().flwr_aid
476
+ flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
477
+ try:
478
+ state.delete_node(owner_aid=flwr_aid, node_id=request.node_id)
479
+ except ValueError:
480
+ log(ERROR, NODE_NOT_FOUND_MESSAGE)
481
+ context.abort(grpc.StatusCode.NOT_FOUND, NODE_NOT_FOUND_MESSAGE)
482
+
483
+ return UnregisterNodeResponse()
484
+
485
+ def ListNodes(
486
+ self, request: ListNodesRequest, context: grpc.ServicerContext
487
+ ) -> ListNodesResponse:
488
+ """List all SuperNodes."""
489
+ log(INFO, "ControlServicer.ListNodes")
490
+
491
+ if self.is_simulation:
492
+ log(ERROR, "ListNodes is not available in simulation mode.")
493
+ context.abort(
494
+ grpc.StatusCode.UNIMPLEMENTED,
495
+ "ListNodes is not available in simulation mode.",
496
+ )
497
+ raise grpc.RpcError() # This line is unreachable
498
+
499
+ nodes_info: Sequence[NodeInfo] = []
500
+ # Init link state
501
+ state = self.linkstate_factory.state()
502
+
503
+ flwr_aid = get_current_account_info().flwr_aid
504
+ flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
505
+ # Retrieve all nodes for the account
506
+ nodes_info = state.get_node_info(owner_aids=[flwr_aid])
507
+
508
+ return ListNodesResponse(nodes_info=nodes_info, now=now().isoformat())
509
+
510
+ def ListFederations(
511
+ self, request: ListFederationsRequest, context: grpc.ServicerContext
512
+ ) -> ListFederationsResponse:
513
+ """List all SuperNodes."""
514
+ log(INFO, "ControlServicer.ListFederations")
515
+
516
+ # Init link state
517
+ state = self.linkstate_factory.state()
518
+
519
+ flwr_aid = get_current_account_info().flwr_aid
520
+ flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
521
+
522
+ # Get federations the account is a member of
523
+ federations = state.federation_manager.get_federations(flwr_aid=flwr_aid)
524
+
525
+ return ListFederationsResponse(
526
+ federations=[Federation(name=fed) for fed in federations]
527
+ )
528
+
529
+ def ShowFederation(
530
+ self, request: ShowFederationRequest, context: grpc.ServicerContext
531
+ ) -> ShowFederationResponse:
532
+ """Show details of a specific Federation."""
533
+ log(INFO, "ControlServicer.ShowFederation")
534
+
535
+ # Init link state
536
+ state = self.linkstate_factory.state()
537
+
538
+ flwr_aid = get_current_account_info().flwr_aid
539
+ flwr_aid = _check_flwr_aid_exists(flwr_aid, context)
540
+
541
+ # Get federations the account is a member of
542
+ federations = state.federation_manager.get_federations(flwr_aid=flwr_aid)
543
+
544
+ # Ensure flwr_aid is a member of the requested federation
545
+ federation = request.federation_name
546
+ if federation not in federations:
547
+ context.abort(
548
+ grpc.StatusCode.FAILED_PRECONDITION,
549
+ f"Federation '{federation}' does not exist or you are "
550
+ "not a member of it.",
551
+ )
552
+
553
+ # Fetch federation details
554
+ details = state.federation_manager.get_details(federation)
555
+
556
+ # Build Federation proto object
557
+ federation_proto = Federation(
558
+ name=federation,
559
+ member_aids=details.member_aids,
560
+ nodes=details.nodes,
561
+ runs=[run_to_proto(run) for run in details.runs],
562
+ )
563
+ return ShowFederationResponse(
564
+ federation=federation_proto, now=now().isoformat()
565
+ )
566
+
395
567
 
396
568
  def _create_list_runs_response(
397
569
  run_ids: set[int], state: LinkState, store: ObjectStore
@@ -410,29 +582,33 @@ def _create_list_runs_response(
410
582
  )
411
583
 
412
584
 
413
- def _check_flwr_aid_in_run(
414
- flwr_aid: Optional[str], run: Run, context: grpc.ServicerContext
415
- ) -> None:
416
- """Guard clause to check if `flwr_aid` matches the run's `flwr_aid`."""
417
- # `flwr_aid` must not be None. Abort if it is None.
585
+ def _check_flwr_aid_exists(flwr_aid: str | None, context: grpc.ServicerContext) -> str:
586
+ """Guard clause to check if `flwr_aid` exists."""
418
587
  if flwr_aid is None:
419
588
  context.abort(
420
589
  grpc.StatusCode.PERMISSION_DENIED,
421
- "️⛔️ User authentication is enabled, but `flwr_aid` is None",
590
+ "️⛔️ Failed to fetch the account information.",
422
591
  )
592
+ raise RuntimeError # This line is unreachable
593
+ return flwr_aid
423
594
 
595
+
596
+ def _check_flwr_aid_in_run(
597
+ flwr_aid: str | None, run: Run, context: grpc.ServicerContext
598
+ ) -> None:
599
+ """Guard clause to check if `flwr_aid` matches the run's `flwr_aid`."""
600
+ _check_flwr_aid_exists(flwr_aid, context)
424
601
  # `run.flwr_aid` must not be an empty string. Abort if it is empty.
425
602
  run_flwr_aid = run.flwr_aid
426
603
  if not run_flwr_aid:
427
604
  context.abort(
428
605
  grpc.StatusCode.PERMISSION_DENIED,
429
- "⛔️ User authentication is enabled, but the run is not associated "
430
- "with a `flwr_aid`.",
606
+ "⛔️ Run is not associated with a `flwr_aid`.",
431
607
  )
432
608
 
433
609
  # Exit if `flwr_aid` does not match the run's `flwr_aid`
434
610
  if run_flwr_aid != flwr_aid:
435
611
  context.abort(
436
612
  grpc.StatusCode.PERMISSION_DENIED,
437
- "⛔️ Run ID does not belong to the user",
613
+ "⛔️ Run ID does not belong to the account",
438
614
  )
@@ -18,14 +18,12 @@
18
18
  import argparse
19
19
  from logging import DEBUG, INFO, WARN
20
20
  from pathlib import Path
21
- from typing import Optional
22
21
 
22
+ import yaml
23
23
  from cryptography.exceptions import UnsupportedAlgorithm
24
- from cryptography.hazmat.primitives.asymmetric import ec
25
- from cryptography.hazmat.primitives.serialization import (
26
- load_ssh_private_key,
27
- load_ssh_public_key,
28
- )
24
+ from cryptography.hazmat.primitives.asymmetric import ec, ed25519
25
+ from cryptography.hazmat.primitives.serialization import load_ssh_private_key
26
+ from cryptography.hazmat.primitives.serialization.ssh import load_ssh_public_key
29
27
 
30
28
  from flwr.common import EventType, event
31
29
  from flwr.common.args import try_obtain_root_certificates
@@ -61,9 +59,19 @@ def flower_supernode() -> None:
61
59
  "Ignoring `--flwr-dir`.",
62
60
  )
63
61
 
62
+ trusted_entities = _try_obtain_trusted_entities(args.trusted_entities)
63
+ if trusted_entities:
64
+ _validate_public_keys_ed25519(trusted_entities)
64
65
  root_certificates = try_obtain_root_certificates(args, args.superlink)
65
66
  authentication_keys = _try_setup_client_authentication(args)
66
67
 
68
+ # Warn if authentication keys are provided but transport is not grpc-rere
69
+ if authentication_keys is not None and args.transport != TRANSPORT_TYPE_GRPC_RERE:
70
+ log(
71
+ WARN,
72
+ "SuperNode Authentication is only supported with the grpc-rere transport.",
73
+ )
74
+
67
75
  log(DEBUG, "Isolation mode: %s", args.isolation)
68
76
 
69
77
  start_client_internal(
@@ -81,6 +89,7 @@ def flower_supernode() -> None:
81
89
  isolation=args.isolation,
82
90
  clientappio_api_address=args.clientappio_api_address,
83
91
  health_server_address=args.health_server_address,
92
+ trusted_entities=trusted_entities,
84
93
  )
85
94
 
86
95
 
@@ -120,6 +129,18 @@ def _parse_args_run_supernode() -> argparse.ArgumentParser:
120
129
  help="ClientAppIo API (gRPC) server address (IPv4, IPv6, or a domain name). "
121
130
  f"By default, it is set to {CLIENTAPPIO_API_DEFAULT_SERVER_ADDRESS}.",
122
131
  )
132
+ parser.add_argument(
133
+ "--trusted-entities",
134
+ type=Path,
135
+ default=None,
136
+ metavar="YAML_FILE",
137
+ help=(
138
+ "Path to a YAML file defining trusted entities. "
139
+ "The file must map public key IDs to public keys. "
140
+ "Example: { fpk_UUID1: 'ssh-ed25519 <key1> [comment1]', "
141
+ "fpk_UUID2: 'ssh-ed25519 <key2> [comment2]' }"
142
+ ),
143
+ )
123
144
  add_args_health(parser)
124
145
 
125
146
  return parser
@@ -188,12 +209,12 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None:
188
209
  parser.add_argument(
189
210
  "--auth-supernode-private-key",
190
211
  type=str,
191
- help="The SuperNode's private key (as a path str) to enable authentication.",
212
+ help="Path to the SuperNode's private key to enable authentication.",
192
213
  )
193
214
  parser.add_argument(
194
215
  "--auth-supernode-public-key",
195
216
  type=str,
196
- help="The SuperNode's public key (as a path str) to enable authentication.",
217
+ help="This argument is deprecated and will be removed in a future release.",
197
218
  )
198
219
  parser.add_argument(
199
220
  "--node-config",
@@ -206,13 +227,10 @@ def _parse_args_common(parser: argparse.ArgumentParser) -> None:
206
227
 
207
228
  def _try_setup_client_authentication(
208
229
  args: argparse.Namespace,
209
- ) -> Optional[tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]]:
210
- if not args.auth_supernode_private_key and not args.auth_supernode_public_key:
230
+ ) -> tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey] | None:
231
+ if not args.auth_supernode_private_key:
211
232
  return None
212
233
 
213
- if not args.auth_supernode_private_key or not args.auth_supernode_public_key:
214
- flwr_exit(ExitCode.SUPERNODE_NODE_AUTH_KEYS_REQUIRED)
215
-
216
234
  try:
217
235
  ssh_private_key = load_ssh_private_key(
218
236
  Path(args.auth_supernode_private_key).read_bytes(),
@@ -222,23 +240,53 @@ def _try_setup_client_authentication(
222
240
  raise ValueError()
223
241
  except (ValueError, UnsupportedAlgorithm):
224
242
  flwr_exit(
225
- ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID,
243
+ ExitCode.SUPERNODE_NODE_AUTH_KEY_INVALID,
226
244
  "Unable to parse the private key file.",
227
245
  )
228
246
 
229
- try:
230
- ssh_public_key = load_ssh_public_key(
231
- Path(args.auth_supernode_public_key).read_bytes()
247
+ if args.auth_supernode_public_key:
248
+ log(
249
+ WARN,
250
+ "The `--auth-supernode-public-key` flag is deprecated and will be "
251
+ "removed in a future release. The public key is now derived from the "
252
+ "private key provided by `--auth-supernode-private-key`.",
232
253
  )
233
- if not isinstance(ssh_public_key, ec.EllipticCurvePublicKey):
234
- raise ValueError()
235
- except (ValueError, UnsupportedAlgorithm):
254
+ return ssh_private_key, ssh_private_key.public_key()
255
+
256
+
257
+ def _try_obtain_trusted_entities(
258
+ trusted_entities_path: Path | None,
259
+ ) -> dict[str, str] | None:
260
+ """Validate and return the trust entities."""
261
+ if not trusted_entities_path:
262
+ return None
263
+ if not trusted_entities_path.is_file():
236
264
  flwr_exit(
237
- ExitCode.SUPERNODE_NODE_AUTH_KEYS_INVALID,
238
- "Unable to parse the public key file.",
265
+ ExitCode.SUPERNODE_INVALID_TRUSTED_ENTITIES,
266
+ "Path argument `--trusted-entities` does not point to a file.",
239
267
  )
268
+ try:
269
+ with trusted_entities_path.open("r", encoding="utf-8") as f:
270
+ trusted_entities = yaml.safe_load(f)
271
+ if not isinstance(trusted_entities, dict):
272
+ raise ValueError("Invalid trusted entities format.")
273
+ except (yaml.YAMLError, ValueError) as e:
274
+ flwr_exit(
275
+ ExitCode.SUPERNODE_INVALID_TRUSTED_ENTITIES,
276
+ f"Failed to read YAML file '{trusted_entities_path}': {e}",
277
+ )
278
+ return trusted_entities
240
279
 
241
- return (
242
- ssh_private_key,
243
- ssh_public_key,
244
- )
280
+
281
+ def _validate_public_keys_ed25519(trusted_entities: dict[str, str]) -> None:
282
+ """Validate public keys for the trust entities are Ed25519."""
283
+ for public_key_id in trusted_entities.keys():
284
+ verifier_public_key = load_ssh_public_key(
285
+ trusted_entities[public_key_id].encode("utf-8")
286
+ )
287
+ if not isinstance(verifier_public_key, ed25519.Ed25519PublicKey):
288
+ flwr_exit(
289
+ ExitCode.SUPERNODE_INVALID_TRUSTED_ENTITIES,
290
+ "The provided public key associated with "
291
+ f"trusted entity {public_key_id} is not Ed25519.",
292
+ )