Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preparing to enable SSL/TLS for gRPC server #842

Merged
merged 44 commits into from
Jan 5, 2022
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
52b412b
Enable SSL/TLS for gRPC server
tanertopal Sep 13, 2021
0b241cd
Update src/py/flwr/server/grpc_server/certificates/__init__.py
tanertopal Sep 14, 2021
96bb311
Simplify
tanertopal Sep 14, 2021
4f65baa
Improve docstring
tanertopal Sep 14, 2021
9ac312a
Fix certificates generation
tanertopal Sep 14, 2021
5d73e2c
Fix docstring
tanertopal Sep 14, 2021
9c5d9cf
Improve docstring
tanertopal Sep 14, 2021
133ae5b
Fix test
tanertopal Sep 14, 2021
05399ad
Fix filename
tanertopal Sep 14, 2021
b8ea6b5
Merge branch 'main' into secure_grpc_server
danieljanes Sep 16, 2021
c880321
Merge branch 'main' into secure_grpc_server
tanertopal Sep 16, 2021
d62861e
Merge branch 'main' into secure_grpc_server
tanertopal Sep 16, 2021
a20f8a9
Merge branch 'main' into secure_grpc_server
tanertopal Sep 17, 2021
0465cf4
Merge branch 'main' into secure_grpc_server
tanertopal Sep 24, 2021
5af0341
Merge branch 'main' into secure_grpc_server
tanertopal Nov 14, 2021
8fd1e17
Merge branch 'main' into secure_grpc_server
tanertopal Nov 14, 2021
4825263
Update certificates.py
tanertopal Nov 14, 2021
025e997
Update pip and setuptools
tanertopal Nov 14, 2021
9ff1607
Update certificates.py
tanertopal Nov 14, 2021
11c4dfc
Create certificates.py
tanertopal Nov 14, 2021
a5d4172
Merge branch 'secure_grpc_server' of github.com:adap/flower into secu…
tanertopal Nov 15, 2021
71518e8
Update src/py/flwr/server/grpc_server/grpc_server.py
tanertopal Dec 6, 2021
e3a961d
Update src/py/flwr/server/grpc_server/grpc_server.py
tanertopal Dec 6, 2021
f7eb017
Merge branch 'main' into secure_grpc_server
tanertopal Dec 6, 2021
83924aa
Move certificate script into dev
tanertopal Dec 7, 2021
8e574db
Merge branch 'secure_grpc_server' of github.com:adap/flower into secu…
tanertopal Dec 7, 2021
26d4435
Merge branch 'main' into secure_grpc_server
tanertopal Jan 5, 2022
7b166d5
Update generate.sh
tanertopal Jan 5, 2022
7fbb429
Update .gitignore
tanertopal Jan 5, 2022
4ac795c
Update generate.sh
tanertopal Jan 5, 2022
06c8a4b
Update grpc_server_test.py
tanertopal Jan 5, 2022
ef37508
Update src/py/flwr/server/grpc_server/grpc_server.py
tanertopal Jan 5, 2022
ff54e50
Improve tests for function
tanertopal Jan 5, 2022
93f9195
Merge branch 'secure_grpc_server' of github.com:adap/flower into secu…
tanertopal Jan 5, 2022
9439dc5
Merge branch 'main' into secure_grpc_server
tanertopal Jan 5, 2022
d6246fd
Update src/py/flwr/server/grpc_server/grpc_server.py
danieljanes Jan 5, 2022
f2fcdf2
Change API
tanertopal Jan 5, 2022
dcf8a7a
Fix
tanertopal Jan 5, 2022
32f2d36
Update src/py/flwr/server/grpc_server/grpc_server.py
tanertopal Jan 5, 2022
db60b0f
Fix docs
tanertopal Jan 5, 2022
970c07d
Update src/py/flwr/server/grpc_server/grpc_server.py
danieljanes Jan 5, 2022
bcd4af7
Fix mypy error
tanertopal Jan 5, 2022
0884327
Fix
tanertopal Jan 5, 2022
70e3a36
fix
tanertopal Jan 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ data/
doc/source/api_documentation
doc/source/_build
flwr_logs
.cache

# Datasets
cifar-10-python.tar.gz
Expand Down
20 changes: 20 additions & 0 deletions dev/certificates/certificate.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[req]
default_bits = 4096
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn

[dn]
C = DE
ST = HH
O = Flower
CN = localhost

[req_ext]
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
IP.1 = ::1
IP.2 = 127.0.0.1
52 changes: 52 additions & 0 deletions dev/certificates/generate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/bin/bash
# This script will generate all certificates if ca.crt does not exist

set -e
cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/../../

CA_PASSWORD=notsafe

CERT_DIR=.cache/certificates

# Generate directories if not exists
mkdir -p .cache/certificates

# if [ -f ".cache/certificates/ca.crt" ]; then
# echo "Skipping certificate generation as they already exist."
# exit 0
# fi

