TLS, and What Each Version Fixed
Chapter Eighty
Syllabus topic Module 2, "Web Security: Secure Socket Layer and Transport Layer Security"
Pages 533 to 543 of 678
In one line
TLS is SSL taken over by the IETF and then repaired version by version: TLS 1.0 (1999) was SSL 3.0 with HMAC and a new way of expanding keys; 1.1 fixed the CBC initialisation vector; 1.2 let the hash be chosen and brought in authenticated encryption; and 1.3 (2018) rebuilt the handshake, with one round trip, forward secrecy always, and everything after the ServerHello encrypted.
In the words an answer should use: Transport Layer Security (TLS) is an IETF standards-track protocol, first defined in RFC 2246, whose aim is an Internet standard version of SSL. TLS 1.0 is very close to SSLv3 and differs in the version number, the message authentication code (TLS uses HMAC), the pseudorandom function that expands secrets into keys, the alert codes, the cipher suites and client certificate types, the certificate_verify and finished messages, the cryptographic computations, and padding.
From SSL to TLS
SSL was Netscape's. Version 2.0 was described in February 1995 and version 3.0 in November 1996; RFC 6101, which records SSL 3.0 for history, calls it "the November 18, 1996, version of the protocol". The IETF then took the protocol over and in January 1999 published it as TLS 1.0, RFC 2246, which says its differences from SSL 3.0 "are not dramatic, but they are significant enough that TLS 1.0 and SSL 3.0 do not interoperate".
Why TLS 1.0 is "version 3.1". On the wire TLS 1.0 carries the version {3, 1}, and RFC 2246 explains: "The version value 3.1 is historical: TLS version 1.0 is a minor modification to the SSL 3.0 protocol". TLS 1.1 is {3, 2} and TLS 1.2 is {3, 3}. TLS 1.3 is 0x0304, but, as the run shows, not where the others put it.
How TLS 1.0 differs from SSL 3.0
This is the comparison the syllabus's textbook sets out, checked here against both specifications.
| Point | SSL 3.0 | TLS 1.0 |
|---|---|---|
| Version number | 3.0 | 3.1 |
| Record MAC | SSL's own nested hash with pad_1 and pad_2, as the record-layer chapter ran | HMAC, which also covers the version field |
| Master secret | MD5 and SHA-1 nested three times, with the salts 'A', 'BB' and 'CCC' | PRF(pre_master_secret, "master secret", the two randoms) |
| Expanding secrets into keys | nested MD5 and SHA-1 | a pseudorandom function, PRF(secret, label, seed) = P_MD5(S1, label + seed) XOR P_SHA-1(S2, label + seed), with S1 and S2 the two halves of the secret |
| Finished | 36 bytes: MD5 and SHA-1 over the handshake messages, a sender code, the master secret and the pads | 12 bytes: PRF(master_secret, "client finished" or "server finished", MD5 and SHA-1 of the handshake messages) |
| certificate_verify | hashes over the handshake messages, the master secret and the pads | hashes over the handshake messages alone |
| Alerts | 12 codes, among them no_certificate (41) | no_certificate dropped and 12 added, among them decryption_failed, record_overflow, unknown_ca, decode_error, decrypt_error, protocol_version, internal_error and user_canceled |
| Cipher suites | include Fortezza | Fortezza dropped |
| Client certificate types | seven, among them rsa_ephemeral_dh, dss_ephemeral_dh and fortezza_kea | four: rsa_sign, dss_sign, rsa_fixed_dh, dss_fixed_dh |
| Padding | less than one block | any length up to 255 bytes, so that the lengths of messages can be hidden |
TLS, and What Each Version Fixed
What SSL 3.0 had already fixed. RFC 6176 lists what was wrong with SSL 2.0: message authentication used MD5; the handshake was not protected, which "permits a man-in-the-middle to trick the client into picking a weaker cipher suite"; one key served for both integrity and encryption; and a forged TCP FIN could end a session unnoticed. SSL 3.0 answered the last three: its Finished message checks every handshake message, it derives separate MAC and encryption keys, and its close_notify alert lets each side tell a real end from a truncation.
What each version fixed
| Version | Published | Defined in | What it fixed or added | Status on 30 September 2026 |
|---|---|---|---|---|
| SSL 2.0 | February 1995 | Netscape | the first public version | prohibited, RFC 6176 (March 2011) |
| SSL 3.0 | November 1996 | Netscape; recorded as RFC 6101 in 2011 | a protected handshake, separate MAC and encryption keys, closure alerts | prohibited, RFC 7568 (June 2015) |
| TLS 1.0 | January 1999 | RFC 2246 | an IETF standard, with HMAC and the PRF | deprecated, RFC 8996 (March 2021) |
| TLS 1.1 | April 2006 | RFC 4346 | an explicit IV "to protect against CBC attacks" | deprecated, RFC 8996 |
| TLS 1.2 | August 2008 | RFC 5246 | a PRF chosen by the cipher suite (SHA-256), one named hash in signatures, authenticated encryption such as AES-GCM | in use, but RSA and finite-field Diffie-Hellman key exchange forbidden in it by RFC 10015 (July 2026) |
| TLS 1.3 | August 2018 | RFC 8446, replaced in July 2026 by RFC 9846 | a new handshake: one round trip, forward secrecy always, AEAD only, encrypted after the ServerHello | current |
TLS 1.1 also answered the padding-oracle attack the record-layer chapter described, by sending bad_record_mac rather than decryption_failed for a padding error, so that the two failures look the same. TLS 1.2 removed MD5 and SHA-1 from the PRF: "All cipher suites in this document use P_SHA256", as the handshake chapter's run did.
TLS 1.3: a new handshake
Figure 80.1 TLS 1.2 needs two round trips before the client can send data; TLS 1.3 needs one, and hides the certificate.
RFC 9846 lists the major differences from TLS 1.2. In an answer they come to seven.
- One round trip. The client sends a key share in its very first message, guessing which group the server will accept, so the server can reply with its own share, its certificate and its Finished in one flight. If the guess is wrong the server sends a HelloRetryRequest, which costs a round trip.
- Forward secrecy always. "Static RSA and Diffie-Hellman cipher suites have been removed; all public-key based key exchange mechanisms now provide forward secrecy."
- Authenticated encryption only. The legacy algorithms were pruned, and "Those that remain are all Authenticated Encryption with Associated Data (AEAD) algorithms". Compression is gone, and with it the CRIME attack.
- An encrypted handshake. "All handshake messages after the ServerHello are now encrypted", the server's certificate included.
- A new key schedule, built on HKDF, which the second run reproduces.
- Version negotiation in an extension. The version travels in a supported_versions list, and the ChangeCipherSpec protocol is removed "except when needed for middlebox compatibility".
- Shorter cipher suites. A TLS 1.3 suite names only the AEAD cipher and the hash, TLS_AES_256_GCM_SHA384 for example; the key exchange and the signature are negotiated separately.
TLS, and What Each Version Fixed
Zero round trips, at a price. A client returning to a server it knows can send 0-RTT "early data" in its first message. RFC 9846 warns that the security of 0-RTT data is weaker: it is not forward secret, and "There are no guarantees of non-replay between connections". A client may therefore send as early data only what it deems "safe to be replayed": fetching a page, say, never a payment.
The run: munotes.in, and the mark that stops a downgrade
The first listing reads two real handshakes with munotes.in, captured on 30 September 2026: one by a client offering TLS 1.3, and one by a client offering only TLS 1.2. It reports what the server chose, looks at the last bytes of the server's random value, and checks the server's ECDSA signature itself, on the NIST P-256 curve, with the public key from munotes.in's certificate.
# TLS 1.3 ServerHello, its first 96 of 1,210 bytes
02 00 04 b6 03 03 50 2c 7c e9 08 9b bb d2 ad 6c
46 ae ba ba eb 1a 58 21 d0 e7 8c fc 3e 96 a1 b8
79 8e 3c d4 15 52 20 46 e3 1c b7 1d f6 2d 32 bf
8c 77 8d 3d 23 4c 37 95 19 1a e7 18 91 80 27 0e
3e 31 eb 42 d3 f5 2f 13 02 00 04 6e 00 2b 00 02
03 04 00 33 04 64 11 ec 04 60 95 23 7f f8 f7 3f
# TLS 1.2 ClientHello, its first 38 bytes
01 00 00 c7 03 03 bc 8d 5c 0f 58 10 2b c7 2e 45
4f 7e 44 af 60 68 c8 d8 c6 3b a6 a3 78 35 f0 fc
b5 99 b9 df 47 62
# TLS 1.2 ServerHello
02 00 00 41 03 03 b7 e7 ec d1 a2 ca 5b 71 a2 f6
61 c0 f7 f0 16 3d c1 c5 a2 fd 19 d9 0e 6e 44 4f
57 4e 47 52 44 01 00 c0 2c 00 00 19 ff 01 00 01
00 00 00 00 00 00 0b 00 04 03 00 01 02 00 23 00
00 00 17 00 00
# TLS 1.2 ServerKeyExchange
0c 00 00 6e 03 00 1d 20 7b 33 36 05 81 a9 e8 05
be 23 c0 54 5a 0a 74 a1 11 ba 36 0a 39 ea 0e 9e
7d 41 55 65 79 45 1e 27 04 03 00 46 30 44 02 20
58 36 10 85 f0 3a d8 79 3a d0 f8 5a 2e 99 c9 91
73 b1 51 4e cd 64 1a 1f c1 54 00 db 7a 90 09 49
02 20 3c 1b c3 42 df d6 8e 88 cb 49 2f de 28 38
ca 0f e3 11 3b 66 7a a1 7a 5e 89 e4 16 d2 f3 f8
bc c9
# public key in the certificate munotes.in sent
04 b6 50 14 d1 56 d0 7c 49 38 1d 3a d7 e6 12 16
b9 4a 62 43 6d 06 1e 13 55 ba a1 3e 64 35 43 f5
42 d2 ac e7 f8 2a 0f 2a 4a 7d 20 b4 b4 0f 46 4e
5f 54 17 ff d6 55 2e b3 c8 a8 10 a3 00 57 6f 93
b7TLS, and What Each Version Fixed
# munotes.in on 30 September 2026: what TLS 1.3 chose, and the marker that defeats a downgrade.
import hashlib
wire, title = {}, None
for line in open('munotes-handshakes.hex').read().split('\n'):
if line.startswith('# '):
title = line[2:].split(',')[0]
wire[title] = b''
elif line:
wire[title] += bytes.fromhex(line)
# ---- 1. a browser offering TLS 1.3: what the server chose --------------------------------
sh = wire['TLS 1.3 ServerHello']
at = 4 + 2 + 32
at += 1 + sh[at] # the session id echoed back
suite = sh[at:at + 2]
at += 2 + 1 + 2 # suite, compression, extensions length
version = sh[at + 4:at + 6] # supported_versions: the real version
at += 4 + int.from_bytes(sh[at + 2:at + 4], 'big')
group = int.from_bytes(sh[at + 4:at + 6], 'big') # key_share
share = int.from_bytes(sh[at + 6:at + 8], 'big')
print('1. OFFERED TLS 1.3 (ServerHello of %d bytes)' % (int.from_bytes(sh[1:4], 'big') + 4))
print(' version field 0x%s, supported_versions 0x%s: TLS 1.3' % (sh[4:6].hex(), version.hex()))
print(' cipher suite 0x%s: TLS_AES_256_GCM_SHA384' % suite.hex())
print(' key share group 0x%04X (%d): X25519MLKEM768' % (group, group))
print(' server share %d bytes = 1,088 ML-KEM-768 + 32 X25519: %s' % (share, share == 1088 + 32))
# ---- 2. a client offering only TLS 1.2: the same server marks its random -------------------
client_random = wire['TLS 1.2 ClientHello'][6:38]
sh12 = wire['TLS 1.2 ServerHello']
server_random = sh12[6:38]
at = 38 + 1 + sh12[38]
print()
print('2. OFFERED ONLY TLS 1.2')
print(' version 0x%s, cipher suite 0x%s: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384'
% (sh12[4:6].hex(), sh12[at:at + 2].hex()))
print(' last 8 bytes of the server random:', server_random[-8:])
MARK_12 = bytes.fromhex('444F574E47524401') # RFC 8446 s.4.1.3, RFC 9846 s.4.2.3
print(' equal to the TLS 1.2 downgrade marker:', server_random[-8:] == MARK_12)
print(' a client that had offered TLS 1.3 must abort: %s'
% ('illegal_parameter' if server_random[-8:] == MARK_12 else 'no'))
# ---- 3. why an attacker cannot erase the marker: the server SIGNED both randoms ------------
# P-256, FIPS 186-4 D.1.2.3 (y^2 = x^3 - 3x + b)
p = 115792089210356248762697446949407573530086143415290314195533631308867097853951
n = 115792089210356248762697446949407573529996955224135760342422259061068512044369
b = 0x5ac635d8aa3a93e7b3ebbd55769886bc651d06b0cc53b0f63bce3c3e27d2604b
G = (0x6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296,
0x4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5)
def add(P1, P2):
if P1 is None or P2 is None:
return P1 or P2
(x1, y1), (x2, y2) = P1, P2
if x1 == x2 and (y1 + y2) % p == 0:
return None
if P1 == P2:
m = (3 * x1 * x1 - 3) * pow(2 * y1, -1, p)
else:
m = (y2 - y1) * pow(x2 - x1, -1, p)
x3 = (m * m - x1 - x2) % p
return x3, (m * (x1 - x3) - y1) % p
def mul(k, P):
R = None
while k:
if k & 1:
R = add(R, P)
P, k = add(P, P), k >> 1
return R
def ecdsa_verify(public, message, r, s): # FIPS 186-4 s.6.4, with SHA-256
e = int.from_bytes(hashlib.sha256(message).digest(), 'big')
w = pow(s, -1, n)
X = add(mul(e * w % n, G), mul(r * w % n, public))
return X is not None and X[0] % n == r
key = wire['public key in the certificate munotes.in sent']
public = (int.from_bytes(key[1:33], 'big'), int.from_bytes(key[33:65], 'big'))
ske = wire['TLS 1.2 ServerKeyExchange'][4:]
params = ske[:4 + ske[3]] # curve type, named curve, public value
algorithm, sig = ske[len(params):len(params) + 2], ske[len(params) + 4:]
r_len = sig[3]
r = int.from_bytes(sig[4:4 + r_len], 'big')
s = int.from_bytes(sig[6 + r_len:], 'big')
print()
print('3. THE SERVERKEYEXCHANGE SIGNATURE')
print(' named curve 0x%s (x25519), signature scheme 0x%s (ecdsa_secp256r1_sha256)'
% (params[1:3].hex(), algorithm.hex()))
print(' certificate key is on P-256:',
(public[1] ** 2 - public[0] ** 3 + 3 * public[0] - b) % p == 0)
print(' signature over client random + server random + params verifies:',
ecdsa_verify(public, client_random + server_random + params, r, s))
erased = server_random[:24] + bytes(8)
assert erased != server_random
print(' the same signature with the marker erased verifies:',
ecdsa_verify(public, client_random + erased + params, r, s))TLS, and What Each Version Fixed
1. OFFERED TLS 1.3 (ServerHello of 1210 bytes)
version field 0x0303, supported_versions 0x0304: TLS 1.3
cipher suite 0x1302: TLS_AES_256_GCM_SHA384
key share group 0x11EC (4588): X25519MLKEM768
server share 1120 bytes = 1,088 ML-KEM-768 + 32 X25519: True
2. OFFERED ONLY TLS 1.2
version 0x0303, cipher suite 0xc02c: TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
last 8 bytes of the server random: b'DOWNGRD\x01'
equal to the TLS 1.2 downgrade marker: True
a client that had offered TLS 1.3 must abort: illegal_parameter
3. THE SERVERKEYEXCHANGE SIGNATURE
named curve 0x001d (x25519), signature scheme 0x0403 (ecdsa_secp256r1_sha256)
certificate key is on P-256: True
signature over client random + server random + params verifies: True
the same signature with the marker erased verifies: FalseTLS, and What Each Version Fixed
What the run establishes, in order.
munotes.in speaks TLS 1.3, with a post-quantum key exchange. The ServerHello's version field says 0x0303, TLS 1.2, and the real version, 0x0304, is in the supported_versions extension: RFC 9846 keeps the old field fixed because servers "incorrectly implemented version negotiation". The key share is group 4588, X25519MLKEM768, which RFC 10024 (August 2026) defines as ML-KEM-768, the post-quantum method of FIPS 203, combined with X25519, "to provide security as long as at least one of the component algorithms remains secure". Its 1,120 bytes are 1,088 of ML-KEM and 32 of X25519.
Asked for TLS 1.2, the server agrees but leaves a mark. The last eight bytes of its random value spell DOWNGRD followed by the byte 01, which RFC 9846 requires of any TLS 1.3 server that negotiates TLS 1.2. This client offered only TLS 1.2, so it carries on. A client that had offered TLS 1.3 and still received TLS 1.2 would know its offer was removed on the way, and "MUST abort the handshake with an "illegal_parameter" alert".
The mark cannot be rubbed out. In a TLS 1.2 handshake with an ephemeral key exchange, the server signs both random values together with its key share. munotes.in's signature verifies, and with the last eight bytes changed it fails, so an attacker who erased the mark would be caught by the signature check. RFC 9846 states the limit: the mark "does not provide downgrade protection when static RSA is used", because RSA key exchange carries no such signature, one more reason RFC 10015 now forbids it.
The run: TLS 1.3's key schedule
RFC 8448 publishes every secret of a real TLS 1.3 handshake, so that anyone can check an implementation against it. The second listing takes that handshake's client private key and server key share, computes the X25519 shared secret, and derives each secret in the order RFC 9846 gives, comparing each with the value RFC 8448 prints.
# ClientHello, RFC 8448 s.3
010000c00303cb34ecb1e78163ba1c38c6dacb196a6dffa21a8d9912
ec18a2ef6283024dece7000006130113031302010000910000000b00
09000006736572766572ff01000100000a00140012001d0017001800
190100010101020103010400230000003300260024001d002099381d
e560e4bd43d23d8e435a7dbafeb3c06e51c13cae4d5413691e529aaf
2c002b0003020304000d0020001e0403050306030203080408050806
04010501060102010402050206020202002d00020101001c00024001
# ServerHello, RFC 8448 s.3
020000560303a6af06a4121860dc5e6e60249cd34c95930c8ac5cb14
34dac155772ed3e2692800130100002e00330024001d0020c9828876
112095fe66762bdbf7c672e156d6cc253b833df1dd69b1b04e751f0f
002b00020304TLS, and What Each Version Fixed
# TLS 1.3's key schedule, reproduced from RFC 8448's published handshake, value by value.
import hashlib, hmac
P = 2**255 - 19
# ---- X25519, RFC 7748 s.5 -----------------------------------------------------------------
def x25519(k, u):
k = int.from_bytes(k, 'little'); k &= ~7; k &= ~(128 << 8 * 31); k |= 64 << 8 * 31
u = int.from_bytes(u, 'little') & ((1 << 255) - 1)
x1, x2, z2, x3, z3, swap = u, 1, 0, u, 1, 0
for t in reversed(range(255)):
bit = (k >> t) & 1; swap ^= bit
if swap: x2, x3, z2, z3 = x3, x2, z3, z2
swap = bit
A = x2 + z2; AA = A * A; B = x2 - z2; BB = B * B; E = AA - BB
C = x3 + z3; Dd = x3 - z3; DA = Dd * A; CB = C * B
x3 = (DA + CB) ** 2 % P; z3 = x1 * (DA - CB) ** 2 % P
x2 = AA * BB % P; z2 = E * (AA + 121665 * E) % P
if swap: x2, x3, z2, z3 = x3, x2, z3, z2
return (x2 * pow(z2, P - 2, P) % P).to_bytes(32, 'little')
# ---- RFC 8448 s.3: the key schedule of a published TLS 1.3 handshake ---------------------
def hkdf_extract(salt, ikm): # RFC 5869, with SHA-256
return hmac.new(salt, ikm, hashlib.sha256).digest()
def hkdf_expand_label(secret, label, context, length): # RFC 9846 s.7.1 (RFC 8446 s.7.1)
full = b'tls13 ' + label
info = length.to_bytes(2, 'big') + bytes([len(full)]) + full + bytes([len(context)]) + context
out, t, i = b'', b'', 1
while len(out) < length:
t = hmac.new(secret, t + info + bytes([i]), hashlib.sha256).digest()
out, i = out + t, i + 1
return out[:length]
def derive_secret(secret, label, messages):
return hkdf_expand_label(secret, label, hashlib.sha256(messages).digest(), 32)
hellos = {}
for line in open('rfc8448-hellos.hex').read().split('\n'):
if line.startswith('# '):
name = line.split()[1].rstrip(',')
hellos[name] = b''
elif line:
hellos[name] += bytes.fromhex(line)
transcript = hellos['ClientHello'] + hellos['ServerHello']
client_private = bytes.fromhex('49af42ba7f7994852d713ef2784bcbcaa7911de26adc5642cb634540e7ea5005')
server_public = bytes.fromhex('c9828876112095fe66762bdbf7c672e156d6cc253b833df1dd69b1b04e751f0f')
PRINTED = { # the values RFC 8448 s.3 prints (it calls the main secret "master")
'shared': '8bd4054fb55b9d63fdfbacf9f04b9f0d35e6d63f537563efd46272900f89492d',
'early': '33ad0a1c607ec03b09e6cd9893680ce210adf300aa1f2660e1b22e10f170f92a',
'handshake': '1dc826e93606aa6fdc0aadc12f741b01046aa6b99f691ed221a9f0ca043fbeac',
'c hs traffic': 'b3eddb126e067f35a780b3abf45e2d8f3b1a950738f52e9600746a0e27a55a21',
's hs traffic': 'b67b7d690cc16c4e75e54213cb2d37b4e9c912bcded9105d42befd59d391ad38',
'main': '18df06843d13a08bf2a449844c5f8a478001bc4d4c627984d5a41da8d0402919',
'server key': '3fce516009c21727d0f2e4e86ee403bc',
'server iv': '5d313eb2671276ee13000b30',
}
zero = bytes(32)
got = {}
got['shared'] = x25519(client_private, server_public)
got['early'] = hkdf_extract(zero, zero)
got['handshake'] = hkdf_extract(derive_secret(got['early'], b'derived', b''), got['shared'])
got['c hs traffic'] = derive_secret(got['handshake'], b'c hs traffic', transcript)
got['s hs traffic'] = derive_secret(got['handshake'], b's hs traffic', transcript)
got['main'] = hkdf_extract(derive_secret(got['handshake'], b'derived', b''), zero)
got['server key'] = hkdf_expand_label(got['s hs traffic'], b'key', b'', 16)
got['server iv'] = hkdf_expand_label(got['s hs traffic'], b'iv', b'', 12)
print('RFC 8448 s.3 REPRODUCED (%d-byte ClientHello, %d-byte ServerHello)'
% (len(hellos['ClientHello']), len(hellos['ServerHello'])))
STEP = {'shared': 'X25519 of the client key and the server share',
'early': 'HKDF-Extract(0, 0): no pre-shared key',
'handshake': 'HKDF-Extract(Derive-Secret(early, "derived"), shared)',
'c hs traffic': 'Derive-Secret(handshake, "c hs traffic", hellos)',
's hs traffic': 'Derive-Secret(handshake, "s hs traffic", hellos)',
'main': 'HKDF-Extract(Derive-Secret(handshake, "derived"), 0)',
'server key': 'HKDF-Expand-Label(s hs traffic, "key", 16)',
'server iv': 'HKDF-Expand-Label(s hs traffic, "iv", 12)'}
for name in PRINTED:
print(' %-12s %-53s %s' % (name, STEP[name], got[name].hex() == PRINTED[name]))
# ---- the keys are bound to the hellos: change one bit of the ClientHello ------------------
altered = bytearray(transcript)
altered[100] ^= 1
assert bytes(altered) != transcript
key = hkdf_expand_label(derive_secret(got['handshake'], b's hs traffic', bytes(altered)),
b'key', b'', 16)
print()
print('ONE BIT OF THE CLIENTHELLO CHANGED')
print(' server handshake key %s' % key.hex())
print(' the same as before: %s' % (key == got['server key']))TLS, and What Each Version Fixed
RFC 8448 s.3 REPRODUCED (196-byte ClientHello, 90-byte ServerHello)
shared X25519 of the client key and the server share True
early HKDF-Extract(0, 0): no pre-shared key True
handshake HKDF-Extract(Derive-Secret(early, "derived"), shared) True
c hs traffic Derive-Secret(handshake, "c hs traffic", hellos) True
s hs traffic Derive-Secret(handshake, "s hs traffic", hellos) True
main HKDF-Extract(Derive-Secret(handshake, "derived"), 0) True
server key HKDF-Expand-Label(s hs traffic, "key", 16) True
server iv HKDF-Expand-Label(s hs traffic, "iv", 12) True
ONE BIT OF THE CLIENTHELLO CHANGED
server handshake key 94712fcdb8f02c7e447fa656c5c1d2a4
the same as before: FalseWhat the run establishes, in order.
HKDF replaces the PRF. TLS 1.2 expanded one master secret with its PRF, as the handshake chapter ran. TLS 1.3 builds a chain: an early secret, then a handshake secret that mixes in the key exchange's result, then the main secret, which RFC 8446 and RFC 8448 call the master secret. Each step is HKDF-Extract, and each key is HKDF-Expand-Label with a label of its own ("c hs traffic", "s hs traffic", "key", "iv"), so the client's keys and the server's, and the handshake's and the data's, are always different.
The hellos are hashed into the keys. The handshake secrets are derived from a hash of the ClientHello and the ServerHello. With one bit of the ClientHello changed, the server's handshake key is completely different, so an attacker who edits either hello leaves the two sides holding keys that do not match, and the handshake fails.
What is forbidden now
- SSL 2.0. RFC 6176 (March 2011) requires that TLS clients and servers "never negotiate the use of SSL version 2.0".
- SSL 3.0. RFC 7568 (June 2015): "SSLv3 MUST NOT be used", because it "is not sufficiently secure"; its CBC padding "trivially permits the recovery of plaintext", the POODLE attack.
- TLS 1.0 and TLS 1.1. RFC 8996 (March 2021): "TLS 1.0 MUST NOT be used" and "TLS 1.1 MUST NOT be used". Among its reasons: "The integrity of the handshake depends on SHA-1 hash", and neither version supports AEAD ciphers. RFC 9846 (Appendix E.5) repeats it for all four old versions: "they MUST NOT be negotiated for any reason".
- RSA key exchange, even in TLS 1.2. RFC 10015 (July 2026): "Clients MUST NOT offer and servers MUST NOT select RSA cipher suites in (D)TLS 1.2 connections". Finite-field Diffie-Hellman in TLS 1.2 is forbidden in the same words.
TLS, and What Each Version Fixed
New names for old secrets. RFC 9846 (Appendix D) renames TLS 1.2's master secret the main secret and the pre-master secret the preliminary secret, keeping the label inside the PRF unchanged "for compatibility". An answer written from the syllabus's textbook uses master secret, and that is correct for the exam; knowing the new names shows the answer is current.
Distinctions that carry marks
| TLS 1.2 | TLS 1.3 | |
|---|---|---|
| Round trips before data | two | one, or none for early data |
| Key exchange | RSA or Diffie-Hellman, ephemeral or not | ephemeral Diffie-Hellman or a hybrid with ML-KEM, always forward secret |
| Record protection | CBC with a MAC, or AEAD | AEAD only |
| A cipher suite names | the key exchange, the signature, the cipher and the hash, which OpenSSL prints as ECDHE-ECDSA-AES256-GCM-SHA384 | the cipher and the hash only: TLS_AES_256_GCM_SHA384 |
| Server certificate | sent in the clear | encrypted |
| Key derivation | the PRF, from a master secret | the HKDF key schedule |
| Where the version goes | the version field, 0x0303 | the supported_versions extension, 0x0304 |
| Compression and ChangeCipherSpec | present | removed |
| Marker in the server random | Means |
|---|---|
| 44 4F 57 4E 47 52 44 01 ("DOWNGRD" 01) | a TLS 1.3 server negotiated TLS 1.2 |
| 44 4F 57 4E 47 52 44 00 ("DOWNGRD" 00) | a server negotiated TLS 1.1 or below |
What beginners get wrong here
Treating SSL and TLS as rival protocols. TLS is SSL continued under a new name by the IETF. Every version of SSL is now prohibited; people who say "SSL certificate" mean a certificate used with TLS.
Giving TLS 1.0 or 1.2 as the latest version. TLS 1.0 and 1.1 have been deprecated since 2021. The current version is TLS 1.3, specified since July 2026 by RFC 9846.
Reading a TLS 1.3 cipher suite as naming the key exchange. TLS_AES_256_GCM_SHA384 says nothing about how the keys were agreed; that is the key share group, X25519MLKEM768 for munotes.in.
Calling 0-RTT a free speed-up. Early data can be replayed by an attacker and is not forward secret.
Saying TLS always encrypts the certificate. Only TLS 1.3 does; TLS 1.2 sends it in the clear.
Thinking the downgrade mark is secret. It is a fixed, public value. It works because the server signs its random value, so the mark cannot be removed unnoticed.
TLS, and What Each Version Fixed
Quick revision
- TLS is the IETF's SSL: RFC 2246 (January 1999), version {3, 1} because it is a minor change to SSL 3.0.
- SSL 3.0 to TLS 1.0: version, HMAC, the PRF, master secret, alerts (no_certificate out, 12 in), cipher suites (Fortezza out), certificate types, certificate_verify and finished (12 bytes from the PRF), padding (up to 255 bytes).
- 1.1 (2006): explicit IV. 1.2 (2008): SHA-256 PRF, named hash in signatures, AEAD. 1.3 (2018, re-published as RFC 9846 in July 2026): one round trip, forward secrecy always, AEAD only, encrypted handshake, HKDF.
- Forbidden: SSL 2.0 (RFC 6176), SSL 3.0 (RFC 7568), TLS 1.0 and 1.1 (RFC 8996), RSA and finite-field DH key exchange in TLS 1.2 (RFC 10015).
- Downgrade mark: DOWNGRD with 01 (fell to 1.2) or 00 (fell to 1.1 or below), protected by the server's signature.
- munotes.in, 30 September 2026: TLS 1.3, TLS_AES_256_GCM_SHA384, key exchange X25519MLKEM768.
- TLS 1.2's master and pre-master secrets are called the main and preliminary secrets since RFC 9846.
Test yourself
1. What is TLS? State how TLS 1.0 differs from SSL 3.0. TLS is the IETF's standard version of SSL, first specified in RFC 2246 in 1999. TLS 1.0 differs from SSL 3.0 in the version number (3.1 against 3.0); the MAC, which is HMAC and also covers the version; the pseudorandom function, which expands secrets using P_MD5 and P_SHA-1 together and also computes the master secret; the alert codes, where no_certificate was removed and twelve were added; the cipher suites, where Fortezza was dropped; the client certificate types, which fell from seven to four; the certificate_verify message, which hashes only the handshake messages; the finished message, which is 12 bytes computed with the PRF; and the padding, which may be up to 255 bytes to hide message lengths.
2. Why does TLS 1.0 carry the version number 3.1? Because TLS 1.0 is a minor modification of SSL 3.0, whose version is 3.0. RFC 2246 calls the value 3.1 historical. TLS 1.1 and 1.2 continued the scheme as 3.2 and 3.3.
3. What did TLS 1.1 and TLS 1.2 each change? TLS 1.1 (RFC 4346, 2006) replaced the implicit CBC initialisation vector with an explicit one to protect against CBC attacks, and reported padding errors with bad_record_mac instead of decryption_failed. TLS 1.2 (RFC 5246, 2008) replaced the MD5 and SHA-1 combination in the PRF with a PRF chosen by the cipher suite, P_SHA256 in all its own suites, replaced the combination in signatures with a single named hash, and added authenticated encryption modes such as AES-GCM.
4. List the major differences between TLS 1.2 and TLS 1.3. TLS 1.3 completes the handshake in one round trip instead of two, and allows zero-round-trip early data on resumption; it removes static RSA and Diffie-Hellman, so every key exchange is forward secret; it keeps only AEAD ciphers and removes compression; it encrypts every handshake message after the ServerHello, including the certificate; it replaces the PRF with an HKDF key schedule; it moves version negotiation into the supported_versions extension and drops ChangeCipherSpec; and its cipher suites name only the AEAD cipher and hash.
TLS, and What Each Version Fixed
5. Why can a TLS 1.3 handshake finish in one round trip? Because the client sends its Diffie-Hellman key share in the ClientHello, guessing the group the server will choose. The server can then compute the shared secret at once and send its own share, its encrypted certificate, CertificateVerify and Finished in a single reply. In TLS 1.2 the key exchange needed a second round trip after the hellos.
6. How does TLS 1.3 protect against an attacker forcing an older version? First, the hellos are hashed into the handshake keys, so any change to them makes the keys disagree and the handshake fail. Second, a TLS 1.3 server that negotiates TLS 1.2 must end its random value with the bytes DOWNGRD 01, or DOWNGRD 00 for an older version, and a client that offered TLS 1.3 must abort if it sees them. Because in TLS 1.2 the server signs its random value, the mark cannot be erased without the signature failing, as munotes.in's own handshake shows.
7. Which versions of SSL and TLS may no longer be used, and on whose authority? SSL 2.0 is prohibited by RFC 6176 (2011), SSL 3.0 by RFC 7568 (2015), and TLS 1.0 and 1.1 by RFC 8996 (2021); RFC 9846 (2026) repeats that none of the four may be negotiated for any reason. TLS 1.2 remains in use, but RFC 10015 (2026) forbids RSA and finite-field Diffie-Hellman key exchange within it.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself for the same subject.