Skip to content

暗号化されたレコード ​

本ページでは、暗号化されたレコードの構造を確認し、暗号化される前の平文を表すTLSInnerPlaintextを、tlsInnerPlaintext構造体として実装します。

暗号化されたレコードの構造 ​

TLSのレコードには、暗号化されていないデータを入れるTLSPlaintextと、暗号化されたデータを入れるTLSCiphertextがあります。TLSCiphertextの構造は次のとおりです。

TLSCiphertextは、外側のContentTypeを常に23として、暗号文と認証タグをencrypted_recordに入れます。暗号化する前のデータは、TLSInnerPlaintextという形式で表します。

TLSCiphertext
├── opaque_type = 23, version, length       ← 5バイトのヘッダ。平文
└── encrypted_record                        ← 暗号化されている
    └── TLSInnerPlaintext
        ├── content                         ← 本当の中身
        ├── type                            ← 本当の種別(1バイト)
        └── zeros                           ← 詰め物(0バイト以上)

TLSCiphertextヘッダ(5バイト)opaque_type1バイトlegacy_record_version2バイトlength2バイトencrypted_record[length]暗号化された状態暗号化したTLSInnerPlaintext認証タグ16バイト復号すると復号した後content本当の中身type1バイトzeros0バイト以上TLSInnerPlaintext

TLSCiphertext ​

TLSCiphertextの構造はRFCで次のように定義されています。

c
struct {
    ContentType opaque_type = application_data; /* 23 */
    ProtocolVersion legacy_record_version = 0x0303; /* TLS v1.2 */
    uint16 length;
    opaque encrypted_record[TLSCiphertext.length];
} TLSCiphertext;

引用元: RFC 9846 §5.2 Record Payload Protection

TLSCiphertextは、TLSPlaintextとフィールドの名前や意味が異なりますが、ヘッダとペイロードの並びは同じです。

  • typeは中身に応じて20〜23でしたが、opaque_typeは常に23(application_data)です。暗号化後は中身がハンドシェイクでもアラートでも、外側は一律application_dataを名乗り、本当の種別を経路上の機器から隠します。
  • fragmentは平文でしたが、encrypted_recordは暗号文と認証タグです。

バイト列の構造が同じなので、第2章で実装したReadRecordとTLSPlaintextをそのまま使えます。TLSCiphertext型は作らず、暗号化したバイト列をContentTypeApplicationData付きのTLSPlaintextとして扱えば済みます。

TLSInnerPlaintext ​

TLSInnerPlaintextの構造はRFCで次のように定義されています。外側のopaque_typeは常に23なので、実際のデータの種別はTLSInnerPlaintextの中に入れます。

c
struct {
    opaque content[TLSPlaintext.length];
    ContentType type;
    uint8 zeros[length_of_padding];
} TLSInnerPlaintext;

引用元: RFC 9846 §5.2 Record Payload Protection

TLSInnerPlaintextは、平文の末尾に実際の種別を1バイト付け、必要に応じて詰め物を加えたものです。復号後に詰め物を取り除き、その直前の1バイトを読むことで、データの種別が分かります。

zerosは、レコードの長さから内容を推測されにくくするための詰め物です。送信者は、レコードの長さの上限内で詰め物の量を決められます。本書では付けませんが、受け取る側は剥がす必要があります。

実装 ​

TLSInnerPlaintextを組み立てる ​

record.goに構造体と、バイト列との相互変換を足します。送る側は中身の末尾に種別を1バイト付けるだけです。

go
type tlsInnerPlaintext struct {
	content     []byte
	contentType ContentType
}

func (p *tlsInnerPlaintext) marshal() []byte {
	buf := make([]byte, 0, len(p.content)+1)
	buf = append(buf, p.content...)
	return append(buf, byte(p.contentType))
}

受信側は、末尾から種別を取り出します。ただし、末尾にzerosが付いている場合があるため、後ろからゼロを読み飛ばし、最初に見つかった非ゼロバイトを種別として扱います。

go
var errInnerPlaintextNoType = errors.New("tls13: inner plaintext missing content type")

func parseTLSInnerPlaintext(data []byte) (*tlsInnerPlaintext, error) {
	i := len(data) - 1
	for i >= 0 && data[i] == 0 {
		i--
	}
	if i < 0 {
		return nil, errInnerPlaintextNoType
	}
	return &tlsInnerPlaintext{
		content:     data[:i],
		contentType: ContentType(data[i]),
	}, nil
}

すべてゼロだった場合は種別を特定できないため、errInnerPlaintextNoTypeを返します。

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

.
├── cmd/
│   └── main.go
├── go.mod
├── common.go
├── record.go
├── handshake.go
├── bytes.go
├── client_hello.go
├── extensions.go
├── server_hello.go
└── key_schedule.go
go
package tls13

import (
	"encoding/binary"
	"errors"
	"io"
)

var (
	errPayloadTooLarge      = errors.New("tls13: payload too large")
	errInnerPlaintextNoType = errors.New("tls13: inner plaintext missing content type")
)

type TLSPlaintext struct {
	Type    ContentType
	Version TLSVersion
	Length  uint16
	Payload []byte
}

func NewTLSPlaintext(contentType ContentType, fragment []byte) *TLSPlaintext {
	return &TLSPlaintext{
		Type:    contentType,
		Version: TLS_VERSION_1_2,
		Length:  uint16(len(fragment)),
		Payload: fragment,
	}
}

func (p *TLSPlaintext) Marshal() []byte {
	buf := make([]byte, 5, 5+len(p.Payload))
	buf[0] = byte(p.Type)
	binary.BigEndian.PutUint16(buf[1:], uint16(p.Version))
	binary.BigEndian.PutUint16(buf[3:], p.Length)
	return append(buf, p.Payload...)
}

func ReadRecord(r io.Reader) (*TLSPlaintext, error) {
	hdr := make([]byte, 5)
	if _, err := io.ReadFull(r, hdr); err != nil {
		return nil, err
	}
	length := binary.BigEndian.Uint16(hdr[3:])
	if int(length) > maxPayloadLen {
		return nil, errPayloadTooLarge
	}
	payload := make([]byte, length)
	if _, err := io.ReadFull(r, payload); err != nil {
		return nil, err
	}
	return &TLSPlaintext{
		Type:    ContentType(hdr[0]),
		Version: TLSVersion(binary.BigEndian.Uint16(hdr[1:])),
		Length:  length,
		Payload: payload,
	}, nil
}

type tlsInnerPlaintext struct {
	content     []byte
	contentType ContentType
}

func (p *tlsInnerPlaintext) marshal() []byte {
	buf := make([]byte, 0, len(p.content)+1)
	buf = append(buf, p.content...)
	return append(buf, byte(p.contentType))
}

func parseTLSInnerPlaintext(data []byte) (*tlsInnerPlaintext, error) {
	i := len(data) - 1
	for i >= 0 && data[i] == 0 {
		i--
	}
	if i < 0 {
		return nil, errInnerPlaintextNoType
	}
	return &tlsInnerPlaintext{
		content:     data[:i],
		contentType: ContentType(data[i]),
	}, nil
}