diff --git a/examples/ssl_server/cert/makecerts.sh b/examples/ssl_server/cert/makecerts.sh new file mode 100644 index 0000000000..2343dfdfab --- /dev/null +++ b/examples/ssl_server/cert/makecerts.sh @@ -0,0 +1,22 @@ +# generates the certificates used by the server_sni_advanced.v example + +# default +gen_key type=rsa rsa_keysize=4096 filename=0x.dk.key +cert_write selfsign=1 issuer_key=0x.dk.key \ + issuer_name=CN=0x.dk,O=0x,C=DK \ + not_before=20130101000000 not_after=20251231235959 \ + is_ca=1 max_pathlen=0 output_file=0x.dk.crt + +# 1x.dk +gen_key type=rsa rsa_keysize=4096 filename=1x.dk.key +cert_write selfsign=1 issuer_key=1x.dk.key \ + issuer_name=CN=1x.dk,O=1x.dk,C=DK \ + not_before=20130101000000 not_after=20251231235959 \ + is_ca=1 max_pathlen=0 output_file=1x.dk.crt + +# 2x.dk +gen_key type=rsa rsa_keysize=4096 filename=2x.dk.key +cert_write selfsign=1 issuer_key=2x.dk.key \ + issuer_name=CN=2x.dk,O=2x.dk,C=DK \ + not_before=20130101000000 not_after=20251231235959 \ + is_ca=1 max_pathlen=0 output_file=2x.dk.crt diff --git a/examples/ssl_server/server_sni.v b/examples/ssl_server/server_sni.v new file mode 100644 index 0000000000..210dc17da9 --- /dev/null +++ b/examples/ssl_server/server_sni.v @@ -0,0 +1,70 @@ +import io +import os +import net.mbedtls + +// This example shows a very minimal HTTP server, that supports SNI. +// Server Name Indication (SNI) is defined in RFC 6066. +// See https://mbed-tls.readthedocs.io/en/latest/kb/how-to/use-sni/ + +// Use the following commands, in separate shells, to test that it works, +// after you start the server: +// curl -ik --resolve 1x.dk:8443:0.0.0.0 https://1x.dk:8443/abcd +// curl -ik --resolve example.com:8443:0.0.0.0 https://example.com:8443/xyz +// Both of them should work, and the server should show that it tried to load +// certificates, responding to the given domain name in each of the requests. + +// callback to return the certificates to use for the connection +fn get_cert(mut l mbedtls.SSLListener, host string) !&mbedtls.SSLCerts { + println('reading certificates for ${host} ...') + + // For our example, just always use the same certificates. In a real server, + // that should be dependent on the host parameter, and perhaps there should + // be some caching, so that they are not read each time for each request from + // the filesystem. + + cert := os.read_file('cert/server.crt') or { + return error('Failed to read certificate ${host}: ${err}') + } + key := os.read_file('cert/server.key') or { return error('Failed to read key ${host}: ${err}') } + println('read certs for ${host}') + + if mut c := mbedtls.new_sslcerts_in_memory('', cert, key) { + return c + } else { + return error('mbedtls.new_sslcerts_in_memory err: ${err}') + } +} + +fn main() { + mut server := mbedtls.new_ssl_listener('0.0.0.0:8443', mbedtls.SSLConnectConfig{ + verify: os.resource_abs_path('cert/ca.crt') + cert: os.resource_abs_path('cert/server.crt') + cert_key: os.resource_abs_path('cert/server.key') + validate: false + get_certificate: get_cert // set the SNI callback to enable this feature + })! + println('Listening on https://0.0.0.0:8443/ ...') + for { + mut client := server.accept() or { + println('accept error: ${err}') + continue + } + mut reader := io.new_buffered_reader(reader: client) + for { + line := reader.read_line() or { break } + if line.len == 0 { + break + } + println(line) + } + client.write_string('HTTP/1.1 200 OK\r\n') or { continue } + + client.write_string('Content-Type: text/html\r\n') or { continue } + client.write_string('Connection: close\r\n') or { continue } + client.write_string('\r\n') or { continue } + + client.write_string('Hello\n') or { continue } + client.shutdown()! + } + server.shutdown()! +} diff --git a/examples/ssl_server/server_sni_advanced.v b/examples/ssl_server/server_sni_advanced.v new file mode 100644 index 0000000000..4a4bdf9e40 --- /dev/null +++ b/examples/ssl_server/server_sni_advanced.v @@ -0,0 +1,120 @@ +import net.mbedtls +import os +import io +import net.http + +// first run cert/makecerts.sh to generate the certificates for this server + +// start the server with: +// v run server_sni_advanced.v + +// test using curl: + +// connection using the 1x.dk certificate +// curl -vik --resolve 1x.dk:8443:0.0.0.0 https://1x.dk:8443/ + +// connection using the 2x.dk certificate +// curl -vik --resolve 2x.dk:8443:0.0.0.0 https://2x.dk:8443/ + +// default certificate (no sni callback) +// curl -vik https://0.0.0.0:8443/ + +@[heap] +struct CertManager { +mut: + // cache for the certificates + certs map[string]&mbedtls.SSLCerts +} + +fn (mut cm CertManager) get_cert(mut l mbedtls.SSLListener, host string) !&mbedtls.SSLCerts { + println('${host}') + + if c := cm.certs[host] { + println('read certs for ${host} already ready') + return c + } else { + cert := os.read_file('cert/${host}.crt') or { + return error('Failed to read certificate ${host}: ${err}') + } + key := os.read_file('cert/${host}.key') or { + return error('Failed to read key ${host}: ${err}') + } + println('read certs for ${host}') + + if mut c := mbedtls.new_sslcerts_in_memory('', cert, key) { + cm.certs[host] = c + return c + } else { + return error('mbedtls.new_sslcerts_in_memory err: ${err}') + } + } +} + +fn main() { + // Load the default certificates + cert := os.read_file('cert/0x.dk.crt') or { + eprintln('Failed to read certificate: ${err}') + return + } + key := os.read_file('cert/0x.dk.key') or { + eprintln('Failed to read key: ${err}') + return + } + + cm := CertManager{} + + // Create the SSL configuration + mut config := mbedtls.SSLConnectConfig{ + cert: cert + cert_key: key + in_memory_verification: true // !importent + get_certificate: cm.get_cert + } + + mut server := mbedtls.new_ssl_listener('0.0.0.0:8443', config) or { + println('new_ssl_listener : ${err.msg()} : ${err.code()}') + return + } + println('Listening on https://0.0.0.0:8443') + + // Accept and handle connections + for { + mut client := server.accept() or { + println('accept : ${err.msg()} : ${err.code()}') + continue + } + go handle_connection(mut client) + } + + server.shutdown() or { + println('server.shutdown : ${err.msg()} : ${err.code()}') + return + } +} + +fn handle_connection(mut ssl_conn mbedtls.SSLConn) { + mut reader := io.new_buffered_reader(reader: ssl_conn) + + request := http.parse_request(mut reader) or { + println('parse_request failed : ${err}') + return + } + + println('Received request: ${request.url}') + + // Respond to the request + body := 'Hello, HTTPS!' + res := http.new_response(http.ResponseConfig{ + body: body + }) + + ssl_conn.write(res.bytes()) or { + eprintln('Failed to write response: ${err}') + return + } + + ssl_conn.shutdown() or { + eprintln('Failed to shutdown connection: ${err}') + return + } +} diff --git a/vlib/net/mbedtls/mbedtls.c.v b/vlib/net/mbedtls/mbedtls.c.v index a84fbd4351..5051e46a5c 100644 --- a/vlib/net/mbedtls/mbedtls.c.v +++ b/vlib/net/mbedtls/mbedtls.c.v @@ -182,6 +182,10 @@ fn C.mbedtls_ssl_free(&C.mbedtls_ssl_context) fn C.mbedtls_ssl_config_init(&C.mbedtls_ssl_config) fn C.mbedtls_ssl_config_defaults(&C.mbedtls_ssl_config, int, int, int) int fn C.mbedtls_ssl_config_free(&C.mbedtls_ssl_config) +fn C.mbedtls_ssl_conf_sni(&C.mbedtls_ssl_config, fn (voidptr, &C.mbedtls_ssl_context, &char, int) int, voidptr) +fn C.mbedtls_ssl_set_hs_ca_chain(&C.mbedtls_ssl_config, &C.mbedtls_x509_crt, &C.mbedtls_x509_crl) +fn C.mbedtls_ssl_set_hs_own_cert(&C.mbedtls_ssl_context, &C.mbedtls_x509_crt, &C.mbedtls_pk_context) int +fn C.mbedtls_ssl_set_hs_authmode(&C.mbedtls_ssl_context, int) fn C.mbedtls_pk_init(&C.mbedtls_pk_context) fn C.mbedtls_pk_free(&C.mbedtls_pk_context) diff --git a/vlib/net/mbedtls/ssl_connection.c.v b/vlib/net/mbedtls/ssl_connection.c.v index a7d049ff0e..c15e3cf897 100644 --- a/vlib/net/mbedtls/ssl_connection.c.v +++ b/vlib/net/mbedtls/ssl_connection.c.v @@ -24,12 +24,84 @@ fn init() { } } -struct SSLCerts { +// SSLCerts represents a pair of CA and client certificates + key +pub struct SSLCerts { +pub mut: cacert C.mbedtls_x509_crt client_cert C.mbedtls_x509_crt client_key C.mbedtls_pk_context } +// new_sslcerts initializes and returns a pair of SSL certificates and key +pub fn new_sslcerts() &SSLCerts { + mut certs := SSLCerts{} + C.mbedtls_x509_crt_init(&certs.cacert) + C.mbedtls_x509_crt_init(&certs.client_cert) + C.mbedtls_pk_init(&certs.client_key) + return &certs +} + +// new_sslcerts_in_memory creates a pair of SSL certificates, given their contents (not paths). +pub fn new_sslcerts_in_memory(verify string, cert string, cert_key string) !&SSLCerts { + mut certs := new_sslcerts() + if verify != '' { + ret := C.mbedtls_x509_crt_parse(&certs.cacert, verify.str, verify.len + 1) + if ret != 0 { + return error_with_code('mbedtls_x509_crt_parse error', ret) + } + } + if cert != '' { + ret := C.mbedtls_x509_crt_parse(&certs.client_cert, cert.str, cert.len + 1) + if ret != 0 { + return error_with_code('mbedtls_x509_crt_parse error', ret) + } + } + if cert_key != '' { + unsafe { + ret := C.mbedtls_pk_parse_key(&certs.client_key, cert_key.str, cert_key.len + 1, + 0, 0, C.mbedtls_ctr_drbg_random, &mbedtls.ctr_drbg) + if ret != 0 { + return error_with_code('v error', ret) + } + } + } + return certs +} + +// new_sslcerts_from_file creates a new pair of SSL certificates, given their paths on the filesystem. +pub fn new_sslcerts_from_file(verify string, cert string, cert_key string) !&SSLCerts { + mut certs := new_sslcerts() + if verify != '' { + ret := C.mbedtls_x509_crt_parse_file(&certs.cacert, &char(verify.str)) + if ret != 0 { + return error_with_code('mbedtls_x509_crt_parse error', ret) + } + } + if cert != '' { + ret := C.mbedtls_x509_crt_parse_file(&certs.client_cert, &char(cert.str)) + if ret != 0 { + return error_with_code('mbedtls_x509_crt_parse error', ret) + } + } + if cert_key != '' { + unsafe { + ret := C.mbedtls_pk_parse_keyfile(&certs.client_key, &char(cert_key.str), + 0, C.mbedtls_ctr_drbg_random, &mbedtls.ctr_drbg) + if ret != 0 { + return error_with_code('v error', ret) + } + } + } + return certs +} + +// cleanup frees the SSL certificates +pub fn (mut c SSLCerts) cleanup() { + C.mbedtls_x509_crt_free(&c.cacert) + C.mbedtls_x509_crt_free(&c.client_cert) + C.mbedtls_pk_free(&c.client_key) +} + // SSLConn is the current connection pub struct SSLConn { pub: @@ -77,9 +149,7 @@ pub fn (mut l SSLListener) shutdown() ! { eprintln(@METHOD) } if unsafe { l.certs != nil } { - C.mbedtls_x509_crt_free(&l.certs.cacert) - C.mbedtls_x509_crt_free(&l.certs.client_cert) - C.mbedtls_pk_free(&l.certs.client_key) + l.certs.cleanup() } C.mbedtls_ssl_free(&l.ssl) C.mbedtls_ssl_config_free(&l.conf) @@ -115,28 +185,12 @@ fn (mut l SSLListener) init() ! { mut ret := 0 if l.config.in_memory_verification { - if l.config.verify != '' { - ret = C.mbedtls_x509_crt_parse(&l.certs.cacert, l.config.verify.str, - l.config.verify.len + 1) - } - if l.config.cert != '' { - ret = C.mbedtls_x509_crt_parse(&l.certs.client_cert, l.config.cert.str, - l.config.cert.len + 1) - } - if l.config.cert_key != '' { - unsafe { - ret = C.mbedtls_pk_parse_key(&l.certs.client_key, l.config.cert_key.str, - l.config.cert_key.len + 1, 0, 0, C.mbedtls_ctr_drbg_random, &mbedtls.ctr_drbg) - } + l.certs = new_sslcerts_in_memory(l.config.verify, l.config.cert, l.config.cert_key) or { + return error('Cert failure') } } else { - if l.config.verify != '' { - ret = C.mbedtls_x509_crt_parse_file(&l.certs.cacert, &char(l.config.verify.str)) - } - ret = C.mbedtls_x509_crt_parse_file(&l.certs.client_cert, &char(l.config.cert.str)) - unsafe { - ret = C.mbedtls_pk_parse_keyfile(&l.certs.client_key, &char(l.config.cert_key.str), - 0, C.mbedtls_ctr_drbg_random, &mbedtls.ctr_drbg) + l.certs = new_sslcerts_from_file(l.config.verify, l.config.cert, l.config.cert_key) or { + return error('Cert failure') } } @@ -174,6 +228,25 @@ fn (mut l SSLListener) init() ! { if ret != 0 { return error_with_code("can't setup ssl", ret) } + + if get_cert_callback := l.config.get_certificate { + l.init_sni(get_cert_callback) + } +} + +// setup SNI callback +fn (mut l SSLListener) init_sni(get_cert_callback fn (mut SSLListener, string) !&SSLCerts) { + $if trace_ssl ? { + eprintln(@METHOD) + } + C.mbedtls_ssl_conf_sni(&l.conf, fn [get_cert_callback, mut l] (p_info voidptr, ssl &C.mbedtls_ssl_context, name &char, lng int) int { + host := unsafe { name.vstring_literal_with_len(lng) } + if certs := get_cert_callback(mut l, host) { + return C.mbedtls_ssl_set_hs_own_cert(ssl, &certs.client_cert, &certs.client_key) + } else { + return -1 + } + }, &l.conf) } // accepts a new connection and returns a SSLConn of the connected client @@ -223,6 +296,8 @@ pub: validate bool // set this to true, if you want to stop requests, when their certificates are found to be invalid in_memory_verification bool // if true, verify, cert, and cert_key are read from memory, not from a file + + get_certificate ?fn (mut SSLListener, string) !&SSLCerts } // new_ssl_conn returns a new SSLConn with the given config.