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
@@ -0,0 +1,117 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Asymmetric cryptography utilities."""
16
+
17
+
18
+ from typing import cast
19
+
20
+ from cryptography.exceptions import InvalidSignature
21
+ from cryptography.hazmat.primitives import hashes, serialization
22
+ from cryptography.hazmat.primitives.asymmetric import ec
23
+
24
+
25
+ def generate_key_pairs() -> (
26
+ tuple[ec.EllipticCurvePrivateKey, ec.EllipticCurvePublicKey]
27
+ ):
28
+ """Generate private and public key pairs with Cryptography."""
29
+ private_key = ec.generate_private_key(ec.SECP384R1())
30
+ public_key = private_key.public_key()
31
+ return private_key, public_key
32
+
33
+
34
+ def private_key_to_bytes(private_key: ec.EllipticCurvePrivateKey) -> bytes:
35
+ """Serialize private key to bytes."""
36
+ return private_key.private_bytes(
37
+ encoding=serialization.Encoding.PEM,
38
+ format=serialization.PrivateFormat.PKCS8,
39
+ encryption_algorithm=serialization.NoEncryption(),
40
+ )
41
+
42
+
43
+ def bytes_to_private_key(private_key_bytes: bytes) -> ec.EllipticCurvePrivateKey:
44
+ """Deserialize private key from bytes."""
45
+ return cast(
46
+ ec.EllipticCurvePrivateKey,
47
+ serialization.load_pem_private_key(data=private_key_bytes, password=None),
48
+ )
49
+
50
+
51
+ def public_key_to_bytes(public_key: ec.EllipticCurvePublicKey) -> bytes:
52
+ """Serialize public key to bytes."""
53
+ return public_key.public_bytes(
54
+ encoding=serialization.Encoding.PEM,
55
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
56
+ )
57
+
58
+
59
+ def bytes_to_public_key(public_key_bytes: bytes) -> ec.EllipticCurvePublicKey:
60
+ """Deserialize public key from bytes."""
61
+ return cast(
62
+ ec.EllipticCurvePublicKey,
63
+ serialization.load_pem_public_key(data=public_key_bytes),
64
+ )
65
+
66
+
67
+ def sign_message(private_key: ec.EllipticCurvePrivateKey, message: bytes) -> bytes:
68
+ """Sign a message using the provided EC private key.
69
+
70
+ Parameters
71
+ ----------
72
+ private_key : ec.EllipticCurvePrivateKey
73
+ The EC private key to sign the message with.
74
+ message : bytes
75
+ The message to be signed.
76
+
77
+ Returns
78
+ -------
79
+ bytes
80
+ The signature of the message.
81
+ """
82
+ signature = private_key.sign(message, ec.ECDSA(hashes.SHA256()))
83
+ return signature
84
+
85
+
86
+ def verify_signature(
87
+ public_key: ec.EllipticCurvePublicKey, message: bytes, signature: bytes
88
+ ) -> bool:
89
+ """Verify a signature against a message using the provided EC public key.
90
+
91
+ Parameters
92
+ ----------
93
+ public_key : ec.EllipticCurvePublicKey
94
+ The EC public key to verify the signature.
95
+ message : bytes
96
+ The original message.
97
+ signature : bytes
98
+ The signature to verify.
99
+
100
+ Returns
101
+ -------
102
+ bool
103
+ True if the signature is valid, False otherwise.
104
+ """
105
+ try:
106
+ public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
107
+ return True
108
+ except InvalidSignature:
109
+ return False
110
+
111
+
112
+ def uses_nist_ec_curve(public_key: ec.EllipticCurvePublicKey) -> bool:
113
+ """Return True if the provided key uses a NIST EC curve."""
114
+ return isinstance(
115
+ public_key.curve,
116
+ (ec.SECP192R1 | ec.SECP224R1 | ec.SECP256R1 | ec.SECP384R1 | ec.SECP521R1),
117
+ )
@@ -0,0 +1,175 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Ed25519-only asymmetric cryptography utilities."""
16
+
17
+ import base64
18
+ from pathlib import Path
19
+
20
+ from cryptography.exceptions import InvalidSignature
21
+ from cryptography.hazmat.primitives import serialization
22
+ from cryptography.hazmat.primitives.asymmetric import ed25519
23
+
24
+
25
+ def generate_key_pair() -> tuple[ed25519.Ed25519PrivateKey, ed25519.Ed25519PublicKey]:
26
+ """Generate an Ed25519 private/public key pair.
27
+
28
+ Returns
29
+ -------
30
+ Tuple[Ed25519PrivateKey, Ed25519PublicKey]
31
+ Private and public key pair.
32
+ """
33
+ private_key = ed25519.Ed25519PrivateKey.generate()
34
+ return private_key, private_key.public_key()
35
+
36
+
37
+ def private_key_to_bytes(private_key: ed25519.Ed25519PrivateKey) -> bytes:
38
+ """Serialize an Ed25519 private key to PEM bytes.
39
+
40
+ Parameters
41
+ ----------
42
+ private_key : Ed25519PrivateKey
43
+ The private key to serialize.
44
+
45
+ Returns
46
+ -------
47
+ bytes
48
+ PEM-encoded private key.
49
+ """
50
+ return private_key.private_bytes(
51
+ encoding=serialization.Encoding.PEM,
52
+ format=serialization.PrivateFormat.PKCS8,
53
+ encryption_algorithm=serialization.NoEncryption(),
54
+ )
55
+
56
+
57
+ def bytes_to_private_key(private_key_bytes: bytes) -> ed25519.Ed25519PrivateKey:
58
+ """Deserialize an Ed25519 private key from PEM bytes.
59
+
60
+ Parameters
61
+ ----------
62
+ private_key_bytes : bytes
63
+ PEM-encoded private key.
64
+
65
+ Returns
66
+ -------
67
+ Ed25519PrivateKey
68
+ Deserialized private key.
69
+ """
70
+ return serialization.load_pem_private_key(
71
+ private_key_bytes, password=None
72
+ ) # type: ignore[return-value]
73
+
74
+
75
+ def public_key_to_bytes(public_key: ed25519.Ed25519PublicKey) -> bytes:
76
+ """Serialize an Ed25519 public key to PEM bytes.
77
+
78
+ Parameters
79
+ ----------
80
+ public_key : Ed25519PublicKey
81
+ The public key to serialize.
82
+
83
+ Returns
84
+ -------
85
+ bytes
86
+ PEM-encoded public key.
87
+ """
88
+ return public_key.public_bytes(
89
+ encoding=serialization.Encoding.PEM,
90
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
91
+ )
92
+
93
+
94
+ def bytes_to_public_key(public_key_bytes: bytes) -> ed25519.Ed25519PublicKey:
95
+ """Deserialize an Ed25519 public key from PEM bytes.
96
+
97
+ Parameters
98
+ ----------
99
+ public_key_bytes : bytes
100
+ PEM-encoded public key.
101
+
102
+ Returns
103
+ -------
104
+ Ed25519PublicKey
105
+ Deserialized public key.
106
+ """
107
+ return serialization.load_pem_public_key(public_key_bytes) # type: ignore[return-value]
108
+
109
+
110
+ def sign_message(private_key: ed25519.Ed25519PrivateKey, message: bytes) -> bytes:
111
+ """Sign a message using an Ed25519 private key.
112
+
113
+ Parameters
114
+ ----------
115
+ private_key : Ed25519PrivateKey
116
+ The private key used for signing.
117
+ message : bytes
118
+ The message to sign.
119
+
120
+ Returns
121
+ -------
122
+ bytes
123
+ The signature of the message.
124
+ """
125
+ return private_key.sign(message)
126
+
127
+
128
+ def verify_signature(
129
+ public_key: ed25519.Ed25519PublicKey, message: bytes, signature: bytes
130
+ ) -> bool:
131
+ """Verify a signature using an Ed25519 public key.
132
+
133
+ Parameters
134
+ ----------
135
+ public_key : Ed25519PublicKey
136
+ The public key used for verification.
137
+ message : bytes
138
+ The original message.
139
+ signature : bytes
140
+ The signature to verify.
141
+
142
+ Returns
143
+ -------
144
+ bool
145
+ True if the signature is valid, False otherwise.
146
+ """
147
+ try:
148
+ public_key.verify(signature, message)
149
+ return True
150
+ except InvalidSignature:
151
+ return False
152
+
153
+
154
+ def create_message_to_sign(fab_digest: bytes, timestamp: int) -> bytes:
155
+ """Create a canonical message:
156
+ timestamp (8 bytes big-endian) + fab_digest.
157
+ """
158
+ timestamp_bytes = timestamp.to_bytes(8, byteorder="big")
159
+ return timestamp_bytes + fab_digest
160
+
161
+
162
+ def decode_base64url(sig: str) -> bytes:
163
+ """Convert signature to b64 format."""
164
+ # add missing padding (=) to a multiple of 4
165
+ pad = (-len(sig)) % 4
166
+ return base64.urlsafe_b64decode(sig + ("=" * pad))
167
+
168
+
169
+ def load_private_key(path: Path) -> ed25519.Ed25519PrivateKey:
170
+ """Load an SSH-format private key (Ed25519) using cryptography."""
171
+ key_bytes = path.read_bytes()
172
+ private_key = serialization.load_ssh_private_key(key_bytes, password=None)
173
+ if not isinstance(private_key, ed25519.Ed25519PrivateKey):
174
+ raise ValueError("Private key is not Ed25519")
175
+ return private_key
@@ -0,0 +1,159 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Mixin providing common SQLite connection and initialization logic."""
16
+
17
+
18
+ import re
19
+ import sqlite3
20
+ from abc import ABC
21
+ from collections.abc import Sequence
22
+ from logging import DEBUG, ERROR
23
+ from typing import Any
24
+
25
+ from flwr.common.logger import log
26
+
27
+ DictOrTuple = tuple[Any, ...] | dict[str, Any]
28
+
29
+
30
+ class SqliteMixin(ABC):
31
+ """Mixin providing common SQLite connection and initialization logic."""
32
+
33
+ def __init__(self, database_path: str) -> None:
34
+ self.database_path = database_path
35
+ self._conn: sqlite3.Connection | None = None
36
+
37
+ @property
38
+ def conn(self) -> sqlite3.Connection:
39
+ """Get the SQLite connection."""
40
+ if self._conn is None:
41
+ raise AttributeError("Database not initialized. Call initialize() first.")
42
+ return self._conn
43
+
44
+ def get_sql_statements(self) -> tuple[str, ...]:
45
+ """Return SQL statements for this class.
46
+
47
+ Subclasses can override this to provide their SQL CREATE statements.
48
+ The base implementation returns an empty tuple.
49
+
50
+ Returns
51
+ -------
52
+ tuple[str, ...]
53
+ SQL CREATE TABLE/INDEX statements for this class.
54
+ """
55
+ return ()
56
+
57
+ def initialize(self, log_queries: bool = False) -> list[tuple[str]]:
58
+ """Connect to the DB, enable FK support, and create tables if needed.
59
+
60
+ This method executes SQL statements returned by `get_sql_statements()`.
61
+
62
+ Parameters
63
+ ----------
64
+ log_queries : bool
65
+ Log each query which is executed.
66
+
67
+ Returns
68
+ -------
69
+ list[tuple[str]]
70
+ The list of all tables in the DB.
71
+
72
+ Examples
73
+ --------
74
+ Override `get_sql_statements()` in your subclass:
75
+
76
+ .. code:: python
77
+
78
+ def get_sql_statements(self) -> tuple[str, ...]:
79
+ return (
80
+ SQL_CREATE_TABLE_FOO,
81
+ SQL_CREATE_TABLE_BAR,
82
+ )
83
+
84
+ To include parent SQL statements, call super():
85
+
86
+ .. code:: python
87
+
88
+ def get_sql_statements(self) -> tuple[str, ...]:
89
+ return super().get_sql_statements() + (
90
+ SQL_CREATE_TABLE_FOO,
91
+ SQL_CREATE_TABLE_BAR,
92
+ )
93
+ """
94
+ self._conn = sqlite3.connect(self.database_path)
95
+ # Enable Write-Ahead Logging (WAL) for better concurrency
96
+ self._conn.execute("PRAGMA journal_mode = WAL;")
97
+ self._conn.execute("PRAGMA synchronous = NORMAL;")
98
+ self._conn.execute("PRAGMA foreign_keys = ON;")
99
+ self._conn.execute("PRAGMA cache_size = -64000;") # 64MB cache
100
+ self._conn.execute("PRAGMA temp_store = MEMORY;") # In-memory temp tables
101
+ self._conn.execute("PRAGMA mmap_size = 268435456;") # 256MB memory-mapped I/O
102
+ self._conn.row_factory = dict_factory
103
+
104
+ if log_queries:
105
+ self._conn.set_trace_callback(lambda q: log(DEBUG, q))
106
+
107
+ # Create tables and indexes
108
+ cur = self._conn.cursor()
109
+ for sql in self.get_sql_statements():
110
+ cur.execute(sql)
111
+ res = cur.execute("SELECT name FROM sqlite_schema;")
112
+ return res.fetchall()
113
+
114
+ def query(
115
+ self,
116
+ query: str,
117
+ data: Sequence[DictOrTuple] | DictOrTuple | None = None,
118
+ ) -> list[dict[str, Any]]:
119
+ """Execute a SQL query and return the results as list of dicts."""
120
+ if self._conn is None:
121
+ raise AttributeError("LinkState is not initialized.")
122
+
123
+ if data is None:
124
+ data = []
125
+
126
+ # Clean up whitespace to make the logs nicer
127
+ query = re.sub(r"\s+", " ", query)
128
+
129
+ try:
130
+ with self._conn:
131
+ if (
132
+ len(data) > 0
133
+ and isinstance(data, (tuple | list))
134
+ and isinstance(data[0], (tuple | dict))
135
+ ):
136
+ rows = self._conn.executemany(query, data)
137
+ else:
138
+ rows = self._conn.execute(query, data)
139
+
140
+ # Extract results before committing to support
141
+ # INSERT/UPDATE ... RETURNING
142
+ # style queries
143
+ result = rows.fetchall()
144
+ except KeyError as exc:
145
+ log(ERROR, {"query": query, "data": data, "exception": exc})
146
+
147
+ return result
148
+
149
+
150
+ def dict_factory(
151
+ cursor: sqlite3.Cursor,
152
+ row: sqlite3.Row,
153
+ ) -> dict[str, Any]:
154
+ """Turn SQLite results into dicts.
155
+
156
+ Less efficent for retrival of large amounts of data but easier to use.
157
+ """
158
+ fields = [column[0] for column in cursor.description]
159
+ return dict(zip(fields, row, strict=True))
@@ -18,7 +18,6 @@
18
18
  import os
19
19
  import subprocess
20
20
  from collections.abc import Sequence
21
- from typing import Optional
22
21
 
23
22
  from .exec_plugin import ExecPlugin
24
23
 
@@ -33,7 +32,7 @@ class BaseExecPlugin(ExecPlugin):
33
32
  command = ""
34
33
  appio_api_address_arg = ""
35
34
 
36
- def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
35
+ def select_run_id(self, candidate_run_ids: Sequence[int]) -> int | None:
37
36
  """Select a run ID to execute from a sequence of candidates."""
38
37
  if not candidate_run_ids:
39
38
  return None
@@ -16,8 +16,8 @@
16
16
 
17
17
 
18
18
  from abc import ABC, abstractmethod
19
- from collections.abc import Sequence
20
- from typing import Any, Callable, Optional
19
+ from collections.abc import Callable, Sequence
20
+ from typing import Any
21
21
 
22
22
  from flwr.common.typing import Run
23
23
 
@@ -36,7 +36,7 @@ class ExecPlugin(ABC):
36
36
  self.get_run = get_run
37
37
 
38
38
  @abstractmethod
39
- def select_run_id(self, candidate_run_ids: Sequence[int]) -> Optional[int]:
39
+ def select_run_id(self, candidate_run_ids: Sequence[int]) -> int | None:
40
40
  """Select a run ID to execute from a sequence of candidates.
