Skip to content

アプリケーションデータを送受信する ​

ハンドシェイクの後に流れるもの ​

ハンドシェイクの完了後は、アプリケーション鍵で保護されたレコードを送受信します。暗号化されたレコードの外側の種別はapplication_dataですが、復号するとTLSInnerPlaintextから本来の種別を取り出せます。本書のクライアントが受け取るメッセージは、主に表1の3種類です。

内側の種別中身本書での扱い
application_dataHTTPなど上位層のデータ呼び出し元に返す
alertエラーや切断の通知エラーとして返す
handshakeNewSessionTicketなどハンドシェイク後のメッセージ読み捨てる

ハンドシェイク後に届くレコードの中身

NewSessionTicketは、サーバーが後の接続でセッションを再開できるように送るメッセージです(RFC 9846 §4.7.1 New Session Ticket Message)。本書ではセッション再開を実装しないため、届いても読み捨てます。

net.Connと同じように扱えるようにする ​

Goでは、TCPの接続をnetパッケージのnet.Connインターフェースで扱えます。net.Connを使うプログラムは、Writeでデータを送り、Readで受け取り、Closeで接続を閉じます。

go
type Conn interface {
	Read(b []byte) (n int, err error)
	Write(b []byte) (n int, err error)
	Close() error
	// ...
}

標準ライブラリcrypto/tlsのtls.Connも、net.Connと同じメソッドを持っています。暗号化と復号はtls.Connの内部で行われるため、利用者はTCP接続と同じようにWriteとReadを呼ぶことでTLS通信を行えます。

本書のConnにも、Write、Read、Closeを実装します。また、tls13.DialでTLS接続を確立できるようにし、呼び出し元が暗号化や復号を意識せずに通信できる形にします。

実装 ​

平文の長さの上限を表す定数を足す ​

common.goに、1つのレコードに入れられる平文の最大長を表すmaxPlaintextLenを追加します。

go
const maxPlaintextLen = 1 << 14

TLSでは、1つのレコードに入れられる平文は214バイト、つまり16,384バイトまでと決まっています。

length: The length (in bytes) of the following TLSPlaintext.fragment. The length MUST NOT exceed 2^14 bytes.

訳)length:後に続くTLSPlaintext.fragmentの長さ(バイト単位)。長さは214バイトを超えてはならない。

引用元: RFC 9846 §5.1 Record Layer

暗号化するレコードでも、元の平文データの上限は214バイトです。暗号化前のTLSInnerPlaintextには種別の1バイトも加わるため、RFC 9846 §5.4 Record Paddingでは、詰め物を含めたTLSInnerPlaintext全体の長さを214 + 1バイトまでとしています。

Write ​

Writeメソッドは、渡されたバイト列をアプリケーションデータとして送ります。

go
func (c *Conn) Write(p []byte) (n int, err error) {
	for n < len(p) {
		end := min(n+maxPlaintextLen, len(p))
		if err := c.writeRecord(NewTLSPlaintext(ContentTypeApplicationData, p[n:end])); err != nil {
			return n, err
		}
		n = end
	}
	return n, nil
}

Writeメソッドは、渡されたバイト列をmaxPlaintextLen以下に分割し、writeRecordでレコードごとに送信します。writeRecordは、outフィールドのアプリケーション鍵で暗号化し、外側の種別をapplication_dataにして書き込みます。

Writeメソッドが返すnは、正常に書き込めたバイト数です。途中で書き込みに失敗した場合は、それまでに書き込んだ分のバイト数とエラーを返します。

Connに読み残しのバッファを足す ​

conn.goのConnに、appDataBufフィールドを足します。

go
type Conn struct {
	conn         net.Conn
	in, out      *recordProtection
	handshakeBuf []byte
	appDataBuf   []byte
}

appDataBufフィールドは、復号したアプリケーションデータのうち、まだ呼び出し元に渡していないバイト列を溜めるバッファです。

Read ​

