ServerHelloと共有秘密
本ページでは、サーバーから受け取ったServerHelloをパースし、選択された暗号スイートや鍵交換方式を確認します。そして、サーバーの公開鍵とクライアントの秘密鍵を使って共有秘密を計算します。
ServerHelloのデータ構造
ServerHelloは、RFCで次のように定義されています。
cstruct { ProtocolVersion legacy_version = 0x0303; /* TLS v1.2 */ Random random; opaque legacy_session_id_echo<0..32>; CipherSuite cipher_suite; uint8 legacy_compression_method = 0; Extension extensions<6..2^16-1>; } ServerHello;
フィールドの並びはClientHelloとほぼ同じですが、いくつか違いがあります。ClientHelloのcipher_suitesには候補を複数並べましたが、ServerHelloのcipher_suiteには、サーバーが選んだ暗号スイートが1つだけ入ります。また、legacy_compression_methodsはlegacy_compression_methodとなり、長さフィールドのない1バイト固定の値になります。
拡張にも違いがあります。supported_versionsには選択されたTLSのバージョンが1つ、key_shareにはサーバーの公開鍵が1つ入ります。
実装
読み取り用のヘルパを足す
読み取り側のヘルパをbytes.goに追加します。 バイト列から値の読み出しと、読み出し後の残りのバイト列を返す形にします。
func readUint8(b []byte) (value uint8, rest []byte, ok bool) {
if len(b) < 1 {
return 0, nil, false
}
return b[0], b[1:], true
}
func readUint16(b []byte) (value uint16, rest []byte, ok bool) {
if len(b) < 2 {
return 0, nil, false
}
return uint16(b[0])<<8 | uint16(b[1]), b[2:], true
}
func readBytes(b []byte, n int) (value []byte, rest []byte, ok bool) {
if len(b) < n {
return nil, nil, false
}
return b[:n], b[n:], true
}
func readUint8Prefixed(b []byte) (value []byte, rest []byte, ok bool) {
n, rest, ok := readUint8(b)
if !ok {
return nil, nil, false
}
return readBytes(rest, int(n))
}
func readUint16Prefixed(b []byte) (value []byte, rest []byte, ok bool) {
n, rest, ok := readUint16(b)
if !ok {
return nil, nil, false
}
return readBytes(rest, int(n))
}
func readUint24Prefixed(b []byte) (value []byte, rest []byte, ok bool) {
if len(b) < 3 {
return nil, nil, false
}
n := int(b[0])<<16 | int(b[1])<<8 | int(b[2])
return readBytes(b[3:], n)
}構造体とパーサーを実装する
まず、extensions.goに拡張を読み取るUnmarshalExtensionsを追加します。拡張は「2バイトの種別 + 2バイトの長さ + 中身」という形式なので、これを繰り返し読み取ってExtensionのスライスにする関数を作ります。
func UnmarshalExtensions(data []byte) ([]Extension, bool) {
var exts []Extension
rest := data
for len(rest) > 0 {
extType, r, ok := readUint16(rest)
if !ok {
return nil, false
}
payload, r, ok := readUint16Prefixed(r)
if !ok {
return nil, false
}
exts = append(exts, Extension{
Type: extensionType(extType),
Payload: append([]byte{}, payload...),
})
rest = r
}
return exts, true
}Payloadには、append([]byte{}, payload...)でコピーしたバイト列を格納します。元のバイト列が後から書き換えられても、取り出した拡張の内容が変わらないようにするためです。
次に、server_hello.goを作成します。ServerHello構造体は、RFCで定義されたフィールドに沿って用意します。パース処理では、先頭から順に値を読み取り、ヘルパが返した残りのバイト列を次の読み取りに渡していきます。
package tls13
import "errors"
type ServerHello struct {
ProtocolVersion TLSVersion
Random [32]byte
LegacySessionID []byte
CipherSuite CipherSuite
CompressionMethod uint8
Extensions []Extension
}
func UnmarshalServerHello(body []byte) (*ServerHello, error) {
malformed := errors.New("tls13: malformed ServerHello")
version, rest, ok := readUint16(body)
if !ok {
return nil, malformed
}
random, rest, ok := readBytes(rest, 32)
if !ok {
return nil, malformed
}
sessionID, rest, ok := readUint8Prefixed(rest)
if !ok {
return nil, malformed
}
suite, rest, ok := readUint16(rest)
if !ok {
return nil, malformed
}
compressionMethod, rest, ok := readUint8(rest)
if !ok {
return nil, malformed
}
extensions, rest, ok := readUint16Prefixed(rest)
if !ok || len(rest) != 0 {
return nil, malformed
}
exts, ok := UnmarshalExtensions(extensions)
if !ok {
return nil, errors.New("tls13: malformed ServerHello extensions")
}
sh := &ServerHello{
ProtocolVersion: TLSVersion(version),
LegacySessionID: append([]byte{}, sessionID...),
CipherSuite: CipherSuite(suite),
CompressionMethod: compressionMethod,
Extensions: exts,
}
copy(sh.Random[:], random)
return sh, nil
}len(rest) != 0では、拡張のバイト列を切り出したあとに余分なデータが残っていないかを確認しています。これにより、申告された長さと実際のデータの境界が合わない場合にエラーを返せます。
拡張から値を取り出す
拡張は共通のExtension型で保持しているため、必要な拡張を探して値を取り出すメソッドを用意します。今回はsupported_versionsとkey_shareの2つを読み取ります。
func (sh *ServerHello) SelectedVersion() (version TLSVersion, found bool) {
for _, ext := range sh.Extensions {
if ext.Type == extSupportedVersions {
if v, _, ok := readUint16(ext.Payload); ok {
return TLSVersion(v), true
}
}
}
return 0, false
}
func (sh *ServerHello) KeyShare() (group NamedGroup, publicKey []byte, found bool) {
for _, ext := range sh.Extensions {
if ext.Type == extKeyShare {
g, rest, ok := readUint16(ext.Payload)
if !ok {
continue
}
key, _, ok := readUint16Prefixed(rest)
if !ok {
continue
}
return NamedGroup(g), key, true
}
}
return 0, nil, false
}cmd/main.goを書き換える
前のページでは、cmd/main.goでClientHelloを送信し、受信したレコードをダンプするところまで実装しました。今回はその応答をServerHelloとして解析し、共有秘密を計算します。ClientHelloを送信するまでの処理はそのまま使い、受信処理を書き換えます。
ServerHelloを受け取ってパースする
前のページで書いた、受信レコードをhex.Dumpで表示するループを削除し、ServerHelloを読み取る処理に置き換えます。まずReadRecordでレコードを1つ読み、UnmarshalHandshakeでハンドシェイクメッセージを取り出します。メッセージの種別がServerHelloであることを確認してから、UnmarshalServerHelloで本体をパースします。
defer conn.Close()
out := rec.Marshal()
fmt.Printf("--> send %d bytes\n%s\n", len(out), hex.Dump(out))
if _, err := conn.Write(out); err != nil {
if _, err := conn.Write(rec.Marshal()); err != nil {
panic(err)
}
var raw bytes.Buffer
r := io.TeeReader(conn, &raw)
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
for i := 1; ; i++ {
if _, err := tls13.ReadRecord(r); err != nil {
break
}
fmt.Printf("<-- record %d: %d bytes\n%s\n", i, raw.Len(), hex.Dump(raw.Bytes()))
raw.Reset()
}
res, err := tls13.ReadRecord(conn)
if err != nil {
panic(err)
}
msg, err := tls13.UnmarshalHandshake(res.Payload)
if err != nil {
panic(err)
}
if msg.HandshakeType != tls13.TypeServerHello {
panic("tls13: expected ServerHello")
}
sh, err := tls13.UnmarshalServerHello(msg.Body)
if err != nil {
panic(err)
}サーバーの選択を検証する
ServerHelloをパースできても、サーバーが選んだ値をそのまま使うわけにはいきません。暗号スイート、TLSのバージョン、鍵交換方式の3つが、クライアントの提示した条件に合っているかを確認します。
想定外の値が返ってきた場合は、処理を中断します。ここでは実装を簡単にするためpanicを使いますが、実際のTLSでは適切なAlertを送信して接続を終了します。
sh, err := tls13.UnmarshalServerHello(msg.Body)
if err != nil {
panic(err)
}
if sh.CipherSuite != tls13.TLS_AES_128_GCM_SHA256 {
panic("tls13: server selected an unsupported cipher suite")
}
version, ok := sh.SelectedVersion()
if !ok || version != tls13.TLS_VERSION_1_3 {
panic("tls13: server did not negotiate TLS 1.3")
}
group, keyExchange, ok := sh.KeyShare()
if !ok || group != tls13.X25519 {
panic("tls13: server did not send an X25519 key share")
}
fmt.Printf("cipher suite = %#04x\n", uint16(sh.CipherSuite))
fmt.Printf("version = %#04x\n", uint16(version))
fmt.Printf("group = %#04x\n", uint16(group))
fmt.Printf("server key = %x\n", keyExchange)key_shareの検証では、同時にサーバーの公開鍵keyExchangeも受け取っています。
HelloRetryRequestについて
ServerHelloと同じ形で届くのにServerHelloではない、HelloRetryRequestというメッセージがあります。サーバーは、クライアントが提示した鍵交換方式を扱えないとき、HelloRetryRequestを返してClientHelloの送り直しを求めます。本書のクライアントはHelloRetryRequestに対応していません。HelloRetryRequestの仕組みはRFC 9846 §4.2.4に定められています。
共有秘密を計算する
最後に、受け取った公開鍵をX25519の公開鍵に変換し、こちらの秘密鍵と掛け合わせます。privはClientHelloを組み立てるときに生成した鍵ペアで、ECDHが32バイトの共有秘密を返します。
fmt.Printf("server key = %x\n", keyExchange)
serverPub, err := ecdh.X25519().NewPublicKey(keyExchange)
if err != nil {
panic(err)
}
shared, err := priv.ECDH(serverPub)
if err != nil {
panic(err)
}
fmt.Printf("shared secret = %x\n", shared)
}動かす
$ go run ./cmd
cipher suite = 0x1301
version = 0x0304
group = 0x001d
server key = a2ca19b41569c8f646b52126333e626e23b4039c110678d5590b54461b3b132f
shared secret = b74bae57936d2fb3860d5201aea180ce1f889bb51d221bee7362d6929fe9100bサーバーが選んだ3つの値を確認できました。0x1301はTLS_AES_128_GCM_SHA256、0x0304はTLS 1.3、0x001dはX25519を表しています。
最後に表示された32バイトが共有秘密です。この値自体はネットワーク上を流れません。交換したのは互いの公開鍵だけですが、それぞれの秘密鍵を持たない第三者が、公開鍵だけから共有秘密を計算することは困難です。
サーバーも、クライアントの公開鍵と自身の秘密鍵から同じ共有秘密を計算します。第4章では、この共有秘密を使って暗号鍵を導出します。
本ページで書いたコード
.
├── cmd/
│ └── main.go
├── go.mod
├── common.go
├── record.go
├── handshake.go
├── bytes.go
├── client_hello.go
├── extensions.go
└── server_hello.gopackage tls13
import "errors"
type ServerHello struct {
ProtocolVersion TLSVersion
Random [32]byte
LegacySessionID []byte
CipherSuite CipherSuite
CompressionMethod uint8
Extensions []Extension
}
func UnmarshalServerHello(body []byte) (*ServerHello, error) {
malformed := errors.New("tls13: malformed ServerHello")
version, rest, ok := readUint16(body)
if !ok {
return nil, malformed
}
random, rest, ok := readBytes(rest, 32)
if !ok {
return nil, malformed
}
sessionID, rest, ok := readUint8Prefixed(rest)
if !ok {
return nil, malformed
}
suite, rest, ok := readUint16(rest)
if !ok {
return nil, malformed
}
compressionMethod, rest, ok := readUint8(rest)
if !ok {
return nil, malformed
}
extensions, rest, ok := readUint16Prefixed(rest)
if !ok || len(rest) != 0 {
return nil, malformed
}
exts, ok := UnmarshalExtensions(extensions)
if !ok {
return nil, errors.New("tls13: malformed ServerHello extensions")
}
sh := &ServerHello{
ProtocolVersion: TLSVersion(version),
LegacySessionID: append([]byte{}, sessionID...),
CipherSuite: CipherSuite(suite),
CompressionMethod: compressionMethod,
Extensions: exts,
}
copy(sh.Random[:], random)
return sh, nil
}
func (sh *ServerHello) SelectedVersion() (version TLSVersion, found bool) {
for _, ext := range sh.Extensions {
if ext.Type == extSupportedVersions {
if v, _, ok := readUint16(ext.Payload); ok {
return TLSVersion(v), true
}
}
}
return 0, false
}
func (sh *ServerHello) KeyShare() (group NamedGroup, publicKey []byte, found bool) {
for _, ext := range sh.Extensions {
if ext.Type == extKeyShare {
g, rest, ok := readUint16(ext.Payload)
if !ok {
continue
}
key, _, ok := readUint16Prefixed(rest)
if !ok {
continue
}
return NamedGroup(g), key, true
}
}
return 0, nil, false
}package tls13
type Extension struct {
Type extensionType
Payload []byte
}
func (e Extension) Marshal() []byte {
buf := appendUint16(nil, uint16(e.Type))
return append(buf, withUint16Length(e.Payload)...)
}
func UnmarshalExtensions(data []byte) ([]Extension, bool) {
var exts []Extension
rest := data
for len(rest) > 0 {
extType, r, ok := readUint16(rest)
if !ok {
return nil, false
}
payload, r, ok := readUint16Prefixed(r)
if !ok {
return nil, false
}
exts = append(exts, Extension{
Type: extensionType(extType),
Payload: append([]byte{}, payload...),
})
rest = r
}
return exts, true
}
func SupportedVersionsExtension() Extension {
versions := appendUint16(nil, uint16(TLS_VERSION_1_3))
return Extension{Type: extSupportedVersions, Payload: withUint8Length(versions)}
}
func ServerNameExtension(name string) Extension {
entry := append([]byte{0}, withUint16Length([]byte(name))...)
return Extension{Type: extServerName, Payload: withUint16Length(entry)}
}
func SignatureAlgorithmsExtension() Extension {
var algorithms []byte
for _, a := range []SignatureScheme{
ECDSA_SECP256R1_SHA256, ECDSA_SECP384R1_SHA384,
RSA_PSS_RSAE_SHA256, RSA_PSS_RSAE_SHA384, RSA_PSS_RSAE_SHA512,
RSA_PKCS1_SHA256,
} {
algorithms = appendUint16(algorithms, uint16(a))
}
return Extension{Type: extSignatureAlgorithms, Payload: withUint16Length(algorithms)}
}
func SupportedGroupsExtension() Extension {
groups := appendUint16(nil, uint16(X25519))
return Extension{Type: extSupportedGroups, Payload: withUint16Length(groups)}
}
func KeyShareExtension(publicKey []byte) Extension {
entry := appendUint16(nil, uint16(X25519))
entry = append(entry, withUint16Length(publicKey)...)
return Extension{Type: extKeyShare, Payload: withUint16Length(entry)}
}package tls13
func appendUint16(b []byte, v uint16) []byte {
return append(b, byte(v>>8), byte(v))
}
func withUint8Length(body []byte) []byte {
return append([]byte{byte(len(body))}, body...)
}
func withUint16Length(body []byte) []byte {
return append([]byte{byte(len(body) >> 8), byte(len(body))}, body...)
}
func readUint8(b []byte) (value uint8, rest []byte, ok bool) {
if len(b) < 1 {
return 0, nil, false
}
return b[0], b[1:], true
}
func readUint16(b []byte) (value uint16, rest []byte, ok bool) {
if len(b) < 2 {
return 0, nil, false
}
return uint16(b[0])<<8 | uint16(b[1]), b[2:], true
}
func readBytes(b []byte, n int) (value []byte, rest []byte, ok bool) {
if len(b) < n {
return nil, nil, false
}
return b[:n], b[n:], true
}
func readUint8Prefixed(b []byte) (value []byte, rest []byte, ok bool) {
n, rest, ok := readUint8(b)
if !ok {
return nil, nil, false
}
return readBytes(rest, int(n))
}
func readUint16Prefixed(b []byte) (value []byte, rest []byte, ok bool) {
n, rest, ok := readUint16(b)
if !ok {
return nil, nil, false
}
return readBytes(rest, int(n))
}
func readUint24Prefixed(b []byte) (value []byte, rest []byte, ok bool) {
if len(b) < 3 {
return nil, nil, false
}
n := int(b[0])<<16 | int(b[1])<<8 | int(b[2])
return readBytes(b[3:], n)
}package main
import (
"crypto/ecdh"
"crypto/rand"
"fmt"
"net"
"tls13"
)
const (
serverName = "example.com"
addr = "example.com:443"
)
func main() {
priv, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
var random [32]byte
rand.Read(random[:])
ch := tls13.NewClientHello(random, []tls13.Extension{
tls13.SupportedVersionsExtension(),
tls13.ServerNameExtension(serverName),
tls13.SignatureAlgorithmsExtension(),
tls13.SupportedGroupsExtension(),
tls13.KeyShareExtension(priv.PublicKey().Bytes()),
})
h := tls13.NewHandshake(tls13.TypeClientHello, ch.Marshal())
rec := tls13.NewTLSPlaintext(tls13.ContentTypeHandshake, h.Marshal())
conn, err := net.Dial("tcp", addr)
if err != nil {
panic(err)
}
defer conn.Close()
if _, err := conn.Write(rec.Marshal()); err != nil {
panic(err)
}
res, err := tls13.ReadRecord(conn)
if err != nil {
panic(err)
}
msg, err := tls13.UnmarshalHandshake(res.Payload)
if err != nil {
panic(err)
}
if msg.HandshakeType != tls13.TypeServerHello {
panic("tls13: expected ServerHello")
}
sh, err := tls13.UnmarshalServerHello(msg.Body)
if err != nil {
panic(err)
}
if sh.CipherSuite != tls13.TLS_AES_128_GCM_SHA256 {
panic("tls13: server selected an unsupported cipher suite")
}
version, ok := sh.SelectedVersion()
if !ok || version != tls13.TLS_VERSION_1_3 {
panic("tls13: server did not negotiate TLS 1.3")
}
group, keyExchange, ok := sh.KeyShare()
if !ok || group != tls13.X25519 {
panic("tls13: server did not send an X25519 key share")
}
fmt.Printf("cipher suite = %#04x\n", uint16(sh.CipherSuite))
fmt.Printf("version = %#04x\n", uint16(version))
fmt.Printf("group = %#04x\n", uint16(group))
fmt.Printf("server key = %x\n", keyExchange)
serverPub, err := ecdh.X25519().NewPublicKey(keyExchange)
if err != nil {
panic(err)
}
shared, err := priv.ECDH(serverPub)
if err != nil {
panic(err)
}
fmt.Printf("shared secret = %x\n", shared)
}