Skip to content

Commit

Permalink
net.mbedtls: support Server Name Indication (SNI) (#22012)
Browse files Browse the repository at this point in the history
  • Loading branch information
martinskou committed Aug 11, 2024
1 parent ac7fedb commit d7bdb72
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 24 deletions.
22 changes: 22 additions & 0 deletions examples/ssl_server/cert/makecerts.sh
Original file line number Diff line number Diff line change
@@ -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
70 changes: 70 additions & 0 deletions examples/ssl_server/server_sni.v
Original file line number Diff line number Diff line change
@@ -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()!
}
120 changes: 120 additions & 0 deletions examples/ssl_server/server_sni_advanced.v
Original file line number Diff line number Diff line change
@@ -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
}
}
4 changes: 4 additions & 0 deletions vlib/net/mbedtls/mbedtls.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
123 changes: 99 additions & 24 deletions vlib/net/mbedtls/ssl_connection.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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')
}
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit d7bdb72

Please sign in to comment.