Readメソッドは、受け取ったアプリケーションデータを呼び出し元のバッファpに書き込みます。

go
func (c *Conn) Read(p []byte) (n int, err error) {
	for len(c.appDataBuf) == 0 {
		rec, err := c.readAndDecryptRecord()
		if err != nil {
			return 0, err
		}
		switch rec.Type {
		case ContentTypeApplicationData:
			c.appDataBuf = rec.Payload
		case ContentTypeHandshake:
		default:
			return 0, fmt.Errorf("tls13: unexpected record type %d", rec.Type)
		}
	}
	n = copy(p, c.appDataBuf)
	c.appDataBuf = c.appDataBuf[n:]
	return n, nil
}

レコードの受信と復号は、実装済みのreadAndDecryptRecordが行います。このメソッドはchange_cipher_specを読み飛ばし、inフィールドのアプリケーション鍵で復号したあと、内側の種別とデータを持つTLSPlaintextを返します。鍵が設定されている状態で平文のレコードを受信した場合は、エラーを返します。

Readメソッドのswitchでは、application_dataを受け取った場合は中身をappDataBufに保存し、handshakeなら読み捨てて次のレコードへ進みます。この段階ではNewSessionTicketなどのハンドシェイク後のメッセージは処理しません。alertを含むその他の種別はエラーにします。

io.ReaderのReadは、レコード全体を一度に返す必要はなく、呼び出し元のバッファpに入るだけのデータを返せます。そこで、appDataBufからpに収まる分をコピーし、残りは次の呼び出しまで保持します。appDataBufに読み残しがある間は、新しいレコードを読みません。こうして、レコードの境界とReadが返すデータの境界の違いを吸収します。

Close ​

Closeメソッドは、TCPの接続を閉じます。

go
func (c *Conn) Close() error {
	return c.conn.Close()
}

接続の設定を表す構造体を実装する ​

client.goを作り、接続の設定を表すConfig構造体を定義します。

go
type Config struct {
	ServerName string
	RootCAs    *x509.CertPool
}

ServerNameにはサーバー名を指定し、SNIの送信と証明書のホスト名検証に使います。RootCAsには信頼するルート証明書を指定します。RootCAsがnilの場合は、OSのルート証明書ストアを使います。

状態機械をConnにつなぎ替える ​

conn.goのConnに、接続の設定を持つconfigフィールドを足します。

go
type Conn struct {
	conn         net.Conn
	config       *Config
	in, out      *recordProtection
	handshakeBuf []byte
	appDataBuf   []byte
}

ClientHandshakeStateが持っていたserverNameとrootsは、Connのconfigフィールドに移します。NewClientHandshakeStateとHandshakeは、Connのhandshakeメソッドに置き換えます。

go
type ClientHandshakeState struct {
	c          *Conn
	serverName string
	roots      *x509.CertPool

	transcript *transcript

	// ...
}

func NewClientHandshakeState(conn net.Conn, serverName string, roots *x509.CertPool) *ClientHandshakeState {
	return &ClientHandshakeState{
		c:          &Conn{conn: conn},
		serverName: serverName,
		roots:      roots,
		transcript: newTranscript(),
	}
}

