Skip to content

Key Scheduleを実装する ​

本ページでは、HKDF-Expand-LabelとKey Scheduleをkey_schedule.goに実装します。

HKDF-Expand-Labelを実装する ​

RFC 9846は、HKDF-Expand-Labelを次のように定義しています。

c
HKDF-Expand-Label(Secret, Label, Context, Length) =
    HKDF-Expand(Secret, HkdfLabel, Length)

Where HkdfLabel is specified as:

struct {
    uint16 length = Length;
    opaque label<7..255> = "tls13 " + Label;
    opaque context<0..255> = Context;
} HkdfLabel;

引用元: RFC 9846 §7.1 Key Schedule

HKDF-Expand-Labelは、HkdfLabelをバイト列にしてinfoとしてHKDF-Expandに渡す関数です。hkdfLabel構造体と、hkdfExpandLabel関数の順に実装します。

HkdfLabelを実装する ​

RFCのHkdfLabelを、そのままGoのhkdfLabel構造体にします。TLS 1.3のラベルにはすべて"tls13 "が前に付くので、コンストラクタのnewHkdfLabelで付けます。labelとcontextは最大255バイトの可変長のフィールドなので、marshalメソッドではwithUint8Lengthで先頭に1バイトの長さを付けます。

go
package tls13

type hkdfLabel struct {
	length  uint16
	label   string
	context []byte
}

func newHkdfLabel(label string, context []byte, length int) *hkdfLabel {
	return &hkdfLabel{
		length:  uint16(length),
		label:   "tls13 " + label,
		context: context,
	}
}

func (l *hkdfLabel) marshal() []byte {
	b := appendUint16(nil, l.length)
	b = append(b, withUint8Length([]byte(l.label))...)
	return append(b, withUint8Length(l.context)...)
}

hkdfExpandLabelを実装する ​

hkdfExpandLabelは、newHkdfLabelで作ったhkdfLabel構造体をmarshalメソッドでバイト列にし、そのバイト列をinfoとしてhkdf.Expandに渡します。hkdf.Expandはinfoをstring型で受け取りますが、Goのstringは任意のバイト列を入れられるので、変換しても中身は変わりません。

go
func hkdfExpandLabel(secret []byte, label string, context []byte, length int) []byte {
	hkdfLabel := newHkdfLabel(label, context, length)
	out, err := hkdf.Expand(sha256.New, secret, string(hkdfLabel.marshal()), length)
	if err != nil {
		panic(err)
	}
	return out
}

Key Scheduleを組む ​

Key Scheduleの図を、上から順にGoの関数にします。

Transcript Hashを計算する ​

transcriptは、送受信したHandshakeメッセージをwriteで書き足し、sumでその時点のハッシュを返します。hash.HashのSumは状態を変えないので、sumを呼んだ後もwriteを続けられます。

Transcript Hashは、ハンドシェイクの途中で何度も必要になります。ハンドシェイク鍵の導出ではServerHelloまで、サーバーのFinishedの検証ではCertificateVerifyまでというように、同じメッセージ列の先頭から途中までのハッシュを、時点を変えて使います。メッセージを溜めておいて毎回まとめてハッシュ化しても同じ値になりますが、SHA-256はデータを何回かに分けて入力しても、まとめて入力したときと同じハッシュ値を返します。そのため、メッセージを届いた順にWriteでハッシュ関数へ入力していけば、メッセージ全体を持ち続けずに済み、先頭から計算し直す必要もありません。Goのcrypto/tlsも同じ方式です。

go
package tls13

import (
	"crypto/hkdf"
	"crypto/sha256"
	"hash"
)

type transcript struct {
	h hash.Hash
}

func newTranscript() *transcript {
	return &transcript{h: sha256.New()}
}

func (t *transcript) write(msg []byte) {
	t.h.Write(msg)
}

func (t *transcript) sum() []byte {
	return t.h.Sum(nil)
}

HKDF-ExtractとDerive-Secretを実装する ​

