diff options
Diffstat (limited to 'packages/meshbay-node/src/meshbay_node/transport/tls_cert.py')
| -rw-r--r-- | packages/meshbay-node/src/meshbay_node/transport/tls_cert.py | 95 |
1 files changed, 95 insertions, 0 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/transport/tls_cert.py b/packages/meshbay-node/src/meshbay_node/transport/tls_cert.py new file mode 100644 index 0000000..1354ac9 --- /dev/null +++ b/packages/meshbay-node/src/meshbay_node/transport/tls_cert.py @@ -0,0 +1,95 @@ +""" +Self-signed TLS certificate generation for the node. + +The cert is used for transport confidentiality only. +Node identity is verified via Ed25519 PK (from hub), not TLS cert chain. +Clients connect with ssl.CERT_NONE + verify Ed25519 at the MNP handshake layer. + +Certificate is generated once and cached at ~/.config/meshbay/node_tls.pem/.key. +""" + +import logging +import os +from pathlib import Path +import ssl +import datetime +import ipaddress + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID + +log = logging.getLogger(__name__) + +DEFAULT_CERT = Path.home() / ".config" / "meshbay" / "node_tls.crt" +DEFAULT_KEY = Path.home() / ".config" / "meshbay" / "node_tls.key" + + +def generate_self_signed_cert( + cert_path: Path = DEFAULT_CERT, + key_path: Path = DEFAULT_KEY, +) -> tuple[Path, Path]: + """Generate a self-signed RSA-2048 TLS cert valid for 10 years.""" + cert_path.parent.mkdir(parents=True, exist_ok=True) + + rsa_key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, "meshbay-node"), + ]) + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(rsa_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime.now(datetime.timezone.utc)) + .not_valid_after(datetime.datetime.now(datetime.timezone.utc) + + datetime.timedelta(days=3650)) + .add_extension( + x509.SubjectAlternativeName([ + x509.DNSName("localhost"), + x509.IPAddress(ipaddress.IPv4Address("127.0.0.1")), + ]), + critical=False, + ) + .sign(rsa_key, hashes.SHA256()) + ) + + cert_path.write_bytes(cert.public_bytes(serialization.Encoding.PEM)) + key_path.write_bytes(rsa_key.private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.TraditionalOpenSSL, + serialization.NoEncryption(), + )) + cert_path.chmod(0o644) + key_path.chmod(0o600) + log.info("TLS cert generated: %s", cert_path) + return cert_path, key_path + + +def server_ssl_context( + cert_path: Path = DEFAULT_CERT, + key_path: Path = DEFAULT_KEY, +) -> ssl.SSLContext: + """SSL context for the node's TCP server.""" + if not cert_path.exists() or not key_path.exists(): + generate_self_signed_cert(cert_path, key_path) + + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ctx.load_cert_chain(certfile=cert_path, keyfile=key_path) + ctx.minimum_version = ssl.TLSVersion.TLSv1_3 + return ctx + + +def client_ssl_context() -> ssl.SSLContext: + """ + SSL context for clients connecting to a node. + CERT_NONE because we verify node identity via Ed25519 PK at the MNP layer. + """ + ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + ctx.minimum_version = ssl.TLSVersion.TLSv1_3 + return ctx |