123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304(* Claude Code
*
* Copyright (C) 2026 Yoann Padioleau
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public License
* (LGPL) as published by the Free Software Foundation; either version
* 2 of the License, or (at your option) any later version.
*)(* See Tls13.mli *)(*****************************************************************************)(* Bytes *)(*****************************************************************************)letu8(n:int):string=String.make1(Char.chr(nland0xff))letu16(n:int):string=u8(nlsr8)^u8nletu24(n:int):string=u8(nlsr16)^u16nletget8(s:string)(i:int):int=Char.codes.[i]letget16(s:string)(i:int):int=(get8silsl8)lorget8s(i+1)letget24(s:string)(i:int):int=(get8silsl16)lorget16s(i+1)(* a vector: its length in [n] bytes, then its bytes *)letvec8(s:string):string=u8(String.lengths)^sletvec16(s:string):string=u16(String.lengths)^s(*****************************************************************************)(* The key schedule *)(*****************************************************************************)lethash=Sha256.digestlethash_len=32lethmac=Hmac.sha256lethkdf_expand_label(secret:string)~(label:string)~(context:string)(length:int):string=Hkdf.expand~hmacsecret~info:(u16length^vec8("tls13 "^label)^vec8context)lengthletderive_secret(secret:string)(label:string)(transcript:string):string=hkdf_expand_labelsecret~label~context:(hashtranscript)hash_lenletzeros=String.makehash_len'\000'letearly_secret=Hkdf.extract~hmac~salt:zeroszeroslethandshake_secret(shared:string):string=Hkdf.extract~hmac~salt:(derive_secretearly_secret"derived""")sharedletmaster_secret(hs:string):string=Hkdf.extract~hmac~salt:(derive_secreths"derived""")zerostypecipher=Chacha20_poly1305|Aes128_gcmtypekeys={cipher:cipher;key:string;iv:string;seq:int}lettraffic_keys(cipher:cipher)(secret:string):keys=letkey_len=matchcipherwithChacha20_poly1305->32|Aes128_gcm->16in{cipher;key=hkdf_expand_labelsecret~label:"key"~context:""key_len;iv=hkdf_expand_labelsecret~label:"iv"~context:""12;seq=0}letfinished(secret:string)(transcript:string):string=hmac(hkdf_expand_labelsecret~label:"finished"~context:""hash_len)(hashtranscript)(*****************************************************************************)(* The records *)(*****************************************************************************)(* the IV xored with the record's number, on its last 8 bytes *)letnonce(k:keys):string=String.mapi(funic->ifi<4thencelseChar.chr(Char.codeclxor((k.seqlsr(8*(11-i)))land0xff)))k.ivletseal(k:keys)(content_type:int)(data:string):string*keys=letinner=data^u8content_typeinletheader=u823^u160x0303^u16(String.lengthinner+16)inletaead=matchk.cipherwithChacha20_poly1305->Chacha20_poly1305.seal|Aes128_gcm->Gcm.sealin(header^aead~key:k.key~nonce:(noncek)~aad:headerinner,{kwithseq=k.seq+1})letopen_record(k:keys)(header:string)(body:string):((int*string)*keys)option=letaead=matchk.cipherwithChacha20_poly1305->Chacha20_poly1305.open_|Aes128_gcm->Gcm.open_inmatchaead~key:k.key~nonce:(noncek)~aad:headerbodywith|None->None|Someinner->(* the padding's zeros, then the real type *)letreclasti=ifi<0thenNoneelseifinner.[i]<>'\000'thenSomeielselast(i-1)inOption.map(funi->((get8inneri,String.subinner0i),{kwithseq=k.seq+1}))(last(String.lengthinner-1))(*****************************************************************************)(* The client *)(*****************************************************************************)typestate=Handshaking|Open|Closed|Failedofstringtypewaiting=Server_hello|Encrypted_extensions|Certificate|Certificate_verify|Server_finished|Donetypet={secret:string;verify:X509.tlist->(unit,string)result;state:state;waiting:waiting;inbox:string;(* bytes not yet a whole record *)pending:string;(* handshake bytes not yet a whole message *)transcript:string;cipher:cipheroption;hs:string;(* the handshake secret *)client_secret:string;(* this phase's traffic secrets *)server_secret:string;read_keys:keysoption;write_keys:keysoption;chain:X509.tlist;certificate_request:stringoption;(* its context, if the server asked for our certificate *)app:Buffer.t;}lethello_retry="\xcf\x21\xad\x74\xe5\x9a\x61\x11\xbe\x1d\x8c\x02\x1e\x65\xb8\x91\xc2\xa2\x11\x16\x7a\xbb\x8c\x5e\x07\x9e\x09\xe2\xc8\xa8\x33\x9c"letclient_hello~(host:string)~(random:string)~(public:string)~(session_id:string):string=letexttypdata=u16typ^vec16datainletextensions=String.concat""[ext0x0000(vec16(u80^vec16host))(* server_name *);ext0x000a(vec16(u160x001d))(* supported_groups: x25519 *);ext0x000d(vec16(String.concat""(List.mapu16[0x0403;0x0503;0x0804;0x0805;0x0806;0x0401;0x0501;0x0601])))(* signature_algorithms *);ext0x002b(vec8(u160x0304))(* supported_versions: 1.3 *);ext0x0033(vec16(u160x001d^vec16public))(* key_share *);]inletbody=u160x0303^random^vec8session_id^vec16(u160x1303^u160x1301)^vec8"\000"^vec16extensionsinu81^u24(String.lengthbody)^bodyletclient~(host:string)~(random:string)~(secret:string)~(session_id:string)~(verify:X509.tlist->(unit,string)result):t*string=lethello=client_hello~host~random~public:(X25519.public_keysecret)~session_idin({secret;verify;state=Handshaking;waiting=Server_hello;inbox="";pending="";transcript=hello;cipher=None;hs="";client_secret="";server_secret="";read_keys=None;write_keys=None;chain=[];certificate_request=None;app=Buffer.create4096;},u822^u160x0301^vec16hello)letfail(t:t)(why:string):t=ift.state=Handshaking||t.state=Openthen{twithstate=Failedwhy}elset(* a ServerHello's extensions, as (type, data) *)letextensions(s:string)(pos:int):(int*string)list=letstop=pos+2+get16sposinletrecgoiacc=ifi+4>stopthenList.revaccelseletlen=get16s(i+2)ingo(i+4+len)((get16si,String.subs(i+4)len)::acc)ingo(pos+2)[]letserver_hello(t:t)(body:string):t=letsid_len=get8body34inletrandom=String.subbody232inletsuite=get16body(35+sid_len)inletexts=extensionsbody(35+sid_len+3)inifrandom=hello_retrythenfailt"HelloRetryRequest: the server wants another group (not supported)"elseifList.assoc_opt0x002bexts<>Some(u160x0304)thenfailt"not TLS 1.3"elsematch(suite,List.assoc_opt0x0033exts)with|(0x1303|0x1301),Somesharewhenget16share0=0x001d&&get16share2=32->letcipher=ifsuite=0x1303thenChacha20_poly1305elseAes128_gcminletshared=X25519.scalar_multt.secret(String.subshare432)inleths=handshake_secretsharedinletclient_secret=derive_secreths"c hs traffic"t.transcriptandserver_secret=derive_secreths"s hs traffic"t.transcriptin{twithcipher=Somecipher;hs;client_secret;server_secret;read_keys=Some(traffic_keyscipherserver_secret);write_keys=Some(traffic_keyscipherclient_secret);waiting=Encrypted_extensions;}|_->failt"a cipher suite or key share we did not offer"(* the chain of a Certificate message *)letchain_of(body:string):(X509.tlist,string)result=letctx=get8body0inletlist_start=1+ctx+3andstop=1+ctx+3+get24body(1+ctx)inletrecgoiacc=ifi>=stopthenOk(List.revacc)elseletlen=get24bodyiinletder=String.subbody(i+3)leninletext_len=get16body(i+3+len)inmatchX509.parsederwithOkc->go(i+3+len+2+ext_len)(c::acc)|Errore->Erroreingolist_start[]letcertificate_verify_content(transcript:string):string=String.make64' '^"TLS 1.3, server CertificateVerify"^"\000"^hashtranscript(* one handshake message, [msg] with its 4-byte header; the bytes to send *)lethandle(t:t)(typ:int)(msg:string):t*string=letbody=String.submsg4(String.lengthmsg-4)inlett_after={twithtranscript=t.transcript^msg}inmatch(t.waiting,typ)with|Server_hello,2->(server_hellot_afterbody,"")|Encrypted_extensions,8->({t_afterwithwaiting=Certificate},"")|Certificate,13->(* a CertificateRequest: we have none, and will say so with an
empty Certificate before our Finished (RFC 8446, 4.4.2) *)({t_afterwithcertificate_request=Some(String.subbody1(get8body0))},"")|Certificate,11->(matchchain_ofbodywith|Errore->(failte,"")|Ok[]->(failt"no certificate","")|Okchain->(matcht.verifychainwithOk()->({t_afterwithchain;waiting=Certificate_verify},"")|Errore->(failte,"")))|Certificate_verify,15->letscheme=get16body0andsignature=String.subbody4(get16body2)inifX509.verify_scheme(List.hdt.chain)~scheme~message:(certificate_verify_contentt.transcript)~signaturethen({t_afterwithwaiting=Server_finished},"")else(failt"the server's CertificateVerify does not check","")|Server_finished,20->ifbody<>finishedt.server_secrett.transcriptthen(failt"the server's Finished does not check","")else(* our Finished, then the application's keys (over the transcript
up to the server's Finished) *)letcipher=Option.gett.cipherin(* asked for a certificate: an empty one, in the transcript before our Finished *)letempty=matcht.certificate_requestwithSomectx->letb=vec8ctx^u240inu811^u24(String.lengthb)^b|None->""inletfin=u820^u24hash_len^finishedt.client_secret(t_after.transcript^empty)inletrecord,_=seal(Option.gett.write_keys)22(empty^fin)inletmaster=master_secrett.hsinletclient_secret=derive_secretmaster"c ap traffic"t_after.transcriptandserver_secret=derive_secretmaster"s ap traffic"t_after.transcriptin({t_afterwithclient_secret;server_secret;read_keys=Some(traffic_keyscipherserver_secret);write_keys=Some(traffic_keyscipherclient_secret);waiting=Done;state=Open;},(* a ChangeCipherSpec first, which middleboxes expect (RFC 8446, D.4) *)"\x14\x03\x03\x00\x01\x01"^record)|Done,4->(t,"")(* a NewSessionTicket: no resumption here *)|Done,24->(* KeyUpdate: the server's next keys; and ours, if it asks *)letcipher=Option.gett.cipherinletserver_secret=hkdf_expand_labelt.server_secret~label:"traffic upd"~context:""hash_leninlett={twithserver_secret;read_keys=Some(traffic_keyscipherserver_secret)}inifget8body0=1thenletrecord,_=seal(Option.gett.write_keys)22(u824^u241^u80)inletclient_secret=hkdf_expand_labelt.client_secret~label:"traffic upd"~context:""hash_lenin({twithclient_secret;write_keys=Some(traffic_keyscipherclient_secret)},record)else(t,"")|_->(failt(Printf.sprintf"an unexpected handshake message (%d)"typ),"")(* the whole handshake messages in [pending] *)letrecmessages(t:t)(out:string):t*string=ifString.lengtht.pending<4then(t,out)elseletlen=get24t.pending1inifString.lengtht.pending<4+lenthen(t,out)elseletmsg=String.subt.pending0(4+len)inlett={twithpending=String.subt.pending(4+len)(String.lengtht.pending-4-len)}inlett,more=handlet(get8msg0)msginmatcht.statewithFailed_->(t,out^more)|_->messagest(out^more)letalert(t:t)(data:string):t=ifString.lengthdata>=2&&get8data1=0then{twithstate=Closed}elsefailt(Printf.sprintf"the server's alert %d"(ifString.lengthdata>=2thenget8data1else-1))(* the whole records in the inbox *)letrecrecords(t:t)(out:string):t*string=ifString.lengtht.inbox<5then(t,out)elselettyp=get8t.inbox0andlen=get16t.inbox3inifString.lengtht.inbox<5+lenthen(t,out)elseletheader=String.subt.inbox05andbody=String.subt.inbox5leninlett={twithinbox=String.subt.inbox(5+len)(String.lengtht.inbox-5-len)}inlett,more=match(typ,t.read_keys)with|20,_->(t,"")(* ChangeCipherSpec: ignored, as 1.3 says *)|21,_->(alerttbody,"")|22,None->messages{twithpending=t.pending^body}""|23,Somek->(matchopen_recordkheaderbodywith|None->(failt"a record that does not decrypt","")|Some((22,data),k)->messages{twithread_keys=Somek;pending=t.pending^data}""|Some((23,data),k)->Buffer.add_stringt.appdata;({twithread_keys=Somek},"")|Some((21,data),k)->(alert{twithread_keys=Somek}data,"")|Some((other,_),_)->(failt(Printf.sprintf"a record of type %d"other),""))|_->(failt(Printf.sprintf"a record of type %d, unexpected"typ),"")inmatcht.statewithFailed_->(t,out^more)|_->recordst(out^more)letreceived(t:t)(bytes:string):t*string=records{twithinbox=t.inbox^bytes}""letstate(t:t):state=t.stateletread(t:t):t*string=lets=Buffer.contentst.appinBuffer.cleart.app;(t,s)letwrite(t:t)(data:string):t*string=match(t.state,t.write_keys)with|Open,Somek->(* records of at most 2^14 bytes *)letrecgokiout=ifi>=String.lengthdatathen({twithwrite_keys=Somek},String.concat""(List.revout))elseletchunk=String.subdatai(min16384(String.lengthdata-i))inletr,k=sealk23chunkingok(i+String.lengthchunk)(r::out)ingok0[]|_->(t,"")letclose(t:t):t*string=match(t.state,t.write_keys)with|Open,Somek->letr,k=sealk21"\001\000"in({twithwrite_keys=Somek;state=Closed},r)|_->(t,"")letcertificates(t:t):X509.tlist=t.chainletcipher(t:t):cipheroption=t.cipher