Alertを実装する
現在のクライアントは、サーバーから届く正常な終了通知もエラーとして扱います。本ページでは、接続の正常終了とTLSのエラーを区別できるようにAlertを実装します。ReadメソッドでAlertを受け取り、Closeメソッドで終了を通知します。
Alertプロトコル
Alertは、TLSがエラーや接続の終了を相手に伝えるためのメッセージです。
rustenum { warning(1), fatal(2), (255) } AlertLevel; enum { close_notify(0), unexpected_message(10), bad_record_mac(20), /* (中略) */ decrypt_error(51), /* (中略) */ (255) } AlertDescription; struct { AlertLevel level; AlertDescription description; } Alert;
Alertは、レベルと種別をそれぞれ1バイトで表す、合計2バイトのメッセージです。levelはTLS 1.2以前との互換性のために残されており、TLS 1.3ではアラートの扱いをdescriptionで判断します。種別は複数ありますが、ここでは正常終了を伝えるclose_notifyと、それ以外のエラー通知に分けて扱います。
close_notifyが必要なのは、TCPの接続が切れただけでは、相手が正しく送信を終えたのか判断できないためです。
The client and the server must share knowledge that the connection is ending in order to avoid a truncation attack.
訳)トランケーション攻撃を避けるため、クライアントとサーバーは接続が終了することを互いに知っていなければならない。
通信経路上の攻撃者がTCP接続を途中で切断すると、受信側は残りのデータが存在したことに気付かず、通信が正常に終わったと誤解する可能性があります。このように、データを途中で打ち切る攻撃をトランケーション攻撃と呼びます。close_notifyはTLSで保護されるため、攻撃者が正しい終了通知を偽造することはできません。TLSでは、通常の終了時に送信側がclose_notifyを送り、受信側が正常終了を確認できるようにしています。
実装
Alertを読み書きする処理を実装する
alert.goにAlertを表すalert構造体と読み書きする関数を実装し、common.goに種別の定数を追加します。本書で定数を定義する種別はclose_notifyだけです。
type alert struct {
level uint8
description alertDescription
}
func (a *alert) Error() string {
return fmt.Sprintf("tls13: received alert: level %d, description %d", a.level, a.description)
}
func (a *alert) marshal() []byte {
return []byte{a.level, byte(a.description)}
}
func unmarshalAlert(payload []byte) (*alert, error) {
if len(payload) != 2 {
return nil, errors.New("tls13: malformed alert")
}
return &alert{level: payload[0], description: alertDescription(payload[1])}, nil
}// @see https://datatracker.ietf.org/doc/html/rfc9846#section-6
type alertDescription uint8
const closeNotify alertDescription = 0
const alertLevelWarning = 1alert構造体にErrorメソッドを実装してerrorインターフェースを満たすようにします。これにより、受信したアラートを呼び出し元へエラーとして返せます。
Readでアラートを受け取る処理を実装する
Readメソッドのswitchにalertの処理を追加します。受け取ったアラートはalertErrorメソッドでio.EOFかエラーに変換します。
func (c *Conn) Read(p []byte) (n int, err error) {
for len(c.appDataBuf) == 0 {
// ...
switch rec.Type {
case ContentTypeApplicationData:
c.appDataBuf = rec.Payload
case ContentTypeHandshake:
case ContentTypeAlert:
return 0, c.alertError(rec.Payload)
default:
return 0, fmt.Errorf("tls13: unexpected record type %d", rec.Type)
}
}
// ...
}
func (c *Conn) alertError(payload []byte) error {
alert, err := unmarshalAlert(payload)
if err != nil {
return err
}
if alert.description == closeNotify {
return io.EOF
}
return alert
}alertErrorは、close_notifyをio.EOFに変換します。アプリケーションデータの送受信中に届くclose_notifyは、サーバーがこれ以上データを送らないことを示します。io.Readerではデータの終わりをio.EOFで表すため、Readメソッドも正常終了として扱います。それ以外のアラートは*alertをエラーとして返します。
Closeでclose_notifyを送る処理を実装する
Closeメソッドを変更し、close_notifyを送ってからTCP接続を閉じるようにします。
func (c *Conn) Close() error {
if c.out != nil {
alert := &alert{level: alertLevelWarning, description: closeNotify}
c.writeRecord(NewTLSPlaintext(ContentTypeAlert, alert.marshal()))
}
return c.conn.Close()
}writeRecordは、outフィールドに設定された鍵でアラートを暗号化して送ります。outがnilの場合は送信用の鍵が設定されていないため、本書のCloseメソッドはアラートを送らずにTCP接続を閉じます。AES-128-GCMを使い、詰め物を付けない場合、暗号化したclose_notifyのレコードは、5バイトのヘッダ、2バイトのアラート、1バイトの種別、16バイトの認証タグを合わせた24バイトです。
動かす
cmd/main.goは変えずに実行します。
$ 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>...HTTPリクエストにはConnection: closeを付けているため、サーバーはレスポンスを送り終えると接続を閉じます。サーバーからclose_notifyが届いた場合、Readメソッドはio.EOFを返し、io.Copyはエラーを返さずにコピーを終えます。最後にdeferで登録したCloseメソッドがクライアントのclose_notifyを送ってTCP接続を閉じます。
実装の流れで目標にしたtls13.DialとWrite、Read、Closeが揃いました。自作のTLS 1.3クライアントを使ってWebサーバーからHTMLを取得できるようになりました。
本ページで書いたコード
.
├── cmd/
│ └── main.go
├── go.mod
├── aead.go
├── alert.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.gopackage tls13
import (
"fmt"
"io"
"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 {
if c.out != nil {
alert := &alert{level: alertLevelWarning, description: closeNotify}
c.writeRecord(NewTLSPlaintext(ContentTypeAlert, alert.marshal()))
}
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:
case ContentTypeAlert:
return 0, c.alertError(rec.Payload)
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) alertError(payload []byte) error {
alert, err := unmarshalAlert(payload)
if err != nil {
return err
}
if alert.description == closeNotify {
return io.EOF
}
return alert
}
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
}package tls13
import (
"errors"
"fmt"
)
type alert struct {
level uint8
description alertDescription
}
func (a *alert) Error() string {
return fmt.Sprintf("tls13: received alert: level %d, description %d", a.level, a.description)
}
func (a *alert) marshal() []byte {
return []byte{a.level, byte(a.description)}
}
func unmarshalAlert(payload []byte) (*alert, error) {
if len(payload) != 2 {
return nil, errors.New("tls13: malformed alert")
}
return &alert{level: payload[0], description: alertDescription(payload[1])}, nil
}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
)
// @see https://datatracker.ietf.org/doc/html/rfc9846#section-6
type alertDescription uint8
const closeNotify alertDescription = 0
const alertLevelWarning = 1
const maxPlaintextLen = 1 << 14
const maxPayloadLen = 1<<14 + 256