Howdy! No longer too prolonged ago I’ve been eager about how I salvage it enjoyable to be taught computer
networking by implementing working versions of staunch community protocols.
And it made me wonder – I’ve implemented toy versions of
traceroute, TCP and DNS.
What about TLS? Would possibly well per chance well I put into effect a toy model of that to be taught extra about how it works?
I asked on Twitter if this would per chance well well be laborious, bought some encouragement and pointers for where to delivery, so I made up my thoughts to pace for it.
This used to be in level of fact enjoyable and I discovered a miniature extra about how fervent staunch
cryptography is – thanks to cryptopals, I already 100% believed that I mustn’t manufacture my maintain
crypto implementations, and seeing how the crypto in TLS 1.3 works gave me even extra of
an appreciation for why I shouldn’t 🙂
As a warning: I’m in level of fact no longer a cryptography particular person, I will per chance whisper some
unsuitable things about cryptography on this post and I fully originate no longer know
the historical past of past TLS vulnerabilities that told TLS 1.3’s create.
All of that talked about, let’s pace put into effect some cryptography! All of my hacky code is on github. I made up my thoughts to make utilize of Saunter due to this of I heard that Saunter has suited crypto libraries.
the simplifications
I only desired to work on this for a few days at most, so I needed to create some
comely dramatic simplifications to create it conceivable to salvage it accomplished snappy.
I made up my thoughts my procedure used to be going to be to download this weblog’s homepage with TLS. So I
don’t must put into effect a fully frequent TLS implementation, I suited must
successfully join to 1 internet internet page.
Particularly, this means that:
- I only improve one cipher suite
- I don’t check the server’s certificates the least bit, I suited ignore it
- my parsing and message formatting may per chance well be extraordinarily janky and fragile due to this of I only must be in a suite to consult with one particular TLS implementation (and imagine me, they are)
an improbable TLS resource: tls13.ulfheim.salvage
Fortunately, earlier than beginning this I remembered vaguely that I’d considered a arena that
defined every single byte in a TLS 1.3 connection, with detailed code examples to
reproduce every portion. Some googling published that it used to be The Contemporary Illustrated TLS Connection.
I’m able to’t stress ample how ample this used to be, I checked out per chance greater than a
hundred times and I only checked out the TLS 1.3 RFC for a few limited things.
some cryptography fundamentals
Sooner than I started engaged on this, my idea of TLS used to be:
- in the beginning there’s some form of Diffie-Hellman key alternate
- you make utilize of the principle alternate to by some capacity per chance well (how???) salvage an AES symmetric key and encrypt the relaxation of the reference to AES
This used to be form of suited, on the other hand it seems it’s extra subtle than that.
Okay, let’s salvage into my hacky toy TLS implementation. It optimistically goes with out pronouncing that you just may per chance fully no longer utilize this code for anything else.
step 1: whisper hello
First now we want to send a “Consumer Howdy” message. For my choices this has suited 4 objects of recordsdata in it:
- A randomly generated public key
- 32 bytes of random recordsdata (the “Consumer Random”)
- The arena name I want to join to (
jvns.ca
) - The cipher suites/signature algorithms we want to make utilize of (which I suited copied
from tls.ulfheim.salvage). This negotiation job is comely essential in
frequent but I’m ignoring it due to this of I only improve one signature algorithm /
cipher suite.
Essentially the most attention-grabbing portion of this to me used to be portion 1 – how originate I generate the general public key?
I used to be confused about this for some time on the other hand it ended up being suited 2 lines of code.
privateKey :=random(32)
publicKey, err :=curve25519.X25519(privateKey, curve25519.Basepoint)
It is seemingly you’ll per chance well well leer the relaxation of the code to generate the client hello message right here
on the other hand it’s very dreary, it’s suited a quantity of bit fiddling.
elliptic curve cryptography is cool
I’m no longer going to provide an rationalization of elliptic curve cryptography right here, but I suited want to whisper how level out how cool it is that you just may per chance well per chance also:
- generate a random 32-byte string as a interior most key
- “multiply” the interior most key by the curve’s contemptible level to salvage the general public key (right here’s elliptic curve “multiplication”, where
n P
manner “add P to itself n times”) - that’s it!!
I wrote “multiply” in fright quotes due to this of this “multiplication” doesn’t allow you to
multiply choices on the elliptic curve by every assorted. It is seemingly you’ll per chance well well only multiply a
level by an integer.
Here’s the feature signature of the X25519
feature we utilize to originate the
“multiplication”. It is seemingly you’ll per chance well well leer surely one of many arguments is called scalar
and one
is called level
. And the convey of the arguments issues! Whereas you switch them it
gained’t originate the suited aspect.
func X25519(scalar, level []byte) ([]byte, error)
I’m no longer going to whisper extra about elliptic curve cryptography right here but I love how
straightforward right here’s to make utilize of – it seems loads straightforward than RSA where your
interior most keys must be prime numbers.
I don’t know if “you may per chance well per chance also utilize any 32-byte string as a interior most key” is correct
for all elliptic curves or suited for this particular elliptic curve (Curve25519).
step 2: parse the server hello
Subsequent the server says hello. That is amazingly dreary, usually we suited must
parse it to salvage the server’s public key which is 32 bytes. Here’s the code though.
step 3: calculate the keys to encrypt the handshake
Now that now we have the server’s public key and we’ve despatched the server our public
key, we can delivery to calculate the keys we’re going to make utilize of to in level of fact encrypt
recordsdata.
I used to be taken aback to be taught that there are as a minimal 4 assorted symmetric keys desirous about TLS:
- client handshake key/iv (for the info the client sends within the handshake)
- server handshake key/iv (for the info the server sends within the handshaek)
- client application key/iv (for the relaxation of the info the client sends)
- server application key/iv (for the relaxation of the info the server sends)
- I ponder also one more key for session resumption, but I didn’t put into effect that
We delivery out by combining the server’s public key and our interior most key to salvage a
shared secret. That is called “elliptic curve diffie hellman” or ECDH and it’s
comely straightforward: “multiply” the server’s interior most key by our public key:
sharedSecret, err :=curve25519.X25519(session.Keys.Deepest, session.ServerHello.PublicKey)
This gives us a 32-byte secret key that both the client and the server has. Yay!
But we want 96 bytes (16 + 12) 4 of keys in entire. That’s greater than 32 bytes!
time for key derivation
It appears to be like the style you switch a limited key into extra keys is called “key
derivation”, and TLS 1.3 makes utilize of an algorithm called “HKDF” to originate this. I in level of fact originate no longer
perceive this but right here’s what my code to originate it appears to be like as if.
It seems to maintain alternately calling hkdf.Make bigger
and hkdf.Extract
over and
all over all once more a bunch of times.
func (session *Session) MakeHandshakeKeys() {
zeros :=create([]byte, 32)
psk :=create([]byte, 32)
// alright to this level
if err !=nil {
apprehension(err)
}
earlySecret :=hkdf.Extract(sha256.Contemporary, psk, zeros) // TODO: psk may per chance well be defective
derivedSecret :=deriveSecret(earlySecret, "derived", []byte{})
session.Keys.HandshakeSecret=hkdf.Extract(sha256.Contemporary, sharedSecret, derivedSecret)
handshakeMessages :=concatenate(session.Messages.ClientHello.Contents(), session.Messages.ServerHello.Contents())
cHsSecret :=deriveSecret(session.Keys.HandshakeSecret, "c hs traffic", handshakeMessages)
session.Keys.ClientHandshakeSecret=cHsSecret
session.Keys.ClientHandshakeKey=hkdfExpandLabel(cHsSecret, "key", []byte{}, 16)
session.Keys.ClientHandshakeIV=hkdfExpandLabel(cHsSecret, "iv", []byte{}, 12)
sHsSecret :=deriveSecret(session.Keys.HandshakeSecret, "s hs traffic", handshakeMessages)
session.Keys.ServerHandshakeKey=hkdfExpandLabel(sHsSecret, "key", []byte{}, 16)
session.Keys.ServerHandshakeIV=hkdfExpandLabel(sHsSecret, "iv", []byte{}, 12)
}
This used to be comely annoying to salvage working due to this of I kept passing the defective
arguments to things. The single motive I managed it used to be due to this of
https://tls13.ulfheim.salvage equipped a bunch of instance inputs and outputs and
instance code so I used to be in a suite to write some unit assessments and check my code against
the internet page’s instance implementation.
Anyway, one way or the other I bought all my keys calculated and it used to be time to delivery decrypting!
an aside on IVs
For every key there’s also an “IV” which stands for “initialization vector”. The
idea seems to be to make utilize of an even initialization vector for every message we
encrypt/decrypt, for Extra Safety ™.
On this implementation the style we salvage an even IV for every message is by
xoring the IV with the different of messages despatched/bought to this level.
step 4: write some decryption code
Now that now we have all these keys and IVs, we can write a decrypt
feature.
I presumed that TLS suited inclined AES, on the other hand it appears to be like it makes utilize of something called
“authentication encryption” on high of AES that I hadn’t heard of earlier than.
The wikipedia article rationalization of authenticated encryption is de facto comely streak:
… authenticated encryption can provide security against chosen ciphertext attack. In these assaults, an adversary makes an try to create an advantage against a cryptosystem (e.g., details about the secret decryption key) by submitting in moderation chosen ciphertexts to a couple “decryption oracle” and analyzing the decrypted results. Authenticated encryption schemes can gape improperly-constructed ciphertexts and refuse to decrypt them. This, in turn, prevents the attacker from requesting the decryption of any ciphertext except it used to be generated precisely the usage of the encryption algorithm
This makes sense to me due to this of I did seemingly the most cryptopals challenges and there’s an attack quite like this in cryptopals place 2 (I don’t know if it’s the actual same aspect).
Anyway, right here’s some code that makes utilize of authenticated encryption the style the TLS 1.3
spec says it is going to. I ponder GCM is an authenticated encryption algorithm.
func decrypt(key, iv, wrapper []byte) []byte {
block, err :=aes.NewCipher(key)
if err !=nil {
apprehension(err.Error())
}
aesgcm, err :=cipher.NewGCM(block)
if err !=nil {
apprehension(err.Error())
}
extra :=wrapper[:5]
ciphertext :=wrapper[5:]
plaintext, err :=aesgcm.Originate(nil, iv, ciphertext, extra)
if err !=nil {
apprehension(err.Error())
}
return plaintext
}
step 5: decrypt the server handshake
Subsequent the server sends some extra handshake recordsdata. This contains the certificates
and a few assorted stuff.
Here’s my code for decrypting the handshake. In total it suited reads the
encrypted recordsdata from the community, decrypts it, and saves it.
yarn :=readRecord(session.Conn)
if yarn.Form() !=0x17 {
apprehension("expected wrapper")
}
session.Messages.ServerHandshake=decrypt(session.Keys.ServerHandshakeKey, session.Keys.ServerHandshakeIV, yarn)
That you just can gape that we don’t in actuality parse this knowledge the least bit – that’s
due to this of we don’t want the contents, since we’re no longer verifying the server’s
certificates.
I used to be taken aback that you just don’t technically must see at the server’s
certificates the least bit to create a TLS connection (though obviously you may per chance check it!). I presumed that you just would be capable to must as a minimal parse it to salvage a
key out of it or something.
We originate must be in a suite to hash the handshake for the following step though, so we
want to store it.
step 6: fetch extra keys
We utilize a hash of the SHA256 handshake recordsdata we suited bought from the server to
generate even extra symmetric keys. That is kind of the closing step!
That is kind of exactly the connected to the principle derivation code from earlier than, but I’m
collectively with it due to this of I used to be taken aback by how out of the ordinary work needed to be accomplished to generate all these keys.
func (session *Session) MakeApplicationKeys() {
handshakeMessages :=concatenate(
session.Messages.ClientHello.Contents(),
session.Messages.ServerHello.Contents(),
session.Messages.ServerHandshake.Contents())
zeros :=create([]byte, 32)
derivedSecret :=deriveSecret(session.Keys.HandshakeSecret, "derived", []byte{})
masterSecret :=hkdf.Extract(sha256.Contemporary, zeros, derivedSecret)
cApSecret :=deriveSecret(masterSecret, "c ap traffic", handshakeMessages)
session.Keys.ClientApplicationKey=hkdfExpandLabel(cApSecret, "key", []byte{}, 16)
session.Keys.ClientApplicationIV=hkdfExpandLabel(cApSecret, "iv", []byte{}, 12)
sApSecret :=deriveSecret(masterSecret, "s ap traffic", handshakeMessages)
session.Keys.ServerApplicationKey=hkdfExpandLabel(sApSecret, "key", []byte{}, 16)
session.Keys.ServerApplicationIV=hkdfExpandLabel(sApSecret, "iv", []byte{}, 12)
}
step 7: quit the handshake
Subsequent now we want to send a “handshake executed” message to the server to verify that the entire lot is accomplished. That code is right here.
And now we’re accomplished the handshake! That used to be the laborious portion, sending and receiving
the info is rather straightforward.
step 8: create a HTTP question
I wrote a SendData
feature that encrypts and sends recordsdata the usage of our keys. This time we’re the usage of the “application” keys and no longer the handshake keys. This made making a HTTP question comely straightforward:
req :=fmt.Sprintf("GET / HTTP/1.1rnHost: %srnrn", arena)
session.SendData([]byte(req))
step 9: we can in actuality decrypt the response!!!
Now comes the 2nd I’d been ready for — in actuality decrypting the response
from the server!!! But right here I needed to be taught something else about TLS.
TLS recordsdata comes in blocks
I previously thought that while you established the connection, encrypted TLS
recordsdata used to be suited a creep. But that’s no longer how it works – as an different, it’s
transmitted in blocks. Devour, you’ll salvage a chunk of ~1400 bytes to decrypt, and
then one more chunk, and then one more chunk.
I’m no longer particular why the blocks have the scale they originate (per chance it’s in convey that every will fit interior a TCP
packet ???), but in idea I ponder they’d per chance well be up to 65535 bytes, since their
dimension arena is 2 bytes. The blocks I bought were all 1386 bytes every.
On every occasion we salvage a block, now we want to:
- calculate a brand new IV as
old_iv xor num_records_received
- decrypt it the usage of the principle and the brand new IV
- increment the rely of recordsdata bought
Here’s what the ReceiveData()
feature I wrote appears to be like as if.
Essentially the most attention-grabbing portion of right here’s the iv[11] ^=session.RecordsReceived
–
that’s the portion that adjusts the IV for every block.
func (session *Session) ReceiveData() []byte {
yarn :=readRecord(session.Conn)
iv :=create([]byte, 12)
reproduction(iv, session.Keys.ServerApplicationIV)
iv[11] ^=session.RecordsReceived
plaintext :=decrypt(session.Keys.ServerApplicationKey, iv, yarn)
session.RecordsReceived +=1
return plaintext
}
This iv[11]
aspect assumes that there are much less than 255 blocks which obviously
is no longer correct in frequent in TLS, but I used to be lazy and to download my weblog’s
homepage I only needed 82 blocks.
We in level of fact decide to originate this once we send recordsdata too, but I didn’t put into effect it
due to this of we only despatched 1 packet.
recount: getting the total block of tLS recordsdata
I ran into one recount with TCP where usually I’d strive to read a block of TLS
recordsdata (~1386 bytes), but I wouldn’t salvage the total aspect. I wager the TLS blocks
may per chance well be split up across so much of TCP packets.
I mounted this in a terribly boring manner, by suited polling the TCP connection in a loop
unless it gave me the info I needed. Here’s my code to originate that:
func read(dimension int, reader io.Reader) []byte {
var buf []byte
for len(buf) !=dimension {
buf=append(buf, readUpto(dimension-len(buf), reader)...)
}
return buf
}
I purchase a staunch TLS implementation would utilize a thread pool or coroutines or
something to attend a watch on this.
step 10: shimmering once we’re accomplished
When the HTTP response is accomplished, we salvage these bytes: []byte{48, 13, 10, 13, 10, 23}
.
This seems to be due to this of my HTTP server is the usage of chunked switch encoding, so
there’s no Voice material-Length
header and I must label for those bytes at the
end as an different.
So right here’s the code to receive the HTTP response. In total we suited loop unless
we leer those bytes, then we end.
func (session *Session) ReceiveHTTPResponse() []byte {
var response []byte
for {
pt :=session.ReceiveData()
if string(pt)==string([]byte{48, 13, 10, 13, 10, 23}) {
damage
}
response=append(response, pt...)
}
return response
}
that’s it!
In the damage, I ran this system and I downloaded my weblog’s homepage! It worked! Here’s what the results see like:
$ pace create; ./limited-tls
HTTP/1.1 200 OK
Date: Wed, 23 Mar 2022 19: 37: 47 GMT
Voice material-Form: text/html
Switch-Encoding: chunked
Connection: attend-alive
... loads extra headers and HTML be conscious...
Okay, the results are manufacture of anticlimactic, it’s suited the connected to what you’d
leer in case you ran curl -i https://jvns.ca
excluding and not utilizing a formatting. But I used to be
extraordinarily excited after I saw it.
unit assessments are excellent
On every occasion I write networking code like this, I neglect that unit testing is
suited, and I thrash around with a bunch of parsing / formatting code that does
no longer work and suited getting NOPE messages serve from the server on the assorted end.
After which I bear in mind unit assessments. On this case, I copied a bunch of the info from
the https://tls13.ulfheim.salvage instance and set aside it into my unit assessments in convey that I
can also snappy create obvious that my parsing and crypto were working precisely.
It made the entire lot about 10 times more straightforward and sooner.
some things I discovered
This used to be in level of fact enjoyable! I discovered that
- elliptic curve diffie-hellman is amazingly cool, and as a minimal with Curve25519 you may per chance well per chance also utilize literally any 32-byte string as a interior most key
- there are a LOT of varied symmetric keys desirous about TLS and the principle derivation job is comely subtle
- TLS makes utilize of AES with some extra “authenticated encryption” algorithms on high
- TLS recordsdata is despatched/bought as a bunch of blocks, no longer as a creep
My code in level of fact is dreadful, it is going to join to my internet page (jvns.ca
) and I ponder literally no assorted internet sites.
I gained’t pretend to device close the entire causes TLS is designed this kind, on the other hand it
used to be a enjoyable manner to utilize a few days, I in level of fact feel a miniature extra told, and I
ponder it’ll be more straightforward for me to device close things I read about TLS within the
future.
a walk for cryptopals
In convey for you to salvage out about cryptography and also you haven’t tried the
cryptopals challenges, I in level of fact counsel them – you
salvage to put into effect a quantity of assaults on crypto systems and it’s very enjoyable.