net.mbedtls: support Server Name Indication (SNI) (#22012)

This commit is contained in:
Martin Skou 2024-08-11 19:26:13 +02:00 committed by GitHub
parent ac7fedba29
commit d7bdb72b48
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 315 additions and 24 deletions

View File

@ -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

View File

@ -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()!
}

View File

@ -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
}
}

View File

@ -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_init(&C.mbedtls_ssl_config)
fn C.mbedtls_ssl_config_defaults(&C.mbedtls_ssl_config, int, int, int) int 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_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_init(&C.mbedtls_pk_context)
fn C.mbedtls_pk_free(&C.mbedtls_pk_context) fn C.mbedtls_pk_free(&C.mbedtls_pk_context)

View File

@ -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 cacert C.mbedtls_x509_crt
client_cert C.mbedtls_x509_crt client_cert C.mbedtls_x509_crt
client_key C.mbedtls_pk_context 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 // SSLConn is the current connection
pub struct SSLConn { pub struct SSLConn {
pub: pub:
@ -77,9 +149,7 @@ pub fn (mut l SSLListener) shutdown() ! {
eprintln(@METHOD) eprintln(@METHOD)
} }
if unsafe { l.certs != nil } { if unsafe { l.certs != nil } {
C.mbedtls_x509_crt_free(&l.certs.cacert) l.certs.cleanup()
C.mbedtls_x509_crt_free(&l.certs.client_cert)
C.mbedtls_pk_free(&l.certs.client_key)
} }
C.mbedtls_ssl_free(&l.ssl) C.mbedtls_ssl_free(&l.ssl)
C.mbedtls_ssl_config_free(&l.conf) C.mbedtls_ssl_config_free(&l.conf)
@ -115,28 +185,12 @@ fn (mut l SSLListener) init() ! {
mut ret := 0 mut ret := 0
if l.config.in_memory_verification { if l.config.in_memory_verification {
if l.config.verify != '' { l.certs = new_sslcerts_in_memory(l.config.verify, l.config.cert, l.config.cert_key) or {
ret = C.mbedtls_x509_crt_parse(&l.certs.cacert, l.config.verify.str, return error('Cert failure')
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)
}
} }
} else { } else {
if l.config.verify != '' { l.certs = new_sslcerts_from_file(l.config.verify, l.config.cert, l.config.cert_key) or {
ret = C.mbedtls_x509_crt_parse_file(&l.certs.cacert, &char(l.config.verify.str)) return error('Cert failure')
}
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)
} }
} }
@ -174,6 +228,25 @@ fn (mut l SSLListener) init() ! {
if ret != 0 { if ret != 0 {
return error_with_code("can't setup ssl", ret) 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 // 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 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 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. // new_ssl_conn returns a new SSLConn with the given config.