公開APIを整える
本ページでは、不要になったフィールドを削除し、動作確認のために公開していた型、関数、定数を非公開に戻します。パッケージの外から使えるものをDial、Config、Connだけにして、TLS 1.3クライアントを完成させます。
実装
使わなくなったフィールドを削除する
PeerCertificates、VerifiedChains、PeerCertificateVerifyは、cmd/main.goから証明書や検証結果を表示するために公開していたフィールドです。このうちVerifiedChainsはほかの処理で使わないため削除し、VerifyCertificateで検証結果を保存していた処理も取り除きます。
go
type ClientHandshakeState struct {
// ...
PeerCertificates []*x509.Certificate
VerifiedChains [][]*x509.Certificate
PeerCertificateVerify *CertificateVerify
certificateVerifyTranscriptHash []byte
}
func (hs *ClientHandshakeState) VerifyCertificate() error {
certs := hs.PeerCertificates
// ...
chains, err := certs[0].Verify(opts)
if err != nil {
if _, err := certs[0].Verify(opts); err != nil {
return fmt.Errorf("tls13: certificate verification failed: %w", err)
}
hs.VerifiedChains = chains
return nil
}PeerCertificatesとPeerCertificateVerifyは、VerifyCertificateVerifyでサーバー証明書と署名を参照するため残します。
型、関数、定数を非公開にする
パッケージの利用者に公開するのは、Dial、Config、Connだけです。それ以外の型、関数、定数は、名前を小文字で始まるものに変えて非公開にします。
次のコードをtool/main.goに書き、プロジェクトのルートで実行すると、変換表renamesにしたがって名前を変更することができます。
tool/main.go
go
package main
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/importer"
"go/parser"
"go/token"
"go/types"
"os"
"path/filepath"
"sort"
"strings"
)
type rename struct {
file string
old string
new string
}
var renames = []rename{
{"common.go", "ContentType", "contentType"},
{"common.go", "ContentTypeChangeCipherSpec", "contentTypeChangeCipherSpec"},
{"common.go", "ContentTypeAlert", "contentTypeAlert"},
{"common.go", "ContentTypeHandshake", "contentTypeHandshake"},
{"common.go", "ContentTypeApplicationData", "contentTypeApplicationData"},
{"common.go", "HandshakeType", "handshakeType"},
{"common.go", "TypeClientHello", "typeClientHello"},
{"common.go", "TypeServerHello", "typeServerHello"},
{"common.go", "TypeEncryptedExtensions", "typeEncryptedExtensions"},
{"common.go", "TypeCertificate", "typeCertificate"},
{"common.go", "TypeCertificateVerify", "typeCertificateVerify"},
{"common.go", "TypeFinished", "typeFinished"},
{"common.go", "TLSVersion", "tlsVersion"},
{"common.go", "TLS_VERSION_1_2", "tls_version_1_2"},
{"common.go", "TLS_VERSION_1_3", "tls_version_1_3"},
{"common.go", "CipherSuite", "cipherSuite"},
{"common.go", "TLS_AES_128_GCM_SHA256", "tls_aes_128_gcm_sha256"},
{"common.go", "NamedGroup", "namedGroup"},
{"common.go", "X25519", "x25519"},
{"common.go", "SignatureScheme", "signatureScheme"},
{"common.go", "RSA_PKCS1_SHA256", "rsa_pkcs1_sha256"},
{"common.go", "ECDSA_SECP256R1_SHA256", "ecdsa_secp256r1_sha256"},
{"common.go", "ECDSA_SECP384R1_SHA384", "ecdsa_secp384r1_sha384"},
{"common.go", "RSA_PSS_RSAE_SHA256", "rsa_pss_rsae_sha256"},
{"common.go", "RSA_PSS_RSAE_SHA384", "rsa_pss_rsae_sha384"},
{"common.go", "RSA_PSS_RSAE_SHA512", "rsa_pss_rsae_sha512"},
{"record.go", "TLSPlaintext", "tlsPlaintext"},
{"record.go", "TLSPlaintext.Type", "contentType"},
{"record.go", "TLSPlaintext.Version", "version"},
{"record.go", "TLSPlaintext.Length", "length"},
{"record.go", "TLSPlaintext.Payload", "payload"},
{"record.go", "TLSPlaintext.Marshal", "marshal"},
{"record.go", "NewTLSPlaintext", "newTLSPlaintext"},
{"record.go", "ReadRecord", "readRecord"},
{"handshake.go", "Handshake", "handshake"},
{"handshake.go", "Handshake.HandshakeType", "handshakeType"},
{"handshake.go", "Handshake.Length", "length"},
{"handshake.go", "Handshake.Body", "body"},
{"handshake.go", "Handshake.Marshal", "marshal"},
{"handshake.go", "NewHandshake", "newHandshake"},
{"handshake.go", "UnmarshalHandshake", "unmarshalHandshake"},
{"extensions.go", "Extension", "extension"},
{"extensions.go", "Extension.Type", "extensionType"},
{"extensions.go", "Extension.Payload", "payload"},
{"extensions.go", "Extension.Marshal", "marshal"},
{"extensions.go", "SupportedVersionsExtension", "supportedVersionsExtension"},
{"extensions.go", "ServerNameExtension", "serverNameExtension"},
{"extensions.go", "SignatureAlgorithmsExtension", "signatureAlgorithmsExtension"},
{"extensions.go", "SupportedGroupsExtension", "supportedGroupsExtension"},
{"extensions.go", "KeyShareExtension", "keyShareExtension"},
{"extensions.go", "UnmarshalExtensions", "unmarshalExtensions"},
{"client_hello.go", "ClientHello", "clientHello"},
{"client_hello.go", "ClientHello.ProtocolVersion", "protocolVersion"},
{"client_hello.go", "ClientHello.Random", "random"},
{"client_hello.go", "ClientHello.LegacySessionID", "legacySessionID"},
{"client_hello.go", "ClientHello.CipherSuites", "cipherSuites"},
{"client_hello.go", "ClientHello.LegacyCompressionMethods", "legacyCompressionMethods"},
{"client_hello.go", "ClientHello.Extensions", "extensions"},
{"client_hello.go", "ClientHello.Marshal", "marshal"},
{"client_hello.go", "NewClientHello", "newClientHello"},
{"server_hello.go", "ServerHello", "serverHello"},
{"server_hello.go", "ServerHello.ProtocolVersion", "protocolVersion"},
{"server_hello.go", "ServerHello.Random", "random"},
{"server_hello.go", "ServerHello.LegacySessionID", "legacySessionID"},
{"server_hello.go", "ServerHello.CipherSuite", "cipherSuite"},
{"server_hello.go", "ServerHello.CompressionMethod", "compressionMethod"},
{"server_hello.go", "ServerHello.Extensions", "extensions"},
{"server_hello.go", "ServerHello.SelectedVersion", "selectedVersion"},
{"server_hello.go", "ServerHello.KeyShare", "keyShare"},
{"server_hello.go", "UnmarshalServerHello", "unmarshalServerHello"},
{"certificate.go", "CertificateVerify", "certificateVerify"},
{"certificate.go", "CertificateVerify.Algorithm", "algorithm"},
{"certificate.go", "CertificateVerify.Signature", "signature"},
{"certificate.go", "CertificateVerify.Verify", "verify"},
{"certificate.go", "UnmarshalCertificateVerify", "unmarshalCertificateVerify"},
{"handshake_client.go", "ClientHandshakeState", "clientHandshakeState"},
{"handshake_client.go", "ClientHandshakeState.PeerCertificates", "peerCertificates"},
{"handshake_client.go", "ClientHandshakeState.PeerCertificateVerify", "peerCertificateVerify"},
{"handshake_client.go", "ClientHandshakeState.GenerateKeyPair", "generateKeyPair"},
{"handshake_client.go", "ClientHandshakeState.SendClientHello", "sendClientHello"},
{"handshake_client.go", "ClientHandshakeState.ReadServerHello", "readServerHello"},
{"handshake_client.go", "ClientHandshakeState.ComputeSharedSecret", "computeSharedSecret"},
{"handshake_client.go", "ClientHandshakeState.EstablishHandshakeKeys", "establishHandshakeKeys"},
{"handshake_client.go", "ClientHandshakeState.ReadEncryptedExtensions", "readEncryptedExtensions"},
{"handshake_client.go", "ClientHandshakeState.ReadCertificate", "readCertificate"},
{"handshake_client.go", "ClientHandshakeState.VerifyCertificate", "verifyCertificate"},
{"handshake_client.go", "ClientHandshakeState.ReadCertificateVerify", "readCertificateVerify"},
{"handshake_client.go", "ClientHandshakeState.VerifyCertificateVerify", "verifyCertificateVerify"},
{"handshake_client.go", "ClientHandshakeState.ReadServerFinished", "readServerFinished"},
{"handshake_client.go", "ClientHandshakeState.WriteClientFinished", "writeClientFinished"},
{"handshake_client.go", "ClientHandshakeState.SwitchToApplicationKeys", "switchToApplicationKeys"},
}
type edit struct {
offset int
length int
text string
}
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() (err error) {
paths, err := filepath.Glob("*.go")
if err != nil {
return err
}
sources := map[string][]byte{}
for _, path := range paths {
src, err := os.ReadFile(path)
if err != nil {
return err
}
sources[path] = src
}
fset, files, pkg, info, err := check(sources)
if err != nil {
return err
}
newNames := map[types.Object]string{}
for _, r := range renames {
obj, err := lookup(pkg, r.old)
if err != nil {
return err
}
if got := filepath.Base(fset.Position(obj.Pos()).Filename); got != r.file {
return fmt.Errorf("%s は %s ではなく %s で定義されています", r.old, r.file, got)
}
newNames[obj] = r.new
}
edits := map[string][]edit{}
for path, file := range files {
ast.Inspect(file, func(n ast.Node) bool {
id, ok := n.(*ast.Ident)
if !ok {
return true
}
obj := info.Defs[id]
if obj == nil {
obj = info.Uses[id]
}
if name, ok := newNames[obj]; ok {
offset := fset.Position(id.Pos()).Offset
edits[path] = append(edits[path], edit{offset: offset, length: len(id.Name), text: name})
}
return true
})
}
results := map[string][]byte{}
for path, src := range sources {
out := applyEdits(src, edits[path])
formatted, err := format.Source(out)
if err != nil {
return fmt.Errorf("%s: %w", path, err)
}
results[path] = formatted
}
if _, _, _, _, err := check(results); err != nil {
return fmt.Errorf("書き換えたあとのコードがコンパイルできません: %w", err)
}
for _, path := range paths {
if bytes.Equal(results[path], sources[path]) {
continue
}
if err := os.WriteFile(path, results[path], 0o644); err != nil {
return err
}
fmt.Printf("%s: %d か所\n", path, len(edits[path]))
}
return nil
}
func check(sources map[string][]byte) (fset *token.FileSet, files map[string]*ast.File, pkg *types.Package, info *types.Info, err error) {
fset = token.NewFileSet()
files = map[string]*ast.File{}
var list []*ast.File
for path, src := range sources {
file, err := parser.ParseFile(fset, path, src, parser.ParseComments)
if err != nil {
return nil, nil, nil, nil, err
}
files[path] = file
list = append(list, file)
}
info = &types.Info{
Defs: map[*ast.Ident]types.Object{},
Uses: map[*ast.Ident]types.Object{},
}
conf := types.Config{Importer: importer.ForCompiler(fset, "source", nil)}
pkg, err = conf.Check(list[0].Name.Name, fset, list, info)
if err != nil {
return nil, nil, nil, nil, err
}
return fset, files, pkg, info, nil
}
func lookup(pkg *types.Package, name string) (obj types.Object, err error) {
typeName, member, found := strings.Cut(name, ".")
obj = pkg.Scope().Lookup(typeName)
if obj == nil {
return nil, fmt.Errorf("%s が見つかりません", name)
}
if !found {
return obj, nil
}
obj, _, _ = types.LookupFieldOrMethod(obj.Type(), true, pkg, member)
if obj == nil {
return nil, fmt.Errorf("%s が見つかりません", name)
}
return obj, nil
}
func applyEdits(src []byte, edits []edit) (out []byte) {
sort.Slice(edits, func(i, j int) bool { return edits[i].offset > edits[j].offset })
out = append([]byte{}, src...)
for _, e := range edits {
out = append(out[:e.offset], append([]byte(e.text), out[e.offset+e.length:]...)...)
}
return out
}sh
$ go run ./tool
certificate.go: 20 か所
client_hello.go: 32 か所
common.go: 46 か所
conn.go: 33 か所
extensions.go: 48 か所
handshake.go: 25 か所
handshake_client.go: 101 か所
record.go: 34 か所
record_protection.go: 10 か所
server_hello.go: 36 か所go docで、パッケージの外から使える名前を確かめます。
sh
$ go doc .
package tls13 // import "tls13"
type Config struct{ ... }
type Conn struct{ ... }
func Dial(network, addr string, config *Config) (*Conn, error)表示されたのがConfig、Conn、Dialだけなので、ほかの型、関数、定数が非公開になったことを確認できます。
動かす
外部から見た動作は変わりません。これまでと同じコマンドで実行します。
sh
$ go run ./cmd https://example.com/
HTTP/1.1 200 OK
Date: Sat, 03 Oct 2026 15:36:00 GMT
Content-Type: text/html; charset=utf-8
...本ページで書いたコード
.
├── 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.gogo
package tls13
import (
"crypto/x509"
"errors"
"fmt"
)
type certificate struct {
certificateList [][]byte
}
func unmarshalCertificate(body []byte) (*certificate, error) {
malformed := errors.New("tls13: malformed Certificate")
_, rest, ok := readUint8Prefixed(body)
if !ok {
return nil, malformed
}
list, rest, ok := readUint24Prefixed(rest)
if !ok || len(rest) != 0 {
return nil, malformed
}
cert := &certificate{}
for len(list) > 0 {
certData, r, ok := readUint24Prefixed(list)
if !ok {
return nil, malformed
}
_, r, ok = readUint16Prefixed(r)
if !ok {
return nil, malformed
}
cert.certificateList = append(cert.certificateList, append([]byte{}, certData...))
list = r
}
return cert, nil
}
type certificateVerify struct {
algorithm signatureScheme
signature []byte
}
func unmarshalCertificateVerify(body []byte) (*certificateVerify, error) {
malformed := errors.New("tls13: malformed CertificateVerify")
algorithm, rest, ok := readUint16(body)
if !ok {
return nil, malformed
}
signature, rest, ok := readUint16Prefixed(rest)
if !ok || len(rest) != 0 {
return nil, malformed
}
return &certificateVerify{
algorithm: signatureScheme(algorithm),
signature: append([]byte{}, signature...),
}, nil
}
func (cv *certificateVerify) verify(cert *x509.Certificate, transcriptHash []byte) error {
content := make([]byte, 0, 64+34+len(transcriptHash))
for range 64 {
content = append(content, 0x20)
}
content = append(content, "TLS 1.3, server CertificateVerify"...)
content = append(content, 0)
content = append(content, transcriptHash...)
var algorithm x509.SignatureAlgorithm
switch cv.algorithm {
case rsa_pss_rsae_sha256:
algorithm = x509.SHA256WithRSAPSS
case rsa_pss_rsae_sha384:
algorithm = x509.SHA384WithRSAPSS
case rsa_pss_rsae_sha512:
algorithm = x509.SHA512WithRSAPSS
case ecdsa_secp256r1_sha256:
algorithm = x509.ECDSAWithSHA256
case ecdsa_secp384r1_sha384:
algorithm = x509.ECDSAWithSHA384
default:
return fmt.Errorf("tls13: unsupported signature scheme %#x", uint16(cv.algorithm))
}
return cert.CheckSignature(algorithm, content, cv.signature)
}go
package tls13
const clientRandomLength = 32
type clientHello struct {
protocolVersion tlsVersion
random [clientRandomLength]byte
legacySessionID []byte
cipherSuites []cipherSuite
legacyCompressionMethods []byte
extensions []extension
}
func newClientHello(random [clientRandomLength]byte, exts []extension) *clientHello {
return &clientHello{
protocolVersion: tls_version_1_2,
random: random,
legacySessionID: nil,
cipherSuites: []cipherSuite{tls_aes_128_gcm_sha256},
legacyCompressionMethods: []byte{0x00},
extensions: exts,
}
}
func (ch *clientHello) marshal() []byte {
var cipherSuites []byte
for _, cs := range ch.cipherSuites {
cipherSuites = appendUint16(cipherSuites, uint16(cs))
}
var extensions []byte
for _, ext := range ch.extensions {
extensions = append(extensions, ext.marshal()...)
}
buf := appendUint16(nil, uint16(ch.protocolVersion))
buf = append(buf, ch.random[:]...)
buf = append(buf, withUint8Length(ch.legacySessionID)...)
buf = append(buf, withUint16Length(cipherSuites)...)
buf = append(buf, withUint8Length(ch.legacyCompressionMethods)...)
return append(buf, withUint16Length(extensions)...)
}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
)
// @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 + 256go
package 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.contentType {
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.contentType)
}
}
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.contentType != contentTypeChangeCipherSpec {
break
}
}
if c.in == nil {
return rec, nil
}
if rec.contentType != contentTypeApplicationData {
return nil, fmt.Errorf("tls13: unexpected plaintext record type %d", rec.contentType)
}
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.contentType {
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.contentType)
}
}
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.contentType, rec.payload))
}
_, err = c.conn.Write(rec.marshal())
return err
}go
package tls13
type extension struct {
extensionType extensionType
payload []byte
}
func (e extension) marshal() []byte {
buf := appendUint16(nil, uint16(e.extensionType))
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{
extensionType: extensionType(extType),
payload: append([]byte{}, payload...),
})
rest = r
}
return exts, true
}
func supportedVersionsExtension() extension {
versions := appendUint16(nil, uint16(tls_version_1_3))
return extension{extensionType: extSupportedVersions, payload: withUint8Length(versions)}
}
func serverNameExtension(name string) extension {
entry := append([]byte{0}, withUint16Length([]byte(name))...)
return extension{extensionType: 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{extensionType: extSignatureAlgorithms, payload: withUint16Length(algorithms)}
}
func supportedGroupsExtension() extension {
groups := appendUint16(nil, uint16(x25519))
return extension{extensionType: extSupportedGroups, payload: withUint16Length(groups)}
}
func keyShareExtension(publicKey []byte) extension {
entry := appendUint16(nil, uint16(x25519))
entry = append(entry, withUint16Length(publicKey)...)
return extension{extensionType: extKeyShare, payload: withUint16Length(entry)}
}go
package tls13
import "errors"
var errHandshakeTooShort = errors.New("tls13: handshake message too short")
type handshake struct {
handshakeType handshakeType
length [3]byte
body []byte
}
func newHandshake(handshakeType handshakeType, body []byte) *handshake {
length := len(body)
return &handshake{
handshakeType: handshakeType,
length: [3]byte{byte(length >> 16), byte(length >> 8), byte(length)},
body: body,
}
}
func (h *handshake) marshal() []byte {
buf := make([]byte, 4, 4+len(h.body))
buf[0] = byte(h.handshakeType)
copy(buf[1:], h.length[:])
return append(buf, h.body...)
}
func unmarshalHandshake(data []byte) (*handshake, error) {
if len(data) < 4 {
return nil, errHandshakeTooShort
}
length := int(data[1])<<16 | int(data[2])<<8 | int(data[3])
if len(data) != 4+length {
return nil, errors.New("tls13: handshake length mismatch")
}
return &handshake{
handshakeType: handshakeType(data[0]),
length: [3]byte{data[1], data[2], data[3]},
body: data[4:],
}, 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
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)
}
if _, err := certs[0].Verify(opts); err != nil {
return fmt.Errorf("tls13: certificate verification failed: %w", err)
}
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
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 {
contentType contentType
version tlsVersion
length uint16
payload []byte
}
func newTLSPlaintext(contentType contentType, fragment []byte) *tlsPlaintext {
return &tlsPlaintext{
contentType: 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.contentType)
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{
contentType: 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
}go
package tls13
import (
"crypto/cipher"
"encoding/binary"
)
type recordProtection struct {
aead cipher.AEAD
iv []byte
seq uint64
}
func newRecordProtection(trafficSecret []byte) *recordProtection {
key, iv := trafficKeys(trafficSecret)
return &recordProtection{aead: newAESGCM(key), iv: iv}
}
func (p *recordProtection) nextNonce() []byte {
nonce := make([]byte, len(p.iv))
copy(nonce, p.iv)
tail := nonce[len(nonce)-8:]
binary.BigEndian.PutUint64(tail, binary.BigEndian.Uint64(tail)^p.seq)
p.seq++
return nonce
}
func additionalData(contentType contentType, version tlsVersion, length int) []byte {
return []byte{
byte(contentType),
byte(version >> 8), byte(version & 0xff),
byte(length >> 8), byte(length),
}
}
func (p *recordProtection) decrypt(rec *tlsPlaintext) (*tlsInnerPlaintext, error) {
innerBytes, err := p.aead.Open(nil, p.nextNonce(), rec.payload, additionalData(rec.contentType, rec.version, int(rec.length)))
if err != nil {
return nil, err
}
return parseTLSInnerPlaintext(innerBytes)
}
func (p *recordProtection) encrypt(contentType contentType, plaintext []byte) (encryptedRecord []byte) {
inner := &tlsInnerPlaintext{content: plaintext, contentType: contentType}
innerBytes := inner.marshal()
ciphertextLen := len(innerBytes) + p.aead.Overhead()
return p.aead.Seal(nil, p.nextNonce(), innerBytes, additionalData(contentTypeApplicationData, tls_version_1_2, ciphertextLen))
}go
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
}
func (sh *serverHello) selectedVersion() (version tlsVersion, found bool) {
for _, ext := range sh.extensions {
if ext.extensionType == 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.extensionType == 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
}