Skip to content

net.mbedtls

fn build_certificate_chain #

fn build_certificate_chain(der_certs [][]u8) !&C.mbedtls_x509_crt

build_certificate_chain parses a list of DER-encoded certificates (leaf-first, as TLS 1.3's Certificate message orders them — RFC 8446 §4.4.2: "The sender's certificate MUST come in the first CertificateEntry in the list") into one mbedTLS certificate chain, entirely standalone: no mbedtls_ssl_context is constructed or needed (confirmed safe in x509_standalone_test.v). This is the call shape net.quic needs — QUIC carries the TLS 1.3 handshake over its own CRYPTO frames, bypassing mbedTLS's own SSL/record-layer state machine (and therefore the net.mbedtls.SSLConn-mediated path net.http's TLS clients use) entirely.

The caller owns the returned chain and MUST call free_certificate_chain when done — mbedtls_x509_crt holds C-heap-allocated internal buffers with no GC visibility.

DER bytes, NOT PEM: unlike this module's PEM-string helpers elsewhere (new_sslcerts_in_memory et al.), each der_certs entry is passed to mbedtls_x509_crt_parse with its EXACT length, no NUL-terminator byte appended. Confirmed against mbedTLS's own source (x509_crt.c): the PEM-vs-DER format sniff only checks buf[buflen-1]=='\0' combined with a "-----BEGIN CERTIFICATE-----" substring match; appending a NUL byte a real DER buffer doesn't have would be an out-of-bounds read one byte past a V []u8 slice's allocation, not just a harmless extra byte.

This exact-length requirement is verified by SOURCE INSPECTION only, not by a passing test: empirically, mbedTLS's DER parser tolerates a too-long declared buflen for well-formed input (its ASN.1 SEQUENCE length is self-describing, so it simply stops reading where the structure says to) — passing der.len+1 here still passes every test in this file, because the extra out-of-bounds byte is real undefined behavior (adjacent-heap-memory dependent, not a guaranteed crash or parse failure) rather than something a functional test can observe. Trust the source-level reasoning above, not test results, for this one. Also confirmed mbedtls_x509_crt_parse_der always copies the buffer (own_buffer=1) rather than retaining a pointer into it, so the parsed chain has no dangling reference back into der_certs once this function returns.

Repeated calls append, they don't overwrite: mbedtls_x509_crt_parse_der _internal walks to the existing tail (while crt->version != 0 && crt->next != NULL) and allocates+links a new node there before parsing into it, so calling this in a loop on the same chain correctly builds a multi-certificate chain rather than clobbering earlier entries. This is source-verified (x509_crt.c), not test-verified — every test using this function so far passes only a single certificate (this codebase has one real test cert fixture available); a genuine 2+-certificate functional test needs a second cert and is deliberately deferred, not silently skipped.

fn check_server_cert_usage #

fn check_server_cert_usage(chain &C.mbedtls_x509_crt) !

check_server_cert_usage verifies chain's LEAF certificate is actually usable as a TLS server signing key: its keyUsage extension (if present) permits digitalSignature, and its extendedKeyUsage extension (if present) permits serverAuth. mbedtls_x509_crt_verify (verify_certificate_chain above) only checks chain-of-trust and hostname/SAN -- a real TLS handshake separately enforces this via mbedtls_ssl_check_cert_usage, which this standalone (never-constructs-an-SSL-context) call path has no equivalent for otherwise. Without this, a certificate that chains to a trusted CA and matches the hostname, but was issued restricted to a DIFFERENT purpose (e.g. clientAuth-only EKU, or a KeyUsage lacking digitalSignature), would still be accepted as a QUIC/TLS 1.3 server certificate (Codex P1, vlang/v#27680 pullrequestreview-4783410111). chain must be the HEAD node of a chain built by build_certificate_chain (the leaf), same precondition as get_leaf_public_key.

fn free_certificate_chain #

fn free_certificate_chain(chain &C.mbedtls_x509_crt)

free_certificate_chain releases a chain returned by build_certificate_chain. Like every other mbedTLS resource in this module, calling it twice on the same chain is a double-free — callers own exactly one matching free_certificate_chain call per build_certificate_chain call.

fn get_leaf_public_key #

fn get_leaf_public_key(chain &C.mbedtls_x509_crt) &C.mbedtls_pk_context

get_leaf_public_key returns the public key of chain's HEAD certificate -- the "leaf"/end-entity certificate. build_certificate_chain always parses the sender's own certificate first (RFC 8446 §4.4.2's own wire order: "The sender's certificate MUST come in the first CertificateEntry in the list"), and mbedtls_x509_crt_parse's chain-append walks forward from that head node to link subsequent certificates -- so chain itself, unwalked, already points at the leaf. The returned pointer's lifetime is tied to chain: do not call this after free_certificate_chain.

fn new_ssl_conn #

fn new_ssl_conn(config SSLConnectConfig) !&SSLConn

new_ssl_conn returns a new SSLConn with the given config.

fn new_ssl_listener #

fn new_ssl_listener(saddr string, config SSLConnectConfig) !&SSLListener

create a new SSLListener binding to saddr

fn new_sslcerts #

fn new_sslcerts() &SSLCerts

new_sslcerts initializes and returns a pair of SSL certificates and key

fn new_sslcerts_from_file #

fn new_sslcerts_from_file(verify string, cert string, cert_key string) !&SSLCerts

new_sslcerts_from_file creates a new pair of SSL certificates, given their paths on the filesystem.

fn new_sslcerts_in_memory #

fn new_sslcerts_in_memory(verify string, cert string, cert_key string) !&SSLCerts

new_sslcerts_in_memory creates a pair of SSL certificates, given their contents (not paths).

fn public_key_curve_is_secp256r1 #

fn public_key_curve_is_secp256r1(pk &C.mbedtls_pk_context) bool

public_key_curve_is_secp256r1 reports whether pk is an EC key on the secp256r1 (P-256, aka prime256v1/NIST P-256) curve specifically -- distinct from merely being an EC key, which mbedtls_pk_verify_ext alone confirms. A TLS 1.3 SignatureScheme like ecdsa_secp256r1_sha256 names one exact curve (RFC 8446 §4.2.3); a certificate whose EC key is actually P-384/P-521/any other curve must not be accepted under that scheme name, even though the signature math itself would verify correctly for a genuine signature made with that OTHER curve's key (this isn't a broken- crypto scenario, it's a protocol-identity mismatch between the claimed scheme and the actual key).

fn verify_certificate_chain #

fn verify_certificate_chain(chain &C.mbedtls_x509_crt, ca_bundle_pem string, hostname string) !

verify_certificate_chain validates chain (from build_certificate_chain) against ca_bundle_pem, one or more trusted CA certificates concatenated in PEM format, AND that hostname matches the leaf certificate's SAN/CN. This mirrors SSLConnectConfig.verify's existing contract in this same module: the caller supplies the trust anchor explicitly — there is no OS trust-store lookup anywhere in this codebase today, for any TLS client (HTTP/1.1, HTTP/2, or this QUIC path).

hostname is passed straight through as mbedtls_x509_crt_verify's cn parameter — mbedTLS itself does the SAN/CN matching (DNS names and IP addresses fully supported per its own doc comment), the same mechanism mbedtls_ssl_set_hostname wires into a full SSL handshake's verification, which this standalone call deliberately bypasses. An empty hostname is intentionally NOT special-cased into "skip the check" (passing nil to mbedTLS): it is passed through as an empty C string, which cannot match any real certificate's SAN/CN, so a caller that forgets to supply a real hostname fails closed instead of silently disabling verification.

fn verify_ecdsa_signature #

fn verify_ecdsa_signature(pk &C.mbedtls_pk_context, md_alg MbedtlsMdType, hash []u8, signature []u8) !

verify_ecdsa_signature checks an ECDSA signature over hash -- a digest the CALLER has already computed using the algorithm md_alg names; this function does not hash hash itself. pk may be an MBEDTLS_PK_ECKEY context (the type produced by parsing an EC certificate, not specifically MBEDTLS_PK_ECDSA) -- confirmed against pk_wrap.c's eckey_can_do(), which explicitly accepts MBEDTLS_PK_ECDSA verification requests against an MBEDTLS_PK_ECKEY-typed key, not assumed from the type names alone.

fn verify_rsa_pss_signature #

fn verify_rsa_pss_signature(pk &C.mbedtls_pk_context, md_alg MbedtlsMdType, hash []u8, signature []u8) !

verify_rsa_pss_signature checks an RSASSA-PSS signature the same way, with the salt length pinned to exactly hash.len (not MBEDTLS_RSA_SALT_LEN_ANY) -- RFC 8446 §4.2.3 mandates this for TLS 1.3's rsa_pss_rsae_* schemes ("the length of the Salt MUST equal the length of the digest algorithm"), and this vendored build enforces it rather than silently ignoring it: MBEDTLS_USE_PSA_CRYPTO is disabled (confirmed in mbedtls_config.h, not assumed), so mbedtls_pk_verify_ext's documented "salt length not verified under PSA crypto" caveat does not apply here. The check itself is real, not a no-op: rsa.c's rsa_rsassa_pss_verify_ext rejects a mismatch outright ("if (expected_salt_len != MBEDTLS_RSA_SALT_LEN_ANY && observed_salt_len != (size_t) expected_salt_len) { ... fail }") whenever a specific length (not MBEDTLS_RSA_SALT_LEN_ANY) is supplied -- confirmed by reading rsa.c, not assumed from the header comment alone. mbedTLS's own TLS 1.3 implementation sets expected_salt_len the identical way for the identical reason (ssl_tls13_generic.c: rsassa_pss_options.expected_salt_len = PSA_HASH_LENGTH(hash_alg)), so this isn't a novel usage pattern.

fn MbedtlsMdType.from #

fn MbedtlsMdType.from[W](input W) !MbedtlsMdType

fn Select.from #

fn Select.from[W](input W) !Select

enum MbedtlsMdType #

enum MbedtlsMdType {
	sha256 = 0x09
	sha384 = 0x0a
	sha512 = 0x0b
}

MbedtlsMdType names the subset of mbedtls_md_type_t (md.h) this module's signature-verification functions accept, as the exact enum values mbedTLS itself defines (confirmed against the vendored header, not assumed) -- TLS 1.3's three mandatory-to-implement hash algorithms for CertificateVerify (RFC 8446 §4.2.3: ecdsa_secp256r1_sha256 and the three rsa_pss_rsae_* schemes) and nothing else, since nothing else is used anywhere in this codebase yet.

struct C.mbedtls_ctr_drbg_context #

@[typedef]
struct C.mbedtls_ctr_drbg_context {}

struct C.mbedtls_entropy_context #

@[typedef]
struct C.mbedtls_entropy_context {}

struct C.mbedtls_net_context #

@[typedef]
struct C.mbedtls_net_context {
mut:
	fd int
}

struct C.mbedtls_pk_context #

@[typedef]
struct C.mbedtls_pk_context {}

struct C.mbedtls_pk_rsassa_pss_options #

struct C.mbedtls_pk_rsassa_pss_options {
mut:
	mgf1_hash_id      int
	expected_salt_len int
}

mbedtls_pk_rsassa_pss_options mirrors mbedTLS's own struct (pk.h) field-for-field rather than being kept opaque like mbedtls_x509_crt/ mbedtls_pk_context elsewhere in this file: unlike those, it has no MBEDTLS_PRIVATE-wrapped fields and no internal invariants beyond "two plain ints" -- safe to hand-replicate, unlike the cases this module deliberately keeps behind C shims instead (see mbedtls_helpers.h).

struct C.mbedtls_ssl_config #

@[typedef]
struct C.mbedtls_ssl_config {}

struct C.mbedtls_ssl_context #

@[typedef]
struct C.mbedtls_ssl_context {}

struct C.mbedtls_ssl_recv_t #

@[typedef]
struct C.mbedtls_ssl_recv_t {}

struct C.mbedtls_ssl_recv_timeout_t #

@[typedef]
struct C.mbedtls_ssl_recv_timeout_t {}

struct C.mbedtls_ssl_send_t #

@[typedef]
struct C.mbedtls_ssl_send_t {}

struct C.mbedtls_x509_crl #

@[typedef]
struct C.mbedtls_x509_crl {}

struct C.mbedtls_x509_crt #

@[typedef]
struct C.mbedtls_x509_crt {}

struct SSLCerts #

struct SSLCerts {
pub mut:
	cacert      C.mbedtls_x509_crt
	client_cert C.mbedtls_x509_crt
	client_key  C.mbedtls_pk_context
}

SSLCerts represents a pair of CA and client certificates + key

fn (SSLCerts) cleanup #

fn (mut c SSLCerts) cleanup()

cleanup frees the SSL certificates

struct SSLConn #

struct SSLConn {
pub:
	config SSLConnectConfig
pub mut:
	server_fd    C.mbedtls_net_context
	ssl          C.mbedtls_ssl_context
	conf         C.mbedtls_ssl_config
	certs        &SSLCerts = unsafe { nil }
	ctr_drbg     C.mbedtls_ctr_drbg_context
	entropy      C.mbedtls_entropy_context
	handle       int
	duration     time.Duration
	opened       bool
	ip           string
	read_timeout time.Duration

	owns_socket bool
	// alpn_list is a NUL-terminated C array of pointers to the protocol
	// strings in config.alpn_protocols. mbedtls stores this pointer without
	// copying, so it must outlive the SSL config; it is freed in shutdown().
	alpn_list &&char = unsafe { nil }
	// last_write_sent reports the most recent write_ptr's progress for retry
	// decisions: 0 = provably nothing was sent (safe to replay), or -1 = the
	// count is indeterminate because a failed/retryable write may have already
	// flushed a record to the peer (TLS cannot prove zero). On full success it
	// equals the bytes written.
	last_write_sent int
}

SSLConn is the current connection

fn (SSLConn) complete_handshake #

fn (mut conn SSLConn) complete_handshake(timeout time.Duration) !

complete_handshake finishes the TLS server handshake on a conn returned by accept_raw_with_timeout, waiting up to timeout, then restores blocking mode and the blocking bio. It never calls shutdown: on any error it returns and the caller owns cleanup (so the conn is shut down exactly once, by the caller).

fn (SSLConn) read_timeout #

fn (s &SSLConn) read_timeout() time.Duration

read_timeout returns the current SSL read timeout.

fn (SSLConn) set_read_timeout #

fn (mut s SSLConn) set_read_timeout(timeout time.Duration)

set_read_timeout sets the SSL read timeout for subsequent operations.

fn (SSLConn) close #

fn (mut s SSLConn) close() !

close terminates the ssl connection and does cleanup

fn (SSLConn) shutdown #

fn (mut s SSLConn) shutdown() !

shutdown terminates the ssl connection and does cleanup

fn (SSLConn) negotiated_alpn #

fn (s &SSLConn) negotiated_alpn() string

negotiated_alpn returns the ALPN protocol selected during the TLS handshake (e.g. 'h2' or 'http/1.1'), or an empty string if no protocol was negotiated.

fn (SSLConn) connect #

fn (mut s SSLConn) connect(mut tcp_conn net.TcpConn, hostname string) !

connect sets up an ssl connection on an existing TCP connection

fn (SSLConn) dial #

fn (mut s SSLConn) dial(hostname string, port int) !

dial opens an ssl connection on hostname:port

fn (SSLConn) addr #

fn (s &SSLConn) addr() !net.Addr

addr retrieves the local ip address and port number for this connection

fn (SSLConn) peer_addr #

fn (s &SSLConn) peer_addr() !net.Addr

peer_addr retrieves the ip address and port number used by the peer

fn (SSLConn) socket_read_into_ptr #

fn (mut s SSLConn) socket_read_into_ptr(buf_ptr &u8, len int) !int

socket_read_into_ptr reads len bytes into buf

fn (SSLConn) read #

fn (mut s SSLConn) read(mut buffer []u8) !int

read reads data from the ssl connection into buffer

fn (SSLConn) write_ptr #

fn (mut s SSLConn) write_ptr(bytes &u8, len int) !int

write_ptr writes len bytes from bytes to the ssl connection

fn (SSLConn) write #

fn (mut s SSLConn) write(bytes []u8) !int

write writes data from bytes to the ssl connection

fn (SSLConn) write_string #

fn (mut s SSLConn) write_string(str string) !int

write_string writes a string to the ssl connection

fn (SSLConn) wait_for_write #

fn (mut s SSLConn) wait_for_write(timeout time.Duration) !

wait_for_write waits for a write io operation to be available. Pure raw-socket select() on s.handle — never touches the TLS context, so it is safe to call without holding any lock that guards concurrent access to the context itself (see h2_pooled_transport.v).

fn (SSLConn) wait_for_read #

fn (mut s SSLConn) wait_for_read(timeout time.Duration) !

wait_for_read waits for a read io operation to be available. Pure raw-socket select() on s.handle — never touches the TLS context, so it is safe to call without holding any lock that guards concurrent access to the context itself (see h2_pooled_transport.v).

struct SSLConnectConfig #

@[params]
struct SSLConnectConfig {
pub:
	verify   string // the path to a rootca.pem file, containing trusted CA certificate(s)
	cert     string // the path to a cert.pem file, containing client certificate(s) for the request
	cert_key string // the path to a key.pem file, containing private keys for the client certificate(s)
	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

	read_timeout time.Duration = default_mbedtls_client_read_timeout // the SSL client read timeout

	alpn_protocols []string // the list of ALPN protocols to advertise, e.g. ['h2', 'http/1.1']; empty means no ALPN extension is sent
}

struct SSLListener #

struct SSLListener {
	saddr  string
	config SSLConnectConfig
mut:
	server_fd C.mbedtls_net_context
	ssl       C.mbedtls_ssl_context
	conf      C.mbedtls_ssl_config
	certs     &SSLCerts = unsafe { nil }
	ctr_drbg  C.mbedtls_ctr_drbg_context
	entropy   C.mbedtls_entropy_context
	rng_mutex &sync.Mutex = sync.new_mutex()
	opened    bool
	// alpn_list is a NUL-terminated C array of pointers to the protocol
	// strings in config.alpn_protocols, advertised by accepted connections.
	// It must outlive the SSL config and is freed in shutdown().
	alpn_list &&char = unsafe { nil }
	// handle		int
	// duration	time.Duration
}

SSLListener listens on a TCP port and accepts connection secured with TLS

fn (SSLListener) shutdown #

fn (mut l SSLListener) shutdown() !

finish the listener and clean up resources

fn (SSLListener) accept #

fn (mut l SSLListener) accept() !&SSLConn

accepts a new connection and returns a SSLConn of the connected client

fn (SSLListener) accept_with_timeout #

fn (mut l SSLListener) accept_with_timeout(timeout time.Duration) !&SSLConn

accept_with_timeout waits up to timeout for a new client before accepting it.

fn (SSLListener) accept_raw_with_timeout #

fn (mut l SSLListener) accept_raw_with_timeout(accept_timeout time.Duration) !&SSLConn

accept_raw_with_timeout waits up to accept_timeout for a new client, accepts the raw TCP connection and sets up its (non-blocking) SSL context, but does NOT perform the TLS handshake. The returned conn is non-blocking with a non-blocking bio; the caller must run conn.complete_handshake to finish negotiation. This lets the threaded server accept on one thread and handshake on a worker thread.

fn (SSLListener) accept_with_timeouts #

fn (mut l SSLListener) accept_with_timeouts(accept_timeout time.Duration, handshake_timeout time.Duration) !&SSLConn

accept_with_timeouts waits up to accept_timeout for a new client, then waits up to handshake_timeout for the TLS server handshake to complete.