hkdfExtractは、hkdf.Extractの引数をRFCのHKDF-Extract(salt, IKM)と同じ順に並べ直した関数です。Goのhkdf.ExtractはIKMを先に受け取るので、RFCの式と同じ順で書けるようにしておきます。deriveSecretはRFCのDerive-Secretで、ハッシュ長の32バイトを取り出します。

go
func hkdfExtract(salt, ikm []byte) []byte {
	secret, err := hkdf.Extract(sha256.New, ikm, salt)
	if err != nil {
		panic(err)
	}
	return secret
}

func deriveSecret(secret []byte, label string, transcriptHash []byte) []byte {
	return hkdfExpandLabel(secret, label, transcriptHash, sha256.Size)
}

各段の秘密を導出する ​

deriveEarlySecret、deriveHandshakeSecret、deriveMainSecretは、図のEarly Secret、Handshake Secret、Main Secretの段に対応します。Handshake SecretとMain Secretは、上の段の秘密から"derived"で作った値をsaltにしてExtractします。"derived"のMessagesは空なので、contextには空のバイト列のSHA-256を渡します。

go
func deriveEarlySecret() []byte {
	zeros := make([]byte, sha256.Size)
	return hkdfExtract(zeros, zeros)
}

func deriveHandshakeSecret(sharedSecret []byte) []byte {
	emptyHash := sha256.Sum256(nil)
	salt := deriveSecret(deriveEarlySecret(), "derived", emptyHash[:])
	return hkdfExtract(salt, sharedSecret)
}

func deriveMainSecret(handshakeSecret []byte) []byte {
	emptyHash := sha256.Sum256(nil)
	salt := deriveSecret(handshakeSecret, "derived", emptyHash[:])
	return hkdfExtract(salt, make([]byte, sha256.Size))
}

本書はPSKを使わないので、Early Secretは32バイトのゼロから計算する決まった値です。ただし、Handshake Secretのsaltを作るのに必要なので省略はできません。

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

.
├── 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 (
	"crypto/hkdf"
	"crypto/sha256"
	"hash"
)

type transcript struct {
	h hash.Hash
}

func newTranscript() *transcript {
	return &transcript{h: sha256.New()}
}

func (t *transcript) write(msg []byte) {
	t.h.Write(msg)
}

func (t *transcript) sum() []byte {
	return t.h.Sum(nil)
}

func hkdfExtract(salt, ikm []byte) []byte {
	secret, err := hkdf.Extract(sha256.New, ikm, salt)
	if err != nil {
		panic(err)
	}
	return secret
}

func deriveSecret(secret []byte, label string, transcriptHash []byte) []byte {
	return hkdfExpandLabel(secret, label, transcriptHash, sha256.Size)
}

func deriveEarlySecret() []byte {
	zeros := make([]byte, sha256.Size)
	return hkdfExtract(zeros, zeros)
}

func deriveHandshakeSecret(sharedSecret []byte) []byte {
	emptyHash := sha256.Sum256(nil)
	salt := deriveSecret(deriveEarlySecret(), "derived", emptyHash[:])
	return hkdfExtract(salt, sharedSecret)
}

func deriveMainSecret(handshakeSecret []byte) []byte {
	emptyHash := sha256.Sum256(nil)
	salt := deriveSecret(handshakeSecret, "derived", emptyHash[:])
	return hkdfExtract(salt, make([]byte, sha256.Size))
}

type hkdfLabel struct {
	length  uint16
	label   string
	context []byte
}

func newHkdfLabel(label string, context []byte, length int) *hkdfLabel {
	return &hkdfLabel{
		length:  uint16(length),
		label:   "tls13 " + label,
		context: context,
	}
}

func (l *hkdfLabel) marshal() []byte {
	b := appendUint16(nil, l.length)
	b = append(b, withUint8Length([]byte(l.label))...)
	return append(b, withUint8Length(l.context)...)
}

func hkdfExpandLabel(secret []byte, label string, context []byte, length int) []byte {
	hkdfLabel := newHkdfLabel(label, context, length)
	out, err := hkdf.Expand(sha256.New, secret, string(hkdfLabel.marshal()), length)
	if err != nil {
		panic(err)
	}
	return out
}