41
41
 
42
42
  A candidate run ID is one that has at least one pending message and is
@@ -17,7 +17,7 @@
17
17
 
18
18
  import time
19
19
  from logging import WARN
20
- from typing import Any, Optional, Union
20
+ from typing import Any
21
21
 
22
22
  from flwr.common.config import get_flwr_dir
23
23
  from flwr.common.exit import ExitCode, flwr_exit, register_signal_handlers
@@ -43,14 +43,12 @@ from .plugin import ExecPlugin
43
43
 
44
44
  def run_superexec( # pylint: disable=R0913,R0914,R0917
45
45
  plugin_class: type[ExecPlugin],
46
- stub_class: Union[
47
- type[ClientAppIoStub], type[ServerAppIoStub], type[SimulationIoStub]
48
- ],
46
+ stub_class: type[ClientAppIoStub] | type[ServerAppIoStub] | type[SimulationIoStub],
49
47
  appio_api_address: str,
50
- plugin_config: Optional[dict[str, Any]] = None,
51
- flwr_dir: Optional[str] = None,
52
- parent_pid: Optional[int] = None,
53
- health_server_address: Optional[str] = None,
48
+ plugin_config: dict[str, Any] | None = None,
49
+ flwr_dir: str | None = None,
50
+ parent_pid: int | None = None,
51
+ health_server_address: str | None = None,
54
52
  ) -> None:
55
53
  """Run Flower SuperExec.
56
54
 
@@ -158,12 +156,10 @@ def run_with_deprecation_warning( # pylint: disable=R0913, R0917
158
156
  cmd: str,
159
157
  plugin_type: str,
160
158
  plugin_class: type[ExecPlugin],
161
- stub_class: Union[
162
- type[ClientAppIoStub], type[ServerAppIoStub], type[SimulationIoStub]
163
- ],
159
+ stub_class: type[ClientAppIoStub] | type[ServerAppIoStub] | type[SimulationIoStub],
164
160
  appio_api_address: str,
165
- flwr_dir: Optional[str],
166
- parent_pid: Optional[int],
161
+ flwr_dir: str | None,
162
+ parent_pid: int | None,
167
163
  warn_run_once: bool,
168
164
  ) -> None:
169
165
  """Log a deprecation warning and run the equivalent `flower-superexec` command.
flwr/supercore/utils.py CHANGED
@@ -30,3 +30,23 @@ def mask_string(value: str, head: int = 4, tail: int = 4) -> str:
30
30
  if len(value) <= head + tail:
31
31
  return value
32
32
  return f"{value[:head]}...{value[-tail:]}"
33
+
34
+
35
+ def uint64_to_int64(unsigned: int) -> int:
36
+ """Convert a uint64 integer to a sint64 with the same bit pattern.
37
+
38
+ For values >= 2^63, wraps around by subtracting 2^64.
39
+ """
40
+ if unsigned >= (1 << 63):
41
+ return unsigned - (1 << 64)
42
+ return unsigned
43
+
44
+
45
+ def int64_to_uint64(signed: int) -> int:
46
+ """Convert a sint64 integer to a uint64 with the same bit pattern.
47
+
48
+ For negative values, wraps around by adding 2^64.
49
+ """
50
+ if signed < 0:
51
+ return signed + (1 << 64)
52
+ return signed
@@ -16,14 +16,13 @@
16
16
 
