Basic cryptography, SSL improvements
By: Fabio Alessandrelli 28 September 2019
As part of the MOSS project sponsored by Mozilla, during July I worked on some new features regarding cryptography and SSL to improve the quality and security of Godot networking.
Certificates and keys as resources
SSL certificates (in the form of
*.crt files) and private keys (in the form of
*.key files) are now treated as resources in Godot. This means they will be exported automatically and they can be loaded via the GDScript
Crypto class was introduced that allows to access some basic cryptographic functions.
- You can generate cryptographically secure random bytes via the
generate_random_bytes()function. The bytes are returned in a
- You can generate RSA keys that can be used by
StreamPeerSSLto act as a server.
- You can generate SSL self-signed certificates that again, can be used by
StreamPeerSSLto act as a server.
HashingContext class now provides an interface for computing cryptographic hashes (MD5, SHA-1, SHA-256) over multiple iterations.
This is useful for example when computing hashes of big files (so you don't have to load them all in memory), network streams, and data streams in general (so you don't have to hold buffers). Here is an example of how it works:
const CHUNK_SIZE = 1024 func hash_file(path): var ctx = HashingContext.new() var file = File.new() # Start a SHA-256 context. ctx.start(HashingContext.HASH_SHA256) # Check that file exists. if not file.file_exists(path): return # Open the file to hash. file.open(path, File.READ) # Update the context after reading each chunk. while not file.eof_reached(): ctx.update(file.get_buffer(CHUNK_SIZE)) # Get the computed hash. var res = ctx.finish() # Print the result as hex string and array. printt(res.hex_encode(), Array(res))
StreamPeerSSL can now use a per-object SSL certificate (i.e. you no longer have to set the trusted certificates om project settings), you can specify the valid certificate by passing an
X509Certificate as last parameter in
StreamPeerSSL can now act as a server. The new
accept_stream() function, which accepts a private key, a certificate, and an optional CA chain, will try to establish a connection with the given stream acting as a server. This will soon also allow us to support acting as a WebSocket server over TLS.
Here is an example of a test HTTPS server made in GDScript... not meant to be used in production ;-)
extends Node # A class that represents a client accepted by our server. class Client extends Reference: # The SSL stream of this client. var ssl = StreamPeerSSL.new() # Received request. var recv = "" # Set the stream for this client. func set_stream(stream, key, cert): ssl.blocking_handshake = false ssl.accept_stream(stream, key, cert) # Process network operations for this client. func process(): if ssl.get_status() == StreamPeerSSL.STATUS_HANDSHAKING: # Still performing handshake. ssl.poll() return if ssl.get_status() != StreamPeerSSL.STATUS_CONNECTED: # Disconnected. return ssl.poll() # Read available bytes. if ssl.get_available_bytes() > 0: recv += ssl.get_data(ssl.get_available_bytes()).get_string_from_utf8() # Send response if request is complete. if recv.ends_with("\r\n\r\n"): ssl.put_data(("HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n" + \ "<h2>Godot TLS Test Server</h2>\r\n" + \ "<p>Successful connection using SSL</p>\r\n").to_utf8()) ssl.disconnect_from_stream() func is_disconnected(): return ssl.get_status() != StreamPeerSSL.STATUS_HANDSHAKING and \ ssl.get_status() != StreamPeerSSL.STATUS_CONNECTED # Our TCP server. var _server = TCP_Server.new() # A list of connected clients. var _clients =  # Our private key and certificate. var _key = null var _cert = null func _ready(): var crypto = Crypto.new() # Generate an RSA key (this should be done in a thread to avoid blocking). _key = crypto.generate_rsa(4096) # Generate a self signed certificate to use with our server. _cert = crypto.generate_self_signed_certificate(_key, "CN=example.com,O=A Game Company,C=IT") # Start listening on "*:4343". _server.listen(4343) func _process(delta): # Take new connections. if _server.is_connection_available(): var c = Client.new() c.set_stream(_server.take_connection(), _key, _cert) _clients.append(c) # Take note of disconnected clients. var to_rem =  # Process clients and send response when done. for c in _clients: c.process() if c.is_disconnected(): to_rem.append(c) # Remove disconnected clients. for c in to_rem: _clients.erase(c)
This has been quite a long work, and included some refactoring of the
core code to use a single library for cryptography (mbedTLS) instead of multiple specific libraries for hashing algorithms and AES. This work will allow us to introduce support for AES encryption and more at scripting level in future versions.
Additionally, the SSL overhaul helped a lot in developing the upcoming DTLS implementation.