Repository navigation
TLS and Certificates
The SDK has its own TLS 1.3 server and client in src/tls/. They use the primitives of the Zig standard library and obey RFC 84461. There is no dependency on OpenSSL or on another library. The standard library has a TLS client but no server, and its client has no ALPN and no client certificates.
Applies to: TLS 1.3 only. Zig 0.16.0, zig-sdk 0.1.0. RSA and RSA-PSS certificate keys, X25519MLKEM768, AEGIS, record padding, certificate_authorities, the chain checks of Certificate checks, name constraints, revocation and the retry_cookie option: zig-sdk 0.2.0.
| Feature | Values |
|---|---|
| Protocol version | TLS 1.3 only. A TLS 1.2 client gets a protocol_version or missing_extension alert. |
| Cipher suites |
TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256. As an option: TLS_AEGIS_128L_SHA256 and TLS_AEGIS_256_SHA512. |
| Key exchange | X25519MLKEM7684, X25519, secp256r1, secp384r1. X25519MLKEM768 is the first choice of the default order on both sides. |
| Certificate keys | ECDSA P-256, ECDSA P-384, Ed25519, RSA and RSA-PSS (id-RSASSA-PSS11) with a modulus of 2048 to 4096 bits. |
| Key formats | PKCS#8 and SEC1 in PEM. RSA keys in PKCS#8 (PRIVATE KEY) and PKCS#1 (RSA PRIVATE KEY). RSA-PSS keys in PKCS#8. |
| Signatures of the server and the client | ECDSA P-256 and P-384, Ed25519, RSA-PSS with SHA-256, SHA-384 or SHA-5125. An RSA key never makes a PKCS#1 v1.5 signature in a CertificateVerify. |
| Extensions | ALPN2, server name indication, HelloRetryRequest, key update, middlebox compatibility mode, the HelloRetryRequest cookie, certificate_authorities, status_request (OCSP stapling9), record padding. |
| Client certificates | Optional or required on the server. The client presents a chain when the server asks. |
| Certificate signatures the client verifies | ECDSA P-256 and P-384, Ed25519, RSA PKCS#1 v1.5 and RSASSA-PSS in the chain. The CertificateVerify signature can be ECDSA, Ed25519 or RSA-PSS. |
| Chain checks | Validity, basic constraints, key usage, extended key usage, name constraints7, the host against the subject alternative name only10, and revocation with CRLs and OCSP staples. |
| Not in this version | TLS resumption tickets, pre-shared keys, early data, RSA keys with more than two primes, and revocation data from the network (CRL distribution points and OCSP requests). |
- Make a key and a certificate. This command makes a self-signed P-256 pair for tests:
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout server.key -out server.crt -days 365 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"- Load the chain and the key:
var chain = try mcp.tls.CertChain.loadFiles(gpa, io, "server.crt", "server.key");
defer chain.deinit();
const chains = [_]*const mcp.tls.CertChain{&chain};
const tls_server = try mcp.tls.Server.init(.{ .chains = &chains, .alpn = &.{"http/1.1"} });- Give the TLS server to the HTTP transport:
var transport: mcp.transport.http.Server = .init(io, gpa, &server, .{ .port = 8443, .tls = &tls_server });- Test the endpoint with curl. The
-kflag accepts the self-signed certificate:
curl -k --tlsv1.3 -X POST https://127.0.0.1:8443/mcp -H "content-type: application/json" -H "accept: application/json, text/event-stream" -H "mcp-protocol-version: 2026-07-28" -H "mcp-method: server/discover" --data-binary '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'examples/https_server.zig shows the full program.
- Make a 3072-bit key in PKCS#8. Use the algorithm
RSA, notRSA-PSS:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out server.key- Make a self-signed certificate for tests:
openssl req -x509 -new -key server.key -sha256 -days 365 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" -out server.crt- Load the chain with
mcp.tls.CertChain.loadFiles, as for an ECDSA key.
- Choose a trust policy,
mcp.tls.Trust:
| Policy | Meaning |
|---|---|
ca_set |
The chain must lead to an anchor in a mcp.tls.CaSet. Load anchors with addPem, addDer or addFile. |
bundle |
The chain must lead to an anchor of a std.crypto.Certificate.Bundle, for example the system roots. |
pinned_leaf |
The leaf certificate must equal the given DER bytes. |
self_signed |
The leaf must be self-signed and valid. This proves nothing about who holds it. |
no_verification |
No check at all. Only for tests. |
- Run the handshake on a connected stream:
var conn = try mcp.tls.connect(&reader.interface, &writer.interface, .{
.io = io,
.host = "mcp.example.com",
.trust = .{ .ca_set = &anchors },
.alpn = &.{"http/1.1"},
.read_buffer = &read_buf,
.write_buffer = &write_buf,
});
defer conn.deinit();The client checks the host name against the leaf certificate. The client checks an IP address literal against the iPAddress names and does not send it as server name. The chain walk requires each issuer to be a certificate authority (basic constraints and key usage), and it obeys path length constraints. Every failure ends the handshake with the matching alert, for example bad_certificate, unknown_ca, certificate_expired or unsupported_certificate.
The Streamable HTTP client uses this client for https URLs. HttpClient.Options.tls carries the trust policy and an optional client identity. Without it the client loads the system trust store. See Client Guide.
The WebSocket server and the gRPC server also take a mcp.tls.Server in their tls option. The WebSocket client and the gRPC client take a HttpClient.TlsSetup in their tls option. See Transport WebSocket and Transport gRPC.
X25519MLKEM768 (code point 0x11EC) joins ML-KEM-7686 and X25519, as RFC 10024 defines4. An attacker must break both parts to find the shared secret. The ML-KEM part comes first:
| Value | Layout | Length |
|---|---|---|
| Client key share | ML-KEM-768 encapsulation key, then the X25519 public key | 1216 bytes |
| Server key share | ML-KEM-768 ciphertext, then the X25519 public key | 1120 bytes |
| Shared secret | ML-KEM-768 shared secret, then the X25519 shared secret | 64 bytes |
The default group order is x25519_mlkem768, x25519, secp256r1, secp384r1.
The client sends a key share for the first group of groups. When that group is X25519MLKEM768, the client also sends a key share for the first classical group of the list, X25519 by default. A server without the hybrid group can then use the classical share, and no HelloRetryRequest is necessary. After a HelloRetryRequest, the client sends one key share for the requested group. The requested group must be in groups and must have no key share yet, else the client sends illegal_parameter.
The server selects X25519MLKEM768 when the client supports it, also when the client sent no hybrid key share. In that case the server sends a HelloRetryRequest for the hybrid group. Among the classical groups, the server prefers a group for which the client sent a key share. To disable the hybrid group, remove x25519_mlkem768 from groups. Connection.group holds the negotiated group.
Both sides check the length of the peer key share. The server also checks the ML-KEM encapsulation key. A bad length or a bad key ends the handshake with illegal_parameter. An X25519 result of zero ends the handshake with decrypt_error. The SDK zeroes the key shares and the shared secrets after use.
The server and the client accept an RSA key for their certificate chain. The key obeys these rules:
- The modulus has 2048 to 4096 bits. A smaller or larger key gives
error.UnsupportedKey. - The public exponent is odd, at least 3 and less than 2^32.
- The key has two primes and the CRT values of PKCS#1. A key with more primes gives
error.UnsupportedKey. - The loader checks that the primes divide the modulus and that the CRT values are smaller than their primes. It also sends a test value through the private operation and the public operation. A bad key gives
error.InvalidKey. - The key agrees with the
rsaEncryptionpublic key of the leaf certificate, elseCertChaingiveserror.KeyMismatch. - A key with the algorithm
id-RSASSA-PSSis an RSA-PSS key. See RSA-PSS keys.
The key signs with RSASSA-PSS5. The mask function is MGF1 with the same hash, and the salt has the length of the hash. The key prefers SHA-256, then SHA-384, then SHA-512. The server and the client take the first of these hashes that the peer offers. Thus a peer gets SHA-384 or SHA-512 only when it does not offer SHA-256.
The private operation uses the Chinese remainder theorem and the constant-time arithmetic of std.crypto.ff3. The key verifies each signature with the public exponent before the signature goes out. This check stops a fault attack. A failed check gives error.SigningFailed, and the server sends internal_error.
A peer that offers only PKCS#1 v1.5 schemes gets a handshake_failure alert, because TLS 1.3 does not permit these schemes in a CertificateVerify1.
The SDK verifies an RSA-PSS CertificateVerify only for a modulus of 2048, 3072 or 4096 bits. A key of another size, for example 2560 bits, loads and signs. But an SDK peer then refuses its CertificateVerify with decrypt_error. The SDK also verifies the rsa_pss_pss_* schemes of a peer with an id-RSASSA-PSS certificate, and it signs with such a key.
The SDK supports the two AEGIS cipher suites of TLS 1.3: TLS_AEGIS_128L_SHA256 (0x1307) and TLS_AEGIS_256_SHA512 (0x1306). AEGIS is a family of AEAD algorithms on the AES round function. TLS uses the variants with a 128-bit tag.
| Suite | Key | Nonce and IV | Tag | Hash of the key schedule |
|---|---|---|---|---|
AEGIS_128L_SHA256 |
16 bytes | 16 bytes | 16 bytes | SHA-256 |
AEGIS_256_SHA512 |
32 bytes | 32 bytes | 16 bytes | SHA-512 |
The AEGIS suites are off by default. The default lists of the client and the server do not contain them. To enable AEGIS, give a list with the AEGIS suites to the cipher_suites option of the client and of the server. The list tls.suites.default_suites_with_aegis has the default suites and the two AEGIS suites. On a CPU with AES hardware, that list puts AEGIS-128L first. On other CPUs, ChaCha20-Poly1305 goes first.
const tls_server = try mcp.tls.Server.init(.{
.chains = &chains,
.cipher_suites = mcp.tls.suites.default_suites_with_aegis,
});
const http = try mcp.transport.HttpClient.init(io, gpa, .{
.url = "https://mcp.example.com/mcp",
.tls = .{ .trust = .{ .ca_set = &cas }, .cipher_suites = mcp.tls.suites.default_suites_with_aegis },
});Limits:
- A peer without AEGIS gets a default suite. The handshake does not fail.
- The specification of AEGIS is an Internet-Draft. OpenSSL 3.5 does not have the AEGIS suites. The TLS client of the Zig standard library has them, and the tests run it against the SDK server.
Padding adds zero bytes to the inner plaintext of each encrypted record (RFC 8446 section 5.4). It hides the length of the content from an observer of the network. It costs bandwidth.
The padding option of the client and of the server sets the policy. Its type is tls.Padding:
| Policy | What it does |
|---|---|
.none |
No padding. This is the default. |
.{ .block = n } |
Pads the inner plaintext to a multiple of n bytes. The values 0 and 1 add no padding. |
.{ .random = n } |
Adds a random number of zero bytes to each record, from 0 to n. |
The inner plaintext is the content, the content type byte and the padding. The policy applies to all encrypted records: handshake messages, application data, key updates and alerts. To change the policy of an open connection, call Connection.setPadding. The HTTP client takes the policy in HttpClient.TlsSetup.padding.
const tls_server = try mcp.tls.Server.init(.{
.chains = &chains,
.padding = .{ .block = 1024 },
});Limits:
- Padding stops at an inner plaintext of 2^14 bytes. A record with 2^14 bytes of content gets no padding. No record gets past the limit of RFC 8446, 2^14 + 1 bytes of inner plaintext.
- The TLS client of the Zig standard library refuses a handshake record with 2^14 + 1 bytes of inner plaintext. The padding limit of 2^14 bytes keeps padded records in the limit of that client.
- The receiver removes the padding. A record with only zero bytes is an unexpected message. A record with more than 2^14 + 1 bytes of inner plaintext is a record overflow.
- The random policy gets its seed from the secure random source of
io. - Padding does not hide the number of records or the time between them. Plaintext records have no padding: ClientHello, ServerHello and HelloRetryRequest.
A peer can tell the other peer which certificate authorities it trusts. RFC 8446 section 4.2.41 defines the certificate_authorities extension for this purpose. The SDK sends and reads the extension in two messages:
- The server sends the subject names of its client trust anchors in the CertificateRequest. A client with more than one certificate then picks a chain that the server trusts.
- The client can send the subject names of its trust anchors in the ClientHello. A server with more than one chain then picks a chain that the client trusts.
Only the ca_set and bundle trust policies have anchors. The other policies send no names.
Option of mcp.tls.server.Config
|
What it does |
|---|---|
chains |
The certificate chains in preference order. The first chain is the default. |
send_client_ca_names |
Send the names of the client_trust anchors in the CertificateRequest. The default is true. |
The server picks a chain in three steps:
- Pick the first chain that leads to a name of the client. With a server name, the leaf must also name the host.
- Else pick the first chain whose leaf names the host.
- Else pick the first chain.
A custom select_chain function replaces these steps. It does not get the names of the client.
Option of mcp.tls.ClientOptions
|
What it does |
|---|---|
identity |
The first certificate chain for a CertificateRequest. |
identities |
More certificate chains, after identity in preference order. |
identity_fallback |
The answer when no chain leads to a name of the server. .first (the default) sends the first chain. .none sends an empty certificate list. |
send_ca_names |
Send the names of the trust anchors in the ClientHello. The default is false, because the list can be large. |
The client takes the first chain that leads to a name of the server. Without names in the CertificateRequest, the client sends the first chain.
// The server trusts one CA for client certificates and has two chains of its own.
var client_anchors: mcp.tls.CaSet = .init(gpa);
defer client_anchors.deinit();
try client_anchors.addFile(io, "client-ca.pem");
const chains = [_]*const mcp.tls.CertChain{ &public_chain, &private_chain };
const server = try mcp.tls.Server.init(.{
.chains = &chains,
.client_auth = .required,
.client_trust = .{ .ca_set = &client_anchors },
});
// The client has two certificates and names its own anchors.
const identities = [_]*const mcp.tls.CertChain{ &team_a_chain, &team_b_chain };
var conn = try mcp.tls.connect(&reader.interface, &writer.interface, .{
.io = io,
.host = "mcp.example.com",
.trust = .{ .ca_set = &server_anchors },
.identities = &identities,
.send_ca_names = true,
.read_buffer = &read_buf,
.write_buffer = &write_buf,
});- The server sends at most 15,872 bytes of names (
ca_names.max_request_list_len). The CertificateRequest then stays below 16 KiB. - The client sends at most 8 KiB of names (
ca_names.max_hello_list_len). The first ClientHello also stays at 16 KiB or less. - When the names do not fit, the SDK sends no names. A part of the list can make the peer pick a wrong chain.
- Each received name must be one DER
Namethat is not empty. Other content ends the handshake with the alertdecode_error. - The match compares the issuer names of each certificate in a chain with the received names, byte for byte.
An RSA-PSS key has the algorithm id-RSASSA-PSS11. This key makes only RSASSA-PSS signatures. In TLS 1.3 its signature schemes are rsa_pss_pss_sha256, rsa_pss_pss_sha384 and rsa_pss_pss_sha512 (RFC 8446 section 4.2.3). An RSA key with the algorithm rsaEncryption uses the rsa_pss_rsae schemes. The SDK keeps the two families apart.
The SDK can do these tasks with RSA-PSS keys:
- Load a PKCS#8 RSA-PSS key with or without parameters.
PrivateKey.kind()gives.rsa_pss. - Present an RSA-PSS certificate on the server and on the client. The key signs the CertificateVerify with an
rsa_pss_pssscheme that the peer offers. - Verify an RSA-PSS leaf, and verify RSASSA-PSS signatures in a certificate chain.
- Sign a JWT with PS256.
The parameters of a key are optional. Parameters restrict the hash, the MGF1 hash and the minimum salt length. The SDK applies these rules:
- The hash must be SHA-256, SHA-384 or SHA-512. The MGF1 hash must be the same hash.
- The minimum salt length must not be longer than the hash. TLS 1.3 and PS256 use a salt as long as the hash.
- A key with parameters signs with one scheme only: the scheme of its hash.
- The parameters of the leaf certificate also restrict the key. A key with another hash gives
error.KeyMismatch.
Other parameters give error.UnsupportedKey. Examples are SHA-1 and the defaults of RFC 4055.
jwt.SigningKey.fromPem and jwt.SigningKey.fromDer give a PS256 key for an RSA-PSS key. The key parameters must permit SHA-256. Else the functions return error.UnsupportedKey.
// A key for SHA-256 only, made with:
// openssl genpkey -algorithm RSA-PSS -pkeyopt rsa_keygen_bits:2048 \
// -pkeyopt rsa_pss_keygen_md:sha256 -pkeyopt rsa_pss_keygen_mgf1_md:sha256 \
// -pkeyopt rsa_pss_keygen_saltlen:32 -out server.key
var chain = try mcp.tls.CertChain.loadFiles(gpa, io, "server.crt", "server.key");
defer chain.deinit();
// chain.key.schemes() is { .rsa_pss_pss_sha256 }.
const chains = [_]*const mcp.tls.CertChain{&chain};
const server = try mcp.tls.Server.init(.{ .chains = &chains });
var signing = try mcp.auth.jwt.SigningKey.fromPem(gpa, key_pem);
defer signing.deinit();
// signing.algorithm() is .PS256.- The modulus has 2048 to 4096 bits.
- The SDK signs with a salt as long as the hash.
- A certificate signature can have a salt of any length. Its MGF1 hash must be the same as its hash.
- A client that offers only
rsa_pss_rsaeschemes gets the alerthandshake_failurefrom a server with an RSA-PSS key.
The client checks the host name against the subject alternative name of the leaf. A DNS name must match a dNSName entry, and an IP address literal must match an iPAddress entry. The client never uses the common name of the subject10. A wildcard is the complete leftmost label, it matches one label, and at least two labels follow it. Thus *.example.com matches www.example.com, but *.com, * and w*.example.com match nothing.
When the leaf or an intermediate has an extended key usage, it must permit the role of the peer. A server needs serverAuth, and a client certificate needs clientAuth. anyExtendedKeyUsage permits both. A wrong purpose ends the handshake with unsupported_certificate.
Each intermediate must have basic constraints with cA set. A trust anchor without basic constraints is good, because old version 1 roots have no extensions. When a CA set has two anchors with the issuer name, the SDK tries both.
mcp.tls.verify.verifyChain takes the chain, the trust policy and a ChainOptions with an explicit purpose:
_ = try mcp.tls.verify.verifyChain(certs, .{ .ca_set = &anchors }, .{
.purpose = .server,
.host = "www.example.com",
.now_sec = now,
});A CA certificate can limit the names of the certificates below it with the extension nameConstraints7. The client and the server apply the constraints of each intermediate and of the trust anchor to each certificate below that CA. When two CAs have constraints, a name must agree with both. The check has no option. It is always on for the ca_set and bundle trust policies.
| Name form | Constraint | Names that the constraint covers |
|---|---|---|
dNSName |
example.com |
example.com and each name below it, such as www.example.com. The case does not count. |
dNSName |
.example.com |
Only the names below example.com. |
iPAddress |
An address and a mask: 8 bytes for IPv4, 32 bytes for IPv6 | The addresses in that range. |
directoryName |
A distinguished name | Each subject that starts with the relative distinguished names of the constraint. |
rfc822Name |
user@host, host or .domain
|
One mailbox, the mailboxes at one host, or the mailboxes in a domain. |
uniformResourceIdentifier |
host or .domain
|
The URIs with that host, or with a host in that domain. |
The SDK checks these names of a certificate:
- The subject as a
directoryName, and eachemailAddressattribute of the subject as anrfc822Name. - Each name of the subject alternative name.
- For the leaf, the host that the client checks.
A permitted subtree limits only the names of its form. A name inside an excluded subtree always fails. A self-issued intermediate gets no name check, as RFC 5280 tells. The leaf always gets the check.
The SDK cannot check the forms otherName, x400Address, ediPartyName and registeredID. A critical constraint can have a subtree of such a form, and a certificate below can have a name of that form. Then the check fails with error.TlsCertificateUnsupportedConstraint. A name outside the constraints gives error.TlsCertificateNameNotPermitted. Both errors send the alert bad_certificate.
var conn = mcp.tls.connect(&reader.interface, &writer.interface, options) catch |err| switch (err) {
error.TlsCertificateNameNotPermitted => return error.UntrustedServer,
else => |e| return e,
};Limits:
- The SDK compares relative distinguished names attribute by attribute. In text values it ignores the case of ASCII letters and extra spaces. It does not apply the complete string preparation rules of directory names.
- A URI without a host, or with an IP address as host, fails a URI constraint. An email address without
@fails an email constraint. - A subtree with a
minimumother than zero, or with amaximum, makes the CA certificate invalid.
The SDK checks revocation offline. It does not fetch CRLs and it does not ask OCSP responders. There are two sources of status:
- CRLs7 that the application loads in PEM or in DER.
- OCSP responses8 that the server staples to its certificate. In TLS 1.3 the response is in the
status_requestextension9 of the certificate entry.
The revocation option of the client and the client_revocation option of the server set the policy. The HTTP client takes it in HttpClient.TlsSetup.revocation. Its type is mcp.tls.Revocation:
| Field | Default | What it does |
|---|---|---|
crls |
empty | The CRLs, as []const *const mcp.tls.Crl. |
ocsp_stapling |
.off |
.off, .request or .require. Only the client uses it. |
scope |
.leaf |
.leaf or .chain: the certificates that need a known status for hard_fail. |
unknown |
.soft_fail |
.soft_fail or .hard_fail: what to do with the status "unknown". |
The default policy makes no check, as in earlier versions. The checks apply to the ca_set and bundle trust policies. They do not apply to the trust anchor.
For each certificate of the chain, the SDK finds one status:
- "revoked": a current CRL of the issuer lists the serial number, or a stapled response gives "revoked".
- "good": a current CRL of the issuer does not list the serial number, or a stapled response gives "good".
- "unknown": all other cases.
The status "revoked" always ends the handshake with certificate_revoked. The status "unknown" ends the handshake only with hard_fail, and only for a certificate in scope. Then the alert is certificate_unknown. No CRL of the issuer, a stale CRL and a CRL with a bad signature give "unknown". No staple and a staple with the status "unknown" give it too. With hard_fail and no source of status, each certificate in scope has the status "unknown".
var crl = try mcp.tls.Crl.loadFile(gpa, io, "ca.crl");
defer crl.deinit();
var conn = try mcp.tls.connect(&reader.interface, &writer.interface, .{
.io = io,
.host = "mcp.example.com",
.trust = .{ .ca_set = &anchors },
.revocation = .{ .crls = &.{&crl}, .unknown = .hard_fail },
.read_buffer = &read_buf,
.write_buffer = &write_buf,
});A CRL applies to a certificate when the CRL issuer has the name of the certificate issuer. The issuer in the chain must sign the CRL. When the issuer has a key usage, it must permit cRLSign. A current CRL has its thisUpdate before the verification time and its nextUpdate after it, with a tolerance of five minutes. A CRL without nextUpdate is never current. The reason code of an entry is only for information.
Crl.fromDer, Crl.fromPem and Crl.loadFile refuse a delta CRL and an indirect CRL. They also refuse a CRL with issuingDistributionPoint, and a CRL with a critical extension that the SDK does not know.
With .request or .require, the client sends status_request. The server puts its response in the entry of the leaf. The client checks each staple:
- The response status is
successfuland the response type is the basic response. - A single response names the certificate. The SDK compares the serial number and the hashes of the issuer name and key, with SHA-1 or SHA-256.
- The issuer signs the response, or a delegated responder signs it. The issuer signs the certificate of the delegated responder. That certificate has the extended key usage
OCSPSigningand is valid now. -
thisUpdateis not in the future andnextUpdateis not in the past, with a tolerance of five minutes. WithoutnextUpdate, a response stays current for one day.
A staple that fails a check ends the handshake with bad_certificate_status_response, also with soft_fail (RFC 6066 section 8). With .require, a leaf without a staple ends the handshake with the same alert.
To staple, give the server its responses:
const staple = try std.Io.Dir.cwd().readFileAlloc(io, "leaf.ocsp", gpa, .limited(mcp.tls.ocsp.max_response_len));
defer gpa.free(staple);
const tls_server = try mcp.tls.Server.init(.{
.chains = &chains,
.ocsp_staples = &.{.{ .chain = &chain, .response = staple }},
});The server sends the staple only when the client asks for it. The application gets new responses from the OCSP responder of its CA. To send a newer response, make a new server with a new configuration.
Limits:
- The SDK never fetches revocation data. The application must load a new CRL or a new response before the old one becomes stale.
- The server reads no staples from clients. A
client_revocationwith anocsp_staplingother than.offgiveserror.ClientOcspStapling. - The SDK verifies the signatures of CRLs and OCSP responses with RSASSA-PKCS1-v1_5 (SHA-256, SHA-384 or SHA-512), ECDSA P-256 and P-384, and Ed25519.
- A staple has at most 16 KiB.
mcp.tls.server.Config has these fields:
-
chains: one or more certificate chains. The first one is the default. -
select_chain: a function that picks a chain for a server name. Without it, the server uses the three steps of Server options. -
alpn: the application protocols in preference order. Empty turns ALPN off. -
require_alpn: reject a client that negotiates no protocol. -
cipher_suites: the cipher suites in preference order. -
groups: the groups in preference order. A hybrid group that the client supports wins, also when it costs a HelloRetryRequest. Among the other groups, a group with a client key share wins. -
server_name_mismatch:ignoreoralert(unrecognized_name). -
client_auth:none,optionalorrequired. Withoptionalorrequiredthe server sends aCertificateRequest. -
client_trust: themcp.tls.Trustpolicy for the client chain. The fingerprint of the accepted leaf is inConnection.peer_fingerprint. -
send_client_ca_names: send the names of theclient_trustanchors in theCertificateRequest. See The certificate_authorities extension. -
client_revocation: the revocation policy for client certificates. See Revocation. -
ocsp_staples: the OCSP responses that the server staples. -
padding: the record padding policy. See Record padding. -
retry_cookie: send a HelloRetryRequest with a random cookie of 32 bytes, and require the cookie in the second ClientHello. The client thus shows that it receives data at its address before the key exchange. It costs one round trip.
mcp.tls.ClientOptions has host, trust, alpn, cipher_suites, groups, identity (a CertChain for client authentication), identities, identity_fallback, send_ca_names, revocation, padding, the buffers, alert and now_sec. groups is the preference order of the client. See Post-quantum key exchange for the key shares that the client sends. The client refuses a HelloRetryRequest cookie larger than 8 KiB. It accepts a HelloRetryRequest with a cookie and without a key share, and then keeps its key shares.
The ClientHello of the client has a fixed buffer. Thus each ALPN name has 1 to 255 bytes, and all ALPN names have at most 1024 bytes. cipher_suites and groups have at most 16 entries each. Other options give error.TlsInvalidOptions before the client sends a byte. The client answers an extension that it did not request, or an unknown extension, with the alert unsupported_extension.
mcp.tls.PrivateKey has these declarations for RSA:
-
Kindhas the valuesrsaandrsa_pss. -
schemes(): the signature schemes of the key in preference order. -
signScheme(): sign with one scheme ofschemes(). Another scheme giveserror.SigningFailed. -
max_public_key_len: the size of the buffer forpublicKeyBytes. This value replaces the fixed size of 97 bytes. -
max_signature_lenis 512.
The module mcp.tls.rsa has the RSA key type, the PKCS#1 parser and the EMSA-PSS encoder. It also has the PKCS#1 v1.5 signatures and the deterministic PSS signatures that the JWT signing keys use. See Authorization.
- Loopback tests run the server against the standard library client for each key type except Ed25519, and for each cipher suite. The key types include RSA 2048 and RSA 3072.
- The standard library client negotiates X25519MLKEM768 with the server.
- Loopback tests run the SDK client against the SDK server. They cover every key type, every cipher suite and a HelloRetryRequest. They also cover chain validation by name and by address, host mismatch, untrusted chains and client certificates in every mode.
- Loopback tests run the SDK client with RSA 2048 and RSA 3072 server certificates, and with an RSA client certificate.
- Loopback tests cover X25519MLKEM768 in the first flight, and a server or a client without the hybrid group. They also cover a server with only the hybrid group. They also cover a HelloRetryRequest to the hybrid group and a HelloRetryRequest from the hybrid group to P-384.
- Unit tests check the hybrid layout, the key agreement of each group and the refusal of bad and low-order peer keys.
- Unit tests check the RSA-PSS signatures of each hash with the verifier of the standard library. They also check the EMSA-PSS layout and the refusal of bad RSA keys. A PKCS#1 key and its PKCS#8 form give the same key.
- The tests check the key schedule against the vectors of RFC 8448.
- Interop tests run
openssl s_client(chain verification, ALPN, a HelloRetryRequest withX448:P-384),openssl s_serveragainst the SDK client, andcurlwhen the tools are installed. - Interop tests run
openssl s_clientagainst an RSA 2048 server with each RSA-PSS hash and with an offer of only PKCS#1 v1.5 schemes. With OpenSSL 3.5 or later,openssl s_clientalso negotiates X25519MLKEM768 with an RSA 3072 server, in the first flight and after a HelloRetryRequest. - With OpenSSL 3.5 or later, the SDK client runs against
openssl s_serverwith an RSA 3072 key. The runs force SHA-384 and SHA-512, and use X25519MLKEM768 in the first flight and after a HelloRetryRequest. With an older OpenSSL, this test does not run. - The fuzz target of the TLS parsers also covers the PKCS#1 parser and the server side of the hybrid group.
- The MCP client runs over HTTPS with the CA set, a pinned leaf and a client certificate. It also runs with AEGIS and record padding.
- Loopback tests cover the chain choice of the client and of the server from the CA names, with and without a HelloRetryRequest. They also cover a CertificateRequest with 500 and 600 names. Interop tests check the names with
openssl s_clientandopenssl s_serverin both directions. - Loopback and interop tests cover RSA-PSS server and client keys, and RSASSA-PSS certificate signatures.
- Fixture tests cover each name form of the name constraints, and excluded and permitted subtrees. They also cover two constrained CAs, a self-issued CA and a critical form that the SDK cannot check. Handshake tests cover a permitted and an excluded leaf.
- Fixture tests cover CRLs (current, stale, delta, with an unknown critical extension) and OCSP responses (good, revoked, unknown, stale, from a delegated responder, from a responder without
OCSPSigning). Handshake tests cover a revoked server and client certificate, and stapling between the SDK peers and againstopenssl s_serverandopenssl s_client. - Regression tests cover the extended key usage of each role and an intermediate without basic constraints. They also cover a leaf with only a common name and a root with a new key. Other regression tests cover duplicate or trailing extension data in the ClientHello.
- The fuzz target of the TLS parsers also covers the CA name list, the RSASSA-PSS parameters, the name constraints and the extended key usage. It also covers the parsers of CRLs, times and OCSP responses.
- Loopback tests cover a HelloRetryRequest with a cookie, with and without a key share.
openssl s_clientsends back the cookie of the SDK server. - A stateful fuzz target sends records with each padding policy, gives them to the receiver in pieces of each size, and changes one byte.
The project did an internal security review of the TLS code. The file docs/security/tls-review.md in the repository has the scope and the method of the review. It lists each finding with its fix and its regression test, and it has a package for an external reviewer. No independent third party reviewed the code yet. See Threat Model.
- RFC8446: The Transport Layer Security (TLS) Protocol Version 1.3.
- RFC7301: TLS Application-Layer Protocol Negotiation Extension.
-
ZIG-0.16-STD: Zig standard library documentation,
std.crypto. - RFC10024: "Post-quantum hybrid ECDHE-MLKEM Key Agreement for TLSv1.3".
- RFC8017: "PKCS #1: RSA Cryptography Specifications Version 2.2".
- FIPS-203: Module-Lattice-Based Key-Encapsulation Mechanism Standard.
- RFC5280: Internet X.509 Public Key Infrastructure Certificate and CRL Profile.
- RFC6960: X.509 Internet Public Key Infrastructure Online Certificate Status Protocol.
- RFC6066: Transport Layer Security (TLS) Extensions: Extension Definitions.
- RFC9525: Service Identity in TLS.
- RFC4055: Additional Algorithms and Identifiers for RSA Cryptography for use in the Internet X.509 Public Key Infrastructure.
zig-sdk uses the Apache License 2.0. The wiki uses the project profile of ASD-STE100 Simplified Technical English. The Bibliography lists the sources.
zig-sdk
- Home
- Getting Started
- Concepts
- Server Guide
- Client Guide
- Transports
- Transport stdio
- Transport Streamable HTTP
- Transport gRPC
- Transport Unix Socket
- Transport WebSocket
- TLS and Certificates
- Deploy Reverse Proxy
- Authorization
- Authorization Server
- JSON Schema Support
- Resource Templates
- Tasks Extension
- Skills Extension
- Apps Extension
- Conformance
- Examples
- API Reference
- Interoperability
- Threat Model
- Limits and Security
- Deprecated Features
- Roadmap
- Release Notes
- Versioning
- Contributing
- FAQ
Style and words