rm -f $CERT_DIR/*

# Generate the root certificate authority key and certificate based on key
openssl genrsa -out $CERT_DIR/ca.key 4096
openssl req \
-new \
-x509 \
-key $CERT_DIR/ca.key \
-sha256 \
-subj "/C=DE/ST=HH/O=CA, Inc." \
-days 365 -out $CERT_DIR/ca.crt

# Generate a new private key for the server
openssl genrsa -out $CERT_DIR/server.key 4096

# Create a signing CSR
openssl req \
-new \
-key $CERT_DIR/server.key \
-out $CERT_DIR/server.csr \
-config ./dev/certificates/certificate.conf

# Generate a certificate for the server
openssl x509 \
-req \
-in $CERT_DIR/server.csr \
-CA $CERT_DIR/ca.crt \
-CAkey $CERT_DIR/ca.key \
-CAcreateserial \
-out $CERT_DIR/server.pem \
-days 365 \
-sha256 \
-extfile ./dev/certificates/certificate.conf \
-extensions req_ext
4 changes: 2 additions & 2 deletions src/py/flwr/client/grpc_client/connection_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

from flwr.proto.transport_pb2 import ClientMessage, ServerMessage
from flwr.server.client_manager import SimpleClientManager
from flwr.server.grpc_server.grpc_server import start_insecure_grpc_server
from flwr.server.grpc_server.grpc_server import start_grpc_server

from .connection import insecure_grpc_connection

Expand Down Expand Up @@ -82,7 +82,7 @@ def test_integration_connection() -> None:
# Prepare
port = unused_tcp_port()

server = start_insecure_grpc_server(
server = start_grpc_server(
client_manager=SimpleClientManager(), server_address=f"[::]:{port}"
)

Expand Down
4 changes: 2 additions & 2 deletions src/py/flwr/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from flwr.common import GRPC_MAX_MESSAGE_LENGTH
from flwr.common.logger import log
from flwr.server.client_manager import SimpleClientManager
from flwr.server.grpc_server.grpc_server import start_insecure_grpc_server
from flwr.server.grpc_server.grpc_server import start_grpc_server
from flwr.server.history import History
from flwr.server.server import Server
from flwr.server.strategy import FedAvg, Strategy
Expand Down Expand Up @@ -70,7 +70,7 @@ def start_server( # pylint: disable=too-many-arguments
initialized_server, initialized_config = _init_defaults(server, config, strategy)

# Start gRPC server
grpc_server = start_insecure_grpc_server(
grpc_server = start_grpc_server(
client_manager=initialized_server.client_manager(),
server_address=server_address,
max_message_length=grpc_max_message_length,
Expand Down
90 changes: 84 additions & 6 deletions src/py/flwr/server/grpc_server/grpc_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,45 +12,102 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
"""Implements utility function to create a grpc server."""
"""Implements utility function to create a gRPC server."""


import concurrent.futures
import sys
from logging import ERROR
from pathlib import Path
from typing import ByteString, Optional, Tuple, Union

import grpc

from flwr.common import GRPC_MAX_MESSAGE_LENGTH
from flwr.common.logger import log
from flwr.proto import transport_pb2_grpc
from flwr.server.client_manager import ClientManager
from flwr.server.grpc_server import flower_service_servicer as fss

FILELIKE = Union[str, bytes]
SSLFILES = Tuple[FILELIKE, FILELIKE, FILELIKE]

INVALID_SSL_FILES_ERR_MSG = """
When setting any of root_certificate, certificate, or private_key,
all of them need to be set.
"""


def read_to_byte_string(file_like: FILELIKE) -> ByteString:
"""Read file_like and return as ByteString."""
with open(file_like, "rb") as file:
return file.read()


def valid_ssl_files(ssl_files: SSLFILES) -> bool:
"""Validate type SSLFILES."""
all_files = [Path(ssl_file).is_file() for ssl_file in ssl_files]
is_valid = all(all_files) and len(all_files) == 3

if not is_valid:
log(ERROR, INVALID_SSL_FILES_ERR_MSG)

return is_valid

def start_insecure_grpc_server(

def start_grpc_server(
client_manager: ClientManager,
server_address: str,
max_concurrent_workers: int = 1000,
max_message_length: int = GRPC_MAX_MESSAGE_LENGTH,
ssl_files: Optional[SSLFILES] = None,
) -> grpc.Server:
"""Create grpc server and return registered FlowerServiceServicer instance.
"""Create gRPC server and return instance of grpc.Server.

If used in a main function server.wait_for_termination(timeout=None)
should be called as otherwise the server will immediately stop.

**SSL/TLS**
To enable SSL/TLS you have to pass all of root_certificate, certificate,
and private_key. Setting only some will make the process exit with code 1.

Parameters
----------
client_manager : ClientManager
Instance of ClientManager
server_address : str
Server address in the form of HOST:PORT e.g. "[::]:8080"
max_concurrent_workers : int
Set the maximum number of clients you want the server to process
before returning RESOURCE_EXHAUSTED status (default: 1000)
Maximum number of clients the server can process before returning
RESOURCE_EXHAUSTED status (default: 1000)
max_message_length : int
Maximum message length that the server can send or receive.
Int valued in bytes. -1 means unlimited. (default: GRPC_MAX_MESSAGE_LENGTH)
ssl_files : Tuple[Union[str, bytes]]
Tuple containing root certificate, server certificate, and private key to start
a secure SSL/TLS server. The tuple is expected to have three file-like
elements in the following order:

* CA certificate.
* server certificate.
* server private key.

(default: None)

Returns
-------
server : grpc.Server
An instance of a gRPC server which is already started

Examples
--------
Starting a SSL/TLS enabled server.

>>> start_grpc_server(
>>> client_manager=ClientManager(),
>>> server_address="localhost:8080",
>>> ssl_files=("/crts/root.pem", "/crts/localhost.crt", "/crts/localhost.key")
>>> )
"""
server = grpc.server(
concurrent.futures.ThreadPoolExecutor(max_workers=max_concurrent_workers),
Expand All @@ -73,7 +130,28 @@ def start_insecure_grpc_server(
servicer = fss.FlowerServiceServicer(client_manager)
transport_pb2_grpc.add_FlowerServiceServicer_to_server(servicer, server)

server.add_insecure_port(server_address)
if ssl_files is not None:
if not valid_ssl_files(ssl_files):
sys.exit(1)

root_certificate_b, certificate_b, private_key_b = [
read_to_byte_string(file_path) for file_path in ssl_files
]

server_credentials = grpc.ssl_server_credentials(
((private_key_b, certificate_b),),
root_certificates=root_certificate_b,
# A boolean indicating whether or not to require clients to be
# authenticated. May only be True if root_certificates is not None.
# We are explicitly setting the current gRPC default to document
# the option. For further reference see:
# https://grpc.github.io/grpc/python/grpc.html#create-server-credentials
require_client_auth=False,
)
server.add_secure_port(server_address, server_credentials)
else:
server.add_insecure_port(server_address)

server.start()

return server
72 changes: 68 additions & 4 deletions src/py/flwr/server/grpc_server/grpc_server_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,32 @@
"""Tests for module server."""

import socket
import subprocess
from contextlib import closing
from typing import cast
from os.path import abspath, dirname, join
from typing import Tuple, cast

from flwr.server.client_manager import SimpleClientManager
from flwr.server.grpc_server.grpc_server import start_insecure_grpc_server
from flwr.server.grpc_server.grpc_server import start_grpc_server, valid_ssl_files

root_dir = dirname(abspath(join(__file__, "../../../../..")))


def load_certificates() -> Tuple[str, str, str]:
"""Generate and load SSL/TLS credentials/certificates.

Utility function for loading for SSL/TLS enabled gRPC servertests.
"""
# Trigger script which generates the certificates
subprocess.run(["bash", "./dev/certificates/generate.sh"], check=True, cwd=root_dir)

ssl_files = (
join(root_dir, ".cache/certificates/ca.crt"),
join(root_dir, ".cache/certificates/server.pem"),
join(root_dir, ".cache/certificates/server.key"),
)

return ssl_files


def unused_tcp_port() -> int:
Expand All @@ -30,16 +51,59 @@ def unused_tcp_port() -> int:
return cast(int, sock.getsockname()[1])


def test_integration_start_and_shutdown_server() -> None:
def test_valid_ssl_files_correctly() -> None:
"""Test is validation function works correctly when passed valid list."""
# Prepare
ssl_files = load_certificates()

# Execute
is_valid = valid_ssl_files(ssl_files)

# Assert
assert is_valid


def test_valid_ssl_files_correctly() -> None:
"""Test is validation function works correctly when passed invalid list."""
# Prepare
ssl_files = ["/dev/null", "/dev/null"]

# Execute
is_valid = valid_ssl_files(ssl_files)

# Assert
assert not is_valid


def test_integration_start_and_shutdown_insecure_server() -> None:
"""Create server and check if FlowerServiceServicer is returned."""
# Prepare
port = unused_tcp_port()
client_manager = SimpleClientManager()

# Execute
server = start_insecure_grpc_server(
server = start_grpc_server(
client_manager=client_manager, server_address=f"[::]:{port}"
)

# Teardown
server.stop(1)


def test_integration_start_and_shutdown_secure_server() -> None:
"""Create server and check if FlowerServiceServicer is returned."""
# Prepare
port = unused_tcp_port()
client_manager = SimpleClientManager()

ssl_files = load_certificates()

# Execute
server = start_grpc_server(
client_manager=client_manager,
server_address=f"[::]:{port}",
ssl_files=ssl_files,
)

# Teardown
server.stop(1)