17
17
 
18
18
  from abc import ABC, abstractmethod
19
- from typing import Optional
20
19
 
21
20
 
22
21
  class ArtifactProvider(ABC):
23
22
  """ArtifactProvider interface for providing artifact download links."""
24
23
 
25
24
  @abstractmethod
26
- def get_url(self, run_id: int) -> Optional[str]:
25
+ def get_url(self, run_id: int) -> str | None:
27
26
  """Return the artifact download link for the given run ID."""
28
27
 
29
28
  @property
@@ -12,15 +12,15 @@
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
14
  # ==============================================================================
15
- """Auth plugin components."""
15
+ """Account auth plugin for ControlServicer."""
16
16
 
17
17
 
18
- from .auth_plugin import CliAuthPlugin as CliAuthPlugin
19
- from .auth_plugin import ControlAuthPlugin as ControlAuthPlugin
20
- from .auth_plugin import ControlAuthzPlugin as ControlAuthzPlugin
18
+ from .auth_plugin import ControlAuthnPlugin, ControlAuthzPlugin
19
+ from .noop_auth_plugin import NoOpControlAuthnPlugin, NoOpControlAuthzPlugin
21
20
 
22
21
  __all__ = [
23
- "CliAuthPlugin",
24
- "ControlAuthPlugin",
22
+ "ControlAuthnPlugin",
25
23
  "ControlAuthzPlugin",
24
+ "NoOpControlAuthnPlugin",
25
+ "NoOpControlAuthzPlugin",
26
26
  ]
@@ -0,0 +1,88 @@
1
+ # Copyright 2025 Flower Labs GmbH. All Rights Reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ # ==============================================================================
15
+ """Abstract classes for Flower account auth plugins."""
16
+
17
+
18
+ from abc import ABC, abstractmethod
19
+ from collections.abc import Sequence
20
+ from pathlib import Path
21
+
22
+ from flwr.common.typing import (
23
+ AccountAuthCredentials,
24
+ AccountAuthLoginDetails,
25
+ AccountInfo,
26
+ )
27
+
28
+
29
+ class ControlAuthnPlugin(ABC):
30
+ """Abstract Flower Authentication Plugin class for ControlServicer.
31
+
32
+ Parameters
33
+ ----------
34
+ account_auth_config_path : Path
35
+ Path to the YAML file containing the authentication configuration.
36
+ verify_tls_cert : bool
37
+ Boolean indicating whether to verify the TLS certificate
38
+ when making requests to the server.
39
+ """
40
+
41
+ @abstractmethod
42
+ def __init__(
43
+ self,
44
+ account_auth_config_path: Path,
45
+ verify_tls_cert: bool,
46
+ ):
47
+ """Abstract constructor."""
48
+
49
+ @abstractmethod
50
+ def get_login_details(self) -> AccountAuthLoginDetails | None:
51
+ """Get the login details."""
52
+
53
+ @abstractmethod
54
+ def validate_tokens_in_metadata(
55
+ self, metadata: Sequence[tuple[str, str | bytes]]
56
+ ) -> tuple[bool, AccountInfo | None]:
57
+ """Validate authentication tokens in the provided metadata."""
58
+
59
+ @abstractmethod
60
+ def get_auth_tokens(self, device_code: str) -> AccountAuthCredentials | None:
61
+ """Get authentication tokens."""
62
+
63
+ @abstractmethod
64
+ def refresh_tokens(
65
+ self, metadata: Sequence[tuple[str, str | bytes]]
66
+ ) -> tuple[Sequence[tuple[str, str | bytes]] | None, AccountInfo | None]:
67
+ """Refresh authentication tokens in the provided metadata."""
68
+
69
+
70
+ class ControlAuthzPlugin(ABC): # pylint: disable=too-few-public-methods
71
+ """Abstract Flower Authorization Plugin class for ControlServicer.
72
+
73
+ Parameters
74
+ ----------
75
+ account_auth_config_path : Path
76
+ Path to the YAML file containing the authorization configuration.
77
+ verify_tls_cert : bool
78
+ Boolean indicating whether to verify the TLS certificate
79
+ when making requests to the server.
80
+ """
81
+
82
+ @abstractmethod
83
+ def __init__(self, account_auth_config_path: Path, verify_tls_cert: bool):
84
+ """Abstract constructor."""
85
+
86
+ @abstractmethod
87
+ def authorize(self, account_info: AccountInfo) -> bool:
88
+ """Verify account authorization request."""