func (hs *ClientHandshakeState) Handshake() error {
func (c *Conn) handshake() error {
	hs := &ClientHandshakeState{
		c:          c,
		transcript: newTranscript(),
	}
	if err := hs.GenerateKeyPair(); err != nil {
		return err
	}
	// ...
}

handshakeメソッドは、冒頭で状態機械を組み立ててから、Handshakeメソッドと同じ順で各手順を呼びます。

serverNameとrootsを使っていたSendClientHelloとVerifyCertificateは、Connのconfigフィールドから値を読むように変えます。

go
func (hs *ClientHandshakeState) SendClientHello() error {
	// ...
	ch := NewClientHello(random, []Extension{
		SupportedVersionsExtension(),
		ServerNameExtension(hs.serverName),
		ServerNameExtension(hs.c.config.ServerName),
		SignatureAlgorithmsExtension(),
		SupportedGroupsExtension(),
		KeyShareExtension(hs.privateKey.PublicKey().Bytes()),
	})
	// ...
}

// ...

func (hs *ClientHandshakeState) VerifyCertificate() error {
	certs := hs.PeerCertificates
	opts := x509.VerifyOptions{
		DNSName:       hs.serverName,
		Roots:         hs.roots,
		DNSName:       hs.c.config.ServerName,
		Roots:         hs.c.config.RootCAs,
		Intermediates: x509.NewCertPool(),
		KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
	}
	// ...
}

Dial ​

client.goに、TLS接続を確立する公開関数Dialを追加します。

go
func Dial(network, addr string, config *Config) (*Conn, error) {
	raw, err := net.Dial(network, addr)
	if err != nil {
		return nil, err
	}
	c := &Conn{conn: raw, config: config}
	if err := c.handshake(); err != nil {
		raw.Close()
		return nil, err
	}
	return c, nil
}

DialはTCP接続を確立したあとにハンドシェイクを実行し、成功した場合はConnを返します。途中で失敗した場合はTCP接続を閉じ、エラーを返します。

動かす ​

Dialで接続し、fmt.FprintfでHTTPリクエストを書き込み、io.Copyでレスポンスを標準出力にコピーするコマンドを作ります。

go
package main

import (
	"fmt"
	"io"
	"net/url"
	"os"

	"tls13"
)

func main() {
	if len(os.Args) != 2 {
		fmt.Fprintln(os.Stderr, "usage: tlsclient <https-url>")
		os.Exit(1)
	}
	u, err := url.Parse(os.Args[1])
	if err != nil || u.Scheme != "https" {
		fmt.Fprintln(os.Stderr, "error: https URL required")
		os.Exit(1)
	}
	port := u.Port()
	if port == "" {
		port = "443"
	}

	conn, err := tls13.Dial("tcp", u.Hostname()+":"+port, &tls13.Config{ServerName: u.Hostname()})
	if err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
	defer conn.Close()

	if _, err := fmt.Fprintf(conn, "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", u.RequestURI(), u.Hostname()); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
	if _, err := io.Copy(os.Stdout, conn); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
}
sh
$ go run ./cmd https://example.com
HTTP/1.1 200 OK
Content-Type: text/html
...
<!doctype html><html lang="en"><head><title>Example Domain</title>...
error: tls13: unexpected record type 21
exit status 1

サーバーからのHTTPレスポンスが表示され、アプリケーションデータを送受信できたことが分かります。出力の最後にあるunexpected record type 21は、Readメソッドが種別21のalertを受信し、エラーとして返したことを示しています。HTTPリクエストにConnection: closeを付けているため、サーバーはレスポンスの送信後に接続を閉じます。この例では、その際に送られたalertをReadが処理できず、io.Copyがエラーを返してプログラムが終了します。

本ページで書いたコード ​

.
├── cmd/
│   └── main.go
├── go.mod
├── aead.go
├── bytes.go
├── certificate.go
├── client.go
├── client_hello.go
├── common.go
├── conn.go
├── extensions.go
├── handshake.go
├── handshake_client.go
├── key_schedule.go
├── record.go
├── record_protection.go
└── server_hello.go
go
package tls13

import (
	"fmt"
	"net"
)

type Conn struct {
	conn         net.Conn
	config       *Config
	in, out      *recordProtection
	handshakeBuf []byte
	appDataBuf   []byte
}

func (c *Conn) Write(p []byte) (n int, err error) {
	for n < len(p) {
		end := min(n+maxPlaintextLen, len(p))
		if err := c.writeRecord(NewTLSPlaintext(ContentTypeApplicationData, p[n:end])); err != nil {
			return n, err
		}
		n = end
	}
	return n, nil
}

func (c *Conn) Close() error {
	return c.conn.Close()
}

func (c *Conn) Read(p []byte) (n int, err error) {
	for len(c.appDataBuf) == 0 {
		rec, err := c.readAndDecryptRecord()
		if err != nil {
			return 0, err
		}
		switch rec.Type {
		case ContentTypeApplicationData:
			c.appDataBuf = rec.Payload
		case ContentTypeHandshake:
		default:
			return 0, fmt.Errorf("tls13: unexpected record type %d", rec.Type)
		}
	}
	n = copy(p, c.appDataBuf)
	c.appDataBuf = c.appDataBuf[n:]
	return n, nil
}

func (c *Conn) readAndDecryptRecord() (rec *TLSPlaintext, err error) {
	for {
		rec, err = ReadRecord(c.conn)
		if err != nil {
			return nil, err
		}
		if rec.Type != ContentTypeChangeCipherSpec {
			break
		}
	}
	if c.in == nil {
		return rec, nil
	}
	if rec.Type != ContentTypeApplicationData {
		return nil, fmt.Errorf("tls13: unexpected plaintext record type %d", rec.Type)
	}
	inner, err := c.in.decrypt(rec)
	if err != nil {
		return nil, err
	}
	return NewTLSPlaintext(inner.contentType, inner.content), nil
}

func (c *Conn) readHandshake() (msg *Handshake, raw []byte, err error) {
	for !hasCompleteHandshake(c.handshakeBuf) {
		rec, err := c.readAndDecryptRecord()
		if err != nil {
			return nil, nil, err
		}
		switch rec.Type {
		case ContentTypeHandshake:
			c.handshakeBuf = append(c.handshakeBuf, rec.Payload...)
		case ContentTypeAlert:
			return nil, nil, fmt.Errorf("tls13: received alert record: % x", rec.Payload)
		default:
			return nil, nil, fmt.Errorf("tls13: unexpected record type %d during handshake", rec.Type)
		}
	}
	n := 4 + handshakeBodyLen(c.handshakeBuf)
	raw = c.handshakeBuf[:n]
	c.handshakeBuf = c.handshakeBuf[n:]
	msg, err = UnmarshalHandshake(raw)
	if err != nil {
		return nil, nil, err
	}
	return msg, raw, nil
}

func hasCompleteHandshake(buf []byte) (ok bool) {
	return len(buf) >= 4 && len(buf) >= 4+handshakeBodyLen(buf)
}

func handshakeBodyLen(buf []byte) (bodyLen int) {
	return int(buf[1])<<16 | int(buf[2])<<8 | int(buf[3])
}

func (c *Conn) writeRecord(rec *TLSPlaintext) (err error) {
	if c.out != nil {
		rec = NewTLSPlaintext(ContentTypeApplicationData, c.out.encrypt(rec.Type, rec.Payload))
	}
	_, err = c.conn.Write(rec.Marshal())
	return err
}
go
package tls13

import (
	"crypto/x509"
	"net"
)

type Config struct {
	ServerName string
	RootCAs    *x509.CertPool
}

func Dial(network, addr string, config *Config) (*Conn, error) {
	raw, err := net.Dial(network, addr)
	if err != nil {
		return nil, err
	}
	c := &Conn{conn: raw, config: config}
	if err := c.handshake(); err != nil {
		raw.Close()
		return nil, err
	}
	return c, nil
}
go
package tls13

import (
	"crypto/ecdh"
	"crypto/hmac"
	"crypto/rand"
	"crypto/x509"
	"errors"
	"fmt"
)

type ClientHandshakeState struct {
	c *Conn

	transcript *transcript

	privateKey      *ecdh.PrivateKey
	serverPublicKey []byte
	sharedSecret    []byte

	clientHandshakeTrafficSecret []byte
	serverHandshakeTrafficSecret []byte
	handshakeSecret              []byte

	PeerCertificates                []*x509.Certificate
	VerifiedChains                  [][]*x509.Certificate
	PeerCertificateVerify           *CertificateVerify
	certificateVerifyTranscriptHash []byte
}

func (c *Conn) handshake() error {
	hs := &ClientHandshakeState{
		c:          c,
		transcript: newTranscript(),
	}
	if err := hs.GenerateKeyPair(); err != nil {
		return err
	}
	if err := hs.SendClientHello(); err != nil {
		return err
	}
	if err := hs.ReadServerHello(); err != nil {
		return err
	}
	if err := hs.ComputeSharedSecret(); err != nil {
		return err
	}
	hs.EstablishHandshakeKeys()
	if err := hs.ReadEncryptedExtensions(); err != nil {
		return err
	}
	if err := hs.ReadCertificate(); err != nil {
		return err
	}
	if err := hs.VerifyCertificate(); err != nil {
		return err
	}
	if err := hs.ReadCertificateVerify(); err != nil {
		return err
	}
	if err := hs.VerifyCertificateVerify(); err != nil {
		return err
	}
	if err := hs.ReadServerFinished(); err != nil {
		return err
	}
	if err := hs.WriteClientFinished(); err != nil {
		return err
	}
	hs.SwitchToApplicationKeys()
	return nil
}

func (hs *ClientHandshakeState) GenerateKeyPair() error {
	privateKey, err := ecdh.X25519().GenerateKey(rand.Reader)
	if err != nil {
		return err
	}
	hs.privateKey = privateKey
	return nil
}

func (hs *ClientHandshakeState) SendClientHello() error {
	var random [clientRandomLength]byte
	if _, err := rand.Read(random[:]); err != nil {
		return err
	}

	ch := NewClientHello(random, []Extension{
		SupportedVersionsExtension(),
		ServerNameExtension(hs.c.config.ServerName),
		SignatureAlgorithmsExtension(),
		SupportedGroupsExtension(),
		KeyShareExtension(hs.privateKey.PublicKey().Bytes()),
	})
	h := NewHandshake(TypeClientHello, ch.Marshal())
	raw := h.Marshal()
	if err := hs.c.writeRecord(NewTLSPlaintext(ContentTypeHandshake, raw)); err != nil {
		return err
	}
	hs.transcript.write(raw)
	return nil
}

func (hs *ClientHandshakeState) ReadServerHello() error {
	msg, raw, err := hs.c.readHandshake()
	if err != nil {
		return err
	}
	if msg.HandshakeType != TypeServerHello {
		return fmt.Errorf("tls13: expected handshake message %d, got %d", TypeServerHello, msg.HandshakeType)
	}
	sh, err := UnmarshalServerHello(msg.Body)
	if err != nil {
		return err
	}
	if sh.CipherSuite != TLS_AES_128_GCM_SHA256 {
		return fmt.Errorf("tls13: server selected unsupported cipher suite %#x", uint16(sh.CipherSuite))
	}
	if v, ok := sh.SelectedVersion(); !ok || v != TLS_VERSION_1_3 {
		return errors.New("tls13: server did not negotiate TLS 1.3")
	}
	group, serverPublicKey, ok := sh.KeyShare()
	if !ok {
		return errors.New("tls13: server did not send a key share")
	}
	if group != X25519 {
		return fmt.Errorf("tls13: server selected unsupported group %#x", uint16(group))
	}
	hs.serverPublicKey = serverPublicKey
	hs.transcript.write(raw)
	return nil
}

func (hs *ClientHandshakeState) ComputeSharedSecret() error {
	serverPub, err := ecdh.X25519().NewPublicKey(hs.serverPublicKey)
	if err != nil {
		return err
	}
	hs.sharedSecret, err = hs.privateKey.ECDH(serverPub)
	return err
}

func (hs *ClientHandshakeState) EstablishHandshakeKeys() {
	hs.handshakeSecret = deriveHandshakeSecret(hs.sharedSecret)
	transcriptHash := hs.transcript.sum()
	hs.clientHandshakeTrafficSecret = deriveSecret(hs.handshakeSecret, "c hs traffic", transcriptHash)
	hs.serverHandshakeTrafficSecret = deriveSecret(hs.handshakeSecret, "s hs traffic", transcriptHash)
	hs.c.in = newRecordProtection(hs.serverHandshakeTrafficSecret)
}

func (hs *ClientHandshakeState) ReadEncryptedExtensions() error {
	msg, raw, err := hs.c.readHandshake()
	if err != nil {
		return err
	}
	if msg.HandshakeType != TypeEncryptedExtensions {
		return fmt.Errorf("tls13: expected handshake message %d, got %d", TypeEncryptedExtensions, msg.HandshakeType)
	}
	hs.transcript.write(raw)
	return nil
}

func (hs *ClientHandshakeState) ReadCertificate() error {
	msg, raw, err := hs.c.readHandshake()
	if err != nil {
		return err
	}
	if msg.HandshakeType != TypeCertificate {
		return fmt.Errorf("tls13: expected handshake message %d, got %d", TypeCertificate, msg.HandshakeType)
	}
	certificate, err := unmarshalCertificate(msg.Body)
	if err != nil {
		return err
	}
	if len(certificate.certificateList) == 0 {
		return errors.New("tls13: server sent no certificates")
	}
	certs := make([]*x509.Certificate, len(certificate.certificateList))
	for i, der := range certificate.certificateList {
		if certs[i], err = x509.ParseCertificate(der); err != nil {
			return err
		}
	}
	hs.PeerCertificates = certs
	hs.transcript.write(raw)
	return nil
}

func (hs *ClientHandshakeState) VerifyCertificate() error {
	certs := hs.PeerCertificates
	opts := x509.VerifyOptions{
		DNSName:       hs.c.config.ServerName,
		Roots:         hs.c.config.RootCAs,
		Intermediates: x509.NewCertPool(),
		KeyUsages:     []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
	}
	for _, cert := range certs[1:] {
		opts.Intermediates.AddCert(cert)
	}
	chains, err := certs[0].Verify(opts)
	if err != nil {
		return fmt.Errorf("tls13: certificate verification failed: %w", err)
	}
	hs.VerifiedChains = chains
	return nil
}

func (hs *ClientHandshakeState) ReadCertificateVerify() error {
	msg, raw, err := hs.c.readHandshake()
	if err != nil {
		return err
	}
	if msg.HandshakeType != TypeCertificateVerify {
		return fmt.Errorf("tls13: expected handshake message %d, got %d", TypeCertificateVerify, msg.HandshakeType)
	}
	cv, err := UnmarshalCertificateVerify(msg.Body)
	if err != nil {
		return err
	}
	hs.PeerCertificateVerify = cv
	hs.certificateVerifyTranscriptHash = hs.transcript.sum()
	hs.transcript.write(raw)
	return nil
}

func (hs *ClientHandshakeState) VerifyCertificateVerify() error {
	return hs.PeerCertificateVerify.Verify(hs.PeerCertificates[0], hs.certificateVerifyTranscriptHash)
}

func (hs *ClientHandshakeState) ReadServerFinished() error {
	msg, raw, err := hs.c.readHandshake()
	if err != nil {
		return err
	}
	if msg.HandshakeType != TypeFinished {
		return fmt.Errorf("tls13: expected handshake message %d, got %d", TypeFinished, msg.HandshakeType)
	}
	want := finishedVerifyData(hs.serverHandshakeTrafficSecret, hs.transcript.sum())
	if !hmac.Equal(msg.Body, want) {
		return errors.New("tls13: invalid server Finished")
	}
	hs.transcript.write(raw)
	return nil
}

func (hs *ClientHandshakeState) WriteClientFinished() error {
	msg := NewHandshake(TypeFinished, finishedVerifyData(hs.clientHandshakeTrafficSecret, hs.transcript.sum()))
	hs.c.out = newRecordProtection(hs.clientHandshakeTrafficSecret)
	return hs.c.writeRecord(NewTLSPlaintext(ContentTypeHandshake, msg.Marshal()))
}

func (hs *ClientHandshakeState) SwitchToApplicationKeys() {
	mainSecret := deriveMainSecret(hs.handshakeSecret)
	transcriptHash := hs.transcript.sum()
	hs.c.in = newRecordProtection(deriveSecret(mainSecret, "s ap traffic", transcriptHash))
	hs.c.out = newRecordProtection(deriveSecret(mainSecret, "c ap traffic", transcriptHash))
}
go
package tls13

// @see https://datatracker.ietf.org/doc/html/rfc9846#section-5.1
type ContentType uint8

const (
	ContentTypeChangeCipherSpec ContentType = 20
	ContentTypeAlert            ContentType = 21
	ContentTypeHandshake        ContentType = 22
	ContentTypeApplicationData  ContentType = 23
)

// @see https://datatracker.ietf.org/doc/html/rfc9846#section-4.2.2
type TLSVersion uint16

const (
	TLS_VERSION_1_2 TLSVersion = 0x0303
	TLS_VERSION_1_3 TLSVersion = 0x0304
)

// @see https://datatracker.ietf.org/doc/html/rfc9846#section-4
type HandshakeType uint8

const (
	TypeClientHello         HandshakeType = 1
	TypeServerHello         HandshakeType = 2
	TypeEncryptedExtensions HandshakeType = 8
	TypeCertificate         HandshakeType = 11
	TypeCertificateVerify   HandshakeType = 15
	TypeFinished            HandshakeType = 20
)

// @see https://datatracker.ietf.org/doc/html/rfc9846#appendix-B.4
type CipherSuite uint16

const TLS_AES_128_GCM_SHA256 CipherSuite = 0x1301

// @see https://datatracker.ietf.org/doc/html/rfc9846#section-4.3
type extensionType uint16

const (
	extServerName          extensionType = 0
	extSupportedGroups     extensionType = 10
	extSignatureAlgorithms extensionType = 13
	extSupportedVersions   extensionType = 43
	extKeyShare            extensionType = 51
)

// @see https://datatracker.ietf.org/doc/html/rfc9846#section-4.3.7
type NamedGroup uint16

const X25519 NamedGroup = 0x001d

// @see https://datatracker.ietf.org/doc/html/rfc9846#section-4.3.3
type SignatureScheme uint16

const (
	RSA_PKCS1_SHA256       SignatureScheme = 0x0401
	ECDSA_SECP256R1_SHA256 SignatureScheme = 0x0403
	ECDSA_SECP384R1_SHA384 SignatureScheme = 0x0503
	RSA_PSS_RSAE_SHA256    SignatureScheme = 0x0804
	RSA_PSS_RSAE_SHA384    SignatureScheme = 0x0805
	RSA_PSS_RSAE_SHA512    SignatureScheme = 0x0806
)

const maxPlaintextLen = 1 << 14

const maxPayloadLen = 1<<14 + 256
go
package main

import (
	"fmt"
	"io"
	"net/url"
	"os"

	"tls13"
)

func main() {
	if len(os.Args) != 2 {
		fmt.Fprintln(os.Stderr, "usage: tlsclient <https-url>")
		os.Exit(1)
	}
	u, err := url.Parse(os.Args[1])
	if err != nil || u.Scheme != "https" {
		fmt.Fprintln(os.Stderr, "error: https URL required")
		os.Exit(1)
	}
	port := u.Port()
	if port == "" {
		port = "443"
	}

	conn, err := tls13.Dial("tcp", u.Hostname()+":"+port, &tls13.Config{ServerName: u.Hostname()})
	if err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
	defer conn.Close()

	if _, err := fmt.Fprintf(conn, "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", u.RequestURI(), u.Hostname()); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
	if _, err := io.Copy(os.Stdout, conn); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
}