Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
- **The pure TLS client offers X25519MLKEM768 by default, and the pure server
accepts it (#2483).** Every ClientHello from `std.cryptography.tls13_client`
now carries an X25519MLKEM768 key share (the post-quantum hybrid, RFC 10024)
beside the x25519 and secp256r1 ones, so `connect()`, `connect_mtls()` and
the `std.http.client` fallback in builds without OpenSSL get post-quantum key
exchange from any server that supports it. The ClientHello grows by about
1.2 KB. `connect_pq()` is now `connect()` with a fresh scalar, and the new
`conn_group(conn)` reports the group the server chose. The pure server
(`std.cryptography.tls13_server`, used by `AETHER_PURE_TLS=1`) prefers the
hybrid when a client offers it. It answers a share that fails the FIPS 203
§7.2 check, or has the wrong length, with a fatal `illegal_parameter` alert,
and now sends an alert for any refused ClientHello instead of dropping the
connection. A HelloRetryRequest now ends the handshake: the client already
sends a share for every group it supports, so any retry the server asks for
is one RFC 8446 §4.1.4 says to abort on.
356 changes: 110 additions & 246 deletions std/cryptography/tls13_client/module.ae

Large diffs are not rendered by default.

111 changes: 72 additions & 39 deletions std/cryptography/tls13_hs/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -154,25 +154,33 @@ client_hello(random: ptr, session_id: byte[], x25519_pub: ptr) -> ptr {
// (RFC 6066 §3). CDNs require SNI to select the right certificate. An empty
// host omits the extension (identical to the bare client_hello).
client_hello_sni(random: ptr, session_id: byte[], x25519_pub: ptr, host: string) -> ptr {
return client_hello_full(random, session_id, GROUP_X25519, bytes.view(x25519_pub)[0..32], 0, null, host)
return client_hello_full(random, session_id, GROUP_X25519, bytes.view(x25519_pub)[0..32], 0, null, 0, null, host)
}

// Build a ClientHello offering both x25519 and secp256r1 in supported_groups,
// carrying a key_share for `group` (its `pubkey`/`pubkey_len`) and optionally a
// second key_share for `group2` (pass group2=0 to omit). Offering both shares
// lets a server that prefers the other group complete without a
// HelloRetryRequest. x25519 share = 32-byte public; secp256r1 share = 65-byte
// uncompressed point (0x04 || X || Y). The HRR retry path uses a single share.
client_hello_full(random: ptr, session_id: byte[], group: int, pubkey: byte[], group2: int, pubkey2: byte[], host: string) -> ptr {
// Build a ClientHello offering up to three groups (e.g. X25519MLKEM768, x25519,
// secp256r1) in supported_groups, carrying key_shares for each offered group.
// Pass group2=0 or group3=0 to omit the respective entry.
client_hello_full(random: ptr, session_id: byte[], group: int, pubkey: byte[], group2: int, pubkey2: byte[], group3: int, pubkey3: byte[], host: string) -> ptr {
if session_id.len < 0 { panic("tls13_hs.client_hello_full: the `session_id` slice has no bound (.len -1); give it one, as `(p as byte[])[0..n]`") }
int sid_len = session_id.len
if pubkey.len < 0 { panic("tls13_hs.client_hello_full: the `pubkey` slice has no bound (.len -1); give it one, as `(p as byte[])[0..n]`") }
int pubkey_len = pubkey.len
if pubkey2.len < 0 { panic("tls13_hs.client_hello_full: the `pubkey2` slice has no bound (.len -1); give it one, as `(p as byte[])[0..n]`") }
int pubkey2_len = pubkey2.len
// Generous fixed buffer; this CH is ~230 bytes + the hostname.
buf = bytes.new(1024)
bytes.set(buf, 1023, 0) // force full zero-fill
pubkey_len = 0
if group != 0 {
if pubkey.len < 0 { panic("tls13_hs.client_hello_full: the `pubkey` slice has no bound (.len -1); give it one, as `(p as byte[])[0..n]`") }
pubkey_len = pubkey.len
}
pubkey2_len = 0
if group2 != 0 {
if pubkey2.len < 0 { panic("tls13_hs.client_hello_full: the `pubkey2` slice has no bound (.len -1); give it one, as `(p as byte[])[0..n]`") }
pubkey2_len = pubkey2.len
}
pubkey3_len = 0
if group3 != 0 {
if pubkey3.len < 0 { panic("tls13_hs.client_hello_full: the `pubkey3` slice has no bound (.len -1); give it one, as `(p as byte[])[0..n]`") }
pubkey3_len = pubkey3.len
}
// Generous fixed buffer for up to 3 key shares + extensions.
buf = bytes.new(2048)
bytes.set(buf, 2047, 0) // force full zero-fill

// Reserve the 4-byte handshake header; fill length at the end.
body = 4
Expand Down Expand Up @@ -222,13 +230,19 @@ client_hello_full(random: ptr, session_id: byte[], group: int, pubkey: byte[], g
off = put8(buf, off, 2) // list len
off = put16(buf, off, 0x0304) // TLS 1.3

// supported_groups (10): { x25519, secp256r1 } — offer both so a server
// that won't do x25519 can HelloRetryRequest secp256r1.
// supported_groups (10): list all offered groups
num_groups = 0
if group != 0 { num_groups = num_groups + 1 }
if group2 != 0 { num_groups = num_groups + 1 }
if group3 != 0 { num_groups = num_groups + 1 }

sg_body = 2 + num_groups * 2
off = put16(buf, off, 10)
off = put16(buf, off, 6) // ext body len = 2 (list len) + 2*2
off = put16(buf, off, 4) // list len
off = put16(buf, off, GROUP_X25519)
off = put16(buf, off, GROUP_SECP256R1)
off = put16(buf, off, sg_body) // ext body len
off = put16(buf, off, num_groups * 2) // list len
if group != 0 { off = put16(buf, off, group) }
if group2 != 0 { off = put16(buf, off, group2) }
if group3 != 0 { off = put16(buf, off, group3) }

// signature_algorithms (13):
// { ecdsa_p256_sha256, ecdsa_p384_sha384, rsa_pss_rsae_sha256, ed25519 }
Expand Down Expand Up @@ -260,23 +274,30 @@ client_hello_full(random: ptr, session_id: byte[], group: int, pubkey: byte[], g
off = put8(buf, off, 8) // ProtocolName length
off = put_ascii(buf, off, "http/1.1")

// key_share (51): a KeyShareEntry {group, key<len>} for `group`, plus an
// optional second entry (group2/pubkey2) so we can pre-offer both x25519 and
// secp256r1 and let the server pick without a HelloRetryRequest round-trip.
entry1 = 2 + 2 + pubkey_len
shares_len = entry1
// key_share (51): a KeyShareEntry {group, key<len>} for each offered group.
shares_len = 0
if group != 0 { shares_len = shares_len + 2 + 2 + pubkey_len }
if group2 != 0 { shares_len = shares_len + 2 + 2 + pubkey2_len }
if group3 != 0 { shares_len = shares_len + 2 + 2 + pubkey3_len }

off = put16(buf, off, 51)
off = put16(buf, off, 2 + shares_len) // ext body: client_shares<2> + entries
off = put16(buf, off, shares_len) // client_shares length
off = put16(buf, off, group)
off = put16(buf, off, pubkey_len)
off = put_bytes(buf, off, pubkey)
if group != 0 {
off = put16(buf, off, group)
off = put16(buf, off, pubkey_len)
off = put_bytes(buf, off, pubkey)
}
if group2 != 0 {
off = put16(buf, off, group2)
off = put16(buf, off, pubkey2_len)
off = put_bytes(buf, off, pubkey2)
}
if group3 != 0 {
off = put16(buf, off, group3)
off = put16(buf, off, pubkey3_len)
off = put_bytes(buf, off, pubkey3)
}

// fill extensions length
ext_total = off - ext_start
Expand Down Expand Up @@ -731,7 +752,8 @@ parse_client_hello(msg: byte[], out: *ChParsed) -> string {
}
if ext_type == 0x0033 {
// key_share: client_shares<0..2^16-1> of { group(2), len(2), key }.
// Take the first entry whose group we can complete.
// Prefer post-quantum X25519MLKEM768 (1216 bytes) if offered,
// otherwise fallback to classical X25519 (32 bytes).
if ext_len >= 2 {
ks_total = get16(msg, d)
q = d + 2
Expand All @@ -742,15 +764,26 @@ parse_client_hello(msg: byte[], out: *ChParsed) -> string {
klen = get16(msg, q + 2)
kstart = q + 4
if kstart + klen > ks_end { q = ks_end } else {
if out.group == 0 {
if g == GROUP_X25519 {
if klen == 32 {
kb = bytes.new(klen)
bytes.copy_from_slice(kb, 0, msg[kstart..kstart + klen])
out.group = g
out.key_share = kb
out.key_share_len = klen
}
if g == GROUP_X25519MLKEM768 {
if klen != 1216 {
return "client hello: invalid X25519MLKEM768 key share length"
}
if out.key_share != null {
bytes.free(out.key_share)
out.key_share = null
}
kb = bytes.new(klen)
bytes.copy_from_slice(kb, 0, msg[kstart..kstart + klen])
out.group = g
out.key_share = kb
out.key_share_len = klen
} else if g == GROUP_X25519 && out.group == 0 {
if klen == 32 {
kb = bytes.new(klen)
bytes.copy_from_slice(kb, 0, msg[kstart..kstart + klen])
out.group = g
out.key_share = kb
out.key_share_len = klen
}
}
q = kstart + klen
Expand Down
122 changes: 106 additions & 16 deletions std/cryptography/tls13_server/module.ae
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@
// key fails at the signature step and the peer just sees the connection
// drop. Generate with:
// openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1
// - X25519 key exchange only. A client offering only other groups is
// refused by parse_client_hello; HelloRetryRequest is not implemented,
// so there is no renegotiation to a group we do support.
// - Key exchange: X25519MLKEM768 (post-quantum hybrid) and X25519.
// - Buffer arguments are std.bytes HANDLES, never bytes.data(b) -- see the
// header of tls13_client for why that distinction bites.

Expand All @@ -27,6 +25,7 @@ import std.tcp
import std.cryptography.sha2
import std.cryptography.hmac
import std.cryptography.x25519
import std.cryptography.mlkem
import std.cryptography.p256
import std.cryptography.tls13_kdf
import std.cryptography.tls13_client
Expand Down Expand Up @@ -190,6 +189,33 @@ fn send_record_raw(sock: ptr, rec: ptr) -> string {
return ""
}

// The alert for a ClientHello parse_client_hello refused: illegal_parameter
// (47) for a malformed X25519MLKEM768 share (RFC 10024 §4.2),
// handshake_failure (40) when nothing offered is something we do, and
// decode_error (50) for the rest, which are all framing faults.
fn ch_error_alert(perr: string) -> int {
if string.contains(perr, "X25519MLKEM768") == 1 { return 47 }
if string.contains(perr, "no key_share") == 1 { return 40 }
if string.contains(perr, "no mutually supported") == 1 { return 40 }
if string.contains(perr, "supported_versions") == 1 { return 70 } // protocol_version
return 50
}

// A fatal alert in the clear, before any keys exist. Best effort: the
// handshake is failing either way.
fn send_plaintext_alert(sock: ptr, level: int, desc: int) {
b = bytes.new(7)
bytes.set(b, 0, 0x15) // alert
bytes.set(b, 1, 0x03)
bytes.set(b, 2, 0x03) // legacy record version 0x0303
bytes.set(b, 3, 0x00)
bytes.set(b, 4, 0x02) // length 2
bytes.set(b, 5, level & 0xFF)
bytes.set(b, 6, desc & 0xFF)
srv_send_view(sock, bytes.view(b)[0..7])
bytes.free(b)
}

// Fresh randomness, or null. Returning null on failure is deliberate and the
// callers MUST check: every use here is security-critical -- the server
// random, the ECDSA-P256 signing nonce, and the X25519 private key. Silently
Expand Down Expand Up @@ -302,34 +328,97 @@ accept(sock: @keep ptr, cert_chain: ptr, cert_lens: ptr, priv_key: ptr, x25519_p
has_sni: 0, saw_supported_versions: 0 }
perr = tls13_hs.parse_client_hello(bytes.view(ch_msg)[0..ch_msg_len], &ch_parsed)
if string.equals(perr, "") != 1 {
bytes.free(ch_msg); recio_free(rio)
send_plaintext_alert(sock, 2, ch_error_alert(perr))
tls13_hs.free_ch(&ch_parsed); bytes.free(ch_msg); recio_free(rio)
return null, perr
}

tr = tls13_client.transcript_new()
tls13_client.transcript_add(tr, bytes.view(ch_msg)[0..ch_msg_len])

// 2. Compute X25519 shared secret
shared, agerr = x25519.agree(x25519_priv, ch_parsed.key_share)
if string.equals(agerr, "") != 1 {
// 2. Compute shared secret & server key share
shared = null
shared_len = 0
server_ks = null
server_ks_len = 0

if ch_parsed.group == tls13_hs.GROUP_X25519MLKEM768 {
if ch_parsed.key_share_len != 1216 {
send_plaintext_alert(sock, 2, 47)
tls13_hs.free_ch(&ch_parsed); bytes.free(ch_msg); tls13_client.transcript_free(tr); recio_free(rio)
return null, "X25519MLKEM768 key share wrong length"
}
ek = bytes.new(1184)
bytes.copy_from_bytes(ek, 0, ch_parsed.key_share, 0, 1184)
cxpub = bytes.new(32)
bytes.copy_from_bytes(cxpub, 0, ch_parsed.key_share, 1184, 32)

ct, mlk_ss, merr = mlkem.mlkem768_encaps(ek)
bytes.free(ek)
if string.equals(merr, "") != 1 || ct == null || mlk_ss == null {
if ct != null { bytes.free(ct) }
if mlk_ss != null { bytes.free(mlk_ss) }
bytes.free(cxpub)
send_plaintext_alert(sock, 2, 47) // illegal_parameter (RFC 10024 §4.2)
tls13_hs.free_ch(&ch_parsed); bytes.free(ch_msg); tls13_client.transcript_free(tr); recio_free(rio)
return null, string.concat("X25519MLKEM768 encapsulation failed: ", merr)
}

x_ss, xerr = x25519.agree(x25519_priv, cxpub)
bytes.free(cxpub)
if string.equals(xerr, "") != 1 || x_ss == null {
if x_ss != null { bytes.free(x_ss) }
bytes.free(ct); bytes.free(mlk_ss)
send_plaintext_alert(sock, 2, 47)
tls13_hs.free_ch(&ch_parsed); bytes.free(ch_msg); tls13_client.transcript_free(tr); recio_free(rio)
return null, xerr
}

sxpub = x25519.base_point(x25519_priv)
server_ks = bytes.new(1120)
bytes.copy_from_bytes(server_ks, 0, ct, 0, 1088)
bytes.copy_from_bytes(server_ks, 1088, sxpub, 0, 32)
server_ks_len = 1120
bytes.free(ct); bytes.free(sxpub)

shared = bytes.new(64)
bytes.copy_from_bytes(shared, 0, mlk_ss, 0, 32)
bytes.copy_from_bytes(shared, 32, x_ss, 0, 32)
shared_len = 64
bytes.free(mlk_ss); bytes.free(x_ss)
} else if ch_parsed.group == tls13_hs.GROUP_X25519 {
x_ss, xerr = x25519.agree(x25519_priv, ch_parsed.key_share)
if string.equals(xerr, "") != 1 || x_ss == null {
if x_ss != null { bytes.free(x_ss) }
send_plaintext_alert(sock, 2, 47) // illegal_parameter (RFC 8446 §7.4.2)
tls13_hs.free_ch(&ch_parsed); bytes.free(ch_msg); tls13_client.transcript_free(tr); recio_free(rio)
return null, xerr
}
sxpub = x25519.base_point(x25519_priv)
server_ks = sxpub
server_ks_len = 32
shared = x_ss
shared_len = 32
} else {
send_plaintext_alert(sock, 2, 40)
tls13_hs.free_ch(&ch_parsed); bytes.free(ch_msg); tls13_client.transcript_free(tr); recio_free(rio)
return null, agerr
return null, "unsupported group in client hello"
}
x25519_pub = x25519.base_point(x25519_priv)

// 3. Build and send ServerHello (plaintext)
srnd = rand_into(32)
if srnd == null {
tls13_hs.free_ch(&ch_parsed); bytes.free(ch_msg)
tls13_client.transcript_free(tr); recio_free(rio)
bytes.free(x25519_pub); bytes.free(shared)
bytes.free(server_ks); bytes.free(shared)
return null, "no randomness available for the server random"
}
sh = tls13_hs.server_hello(srnd, bytes.view(ch_parsed.session_id)[0..ch_parsed.session_id_len], ch_parsed.cipher_suite, ch_parsed.group, bytes.view(x25519_pub)[0..32])
sh = tls13_hs.server_hello(srnd, bytes.view(ch_parsed.session_id)[0..ch_parsed.session_id_len], ch_parsed.cipher_suite, ch_parsed.group, bytes.view(server_ks)[0..server_ks_len])
bytes.free(server_ks)
sh_len = bytes.length(sh)
sherr = send_plaintext_handshake(sock, bytes.view(sh)[0..sh_len])
if string.equals(sherr, "") != 1 {
bytes.free(srnd); bytes.free(x25519_pub); bytes.free(shared); bytes.free(sh)
bytes.free(srnd); bytes.free(shared); bytes.free(sh)
tls13_hs.free_ch(&ch_parsed); bytes.free(ch_msg); tls13_client.transcript_free(tr); recio_free(rio)
return null, sherr
}
Expand All @@ -338,13 +427,14 @@ accept(sock: @keep ptr, cert_chain: ptr, cert_lens: ptr, priv_key: ptr, x25519_p
// 4. Key Schedule
rec_aead, rec_key_len, hash_algo, hash_len, suite_err = suite_params(ch_parsed.cipher_suite)
if string.equals(suite_err, "") != 1 {
bytes.free(srnd); bytes.free(x25519_pub); bytes.free(shared); bytes.free(sh)
bytes.free(srnd); bytes.free(shared); bytes.free(sh)
tls13_hs.free_ch(&ch_parsed); bytes.free(ch_msg); tls13_client.transcript_free(tr); recio_free(rio)
return null, suite_err
}

th_chsh = transcript_hash_algo(tr, hash_algo)
keys = derive_handshake_keys_algo(bytes.view(shared)[0..32], bytes.view(th_chsh)[0..hash_len], rec_key_len, hash_algo)
keys = derive_handshake_keys_algo(bytes.view(shared)[0..shared_len], bytes.view(th_chsh)[0..hash_len], rec_key_len, hash_algo)
bytes.free(shared)

srv_hs_ctx = tls13_record.new_aead(rec_aead, bytes.view(hs_server_key(keys))[0..rec_key_len], hs_server_iv(keys))
cli_hs_ctx = tls13_record.new_aead(rec_aead, bytes.view(hs_client_key(keys))[0..rec_key_len], hs_client_iv(keys))
Expand Down Expand Up @@ -419,7 +509,7 @@ accept(sock: @keep ptr, cert_chain: ptr, cert_lens: ptr, priv_key: ptr, x25519_p
}

if fin_ok == 0 {
bytes.free(th_sf); bytes.free(srnd); bytes.free(x25519_pub); bytes.free(shared); bytes.free(sh)
bytes.free(th_sf); bytes.free(srnd); bytes.free(sh)
tls13_hs.free_ch(&ch_parsed); bytes.free(ch_msg); tls13_client.transcript_free(tr); recio_free(rio)
tls13_record.free_ctx(srv_hs_ctx); tls13_record.free_ctx(cli_hs_ctx)
tls13_client.free_hs_keys(keys); bytes.free(th_chsh)
Expand All @@ -442,7 +532,7 @@ accept(sock: @keep ptr, cert_chain: ptr, cert_lens: ptr, priv_key: ptr, x25519_p
// Cleanup handshake state
bytes.free(ck); bytes.free(civ); bytes.free(sk); bytes.free(siv)
bytes.free(ms); bytes.free(c_ap); bytes.free(s_ap); bytes.free(th_sf)
bytes.free(srnd); bytes.free(x25519_pub); bytes.free(shared); bytes.free(sh)
bytes.free(srnd); bytes.free(sh)
tls13_hs.free_ch(&ch_parsed); bytes.free(ch_msg); tls13_client.transcript_free(tr)
tls13_record.free_ctx(srv_hs_ctx); tls13_record.free_ctx(cli_hs_ctx)
tls13_client.free_hs_keys(keys); bytes.free(th_chsh)
Expand Down
Loading
Loading