diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml
deleted file mode 100644
index 06d6ea0d8..000000000
--- a/.github/workflows/e2e.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-# SPDX-FileCopyrightText: 2026 The Pion community
-# SPDX-License-Identifier: MIT
-
-name: E2E
-on:
- pull_request:
- branches:
- - master
- push:
- branches:
- - master
-
-jobs:
- e2e-test:
- name: Test
- runs-on: ubuntu-latest
- timeout-minutes: 10
- steps:
- - name: checkout
- uses: actions/checkout@v7
- - name: test
- run: |
- docker build -t pion-dtls-e2e -f e2e/Dockerfile .
- docker run -i --rm pion-dtls-e2e
diff --git a/.github/workflows/fuzz.yaml b/.github/workflows/fuzz.yaml
index 9e15a481e..e10207573 100644
--- a/.github/workflows/fuzz.yaml
+++ b/.github/workflows/fuzz.yaml
@@ -23,5 +23,5 @@ jobs:
fuzz:
uses: pion/.goassets/.github/workflows/fuzz.reusable.yml@main
with:
- go-version: "1.25" # auto-update/latest-go-version
+ go-version: "1.27" # auto-update/latest-go-version
fuzz-time: "60s"
diff --git a/.github/workflows/goassets-automerge.yml b/.github/workflows/goassets-automerge.yml
new file mode 100644
index 000000000..94367d46c
--- /dev/null
+++ b/.github/workflows/goassets-automerge.yml
@@ -0,0 +1,96 @@
+#
+# DO NOT EDIT THIS FILE
+#
+# It is automatically copied from https://github.com/pion/.goassets repository.
+# If this repository should have package specific CI config,
+# remove the repository name from .goassets/.github/workflows/assets-sync.yml.
+#
+# If you want to update the shared CI config, send a PR to
+# https://github.com/pion/.goassets instead of this repository.
+#
+# SPDX-FileCopyrightText: 2026 The Pion community
+# SPDX-License-Identifier: MIT
+
+name: Auto-merge .goassets updates
+on:
+ pull_request_target:
+ types: [opened]
+ workflow_run:
+ workflows: [API, CodeQL, Fuzz, Go mod tidy, Lint, REUSE Compliance Check, Test]
+ types: [completed]
+ branches: [sync-assets-*]
+
+permissions:
+ checks: read
+ contents: write
+ pull-requests: write
+ statuses: read
+
+concurrency:
+ group: goassets-automerge-${{ github.event.pull_request.head.ref || github.event.workflow_run.head_branch }}
+ cancel-in-progress: false
+
+jobs:
+ auto-merge:
+ if: >-
+ github.repository_owner == 'pion' &&
+ ((github.event_name == 'pull_request_target' &&
+ github.event.pull_request.user.login == 'pionbot' &&
+ github.event.pull_request.base.ref == 'main' &&
+ github.event.pull_request.head.repo.full_name == github.repository) ||
+ (github.event_name == 'workflow_run' &&
+ github.event.workflow_run.conclusion == 'success' &&
+ github.event.workflow_run.head_repository.full_name == github.repository))
+ runs-on: ubuntu-26.04
+ timeout-minutes: 60
+ steps:
+ - name: Validate .goassets PR
+ id: pr
+ env:
+ GH_TOKEN: ${{ github.token }}
+ REPOSITORY: ${{ github.repository }}
+ HEAD: ${{ github.event.pull_request.head.ref || github.event.workflow_run.head_branch }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha }}
+ run: |
+ if ! [[ "$HEAD" =~ ^sync-assets-v[0-9]+\.[0-9]+\.[0-9]+$ ]] &&
+ ! [[ "$REPOSITORY" == "pion/ci-sandbox" && "$HEAD" == "sync-assets-main" ]]; then
+ exit 0
+ fi
+
+ # Fetch current PR data: reruns may refer to a closed PR or an old commit.
+ gh api --method GET "repos/$REPOSITORY/pulls" \
+ -f state=open -f base=main -f head="${REPOSITORY%%/*}:$HEAD" |
+ jq -r --arg repository "$REPOSITORY" --arg head "$HEAD" --arg sha "$HEAD_SHA" '
+ .[] | select(
+ .user.login == "pionbot" and
+ .state == "open" and
+ .draft == false and
+ .base.ref == "main" and
+ .head.repo.full_name == $repository and
+ .head.ref == $head and
+ .head.sha == $sha and
+ .title == ("Update CI configs to " + ($head | ltrimstr("sync-assets-")))
+ ) | "url=\(.html_url)"
+ ' >> "$GITHUB_OUTPUT"
+
+ - name: Approve
+ if: steps.pr.outputs.url != ''
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PR_URL: ${{ steps.pr.outputs.url }}
+ run: gh pr review "$PR_URL" --approve
+
+ - name: Wait for required checks
+ if: steps.pr.outputs.url != ''
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PR_URL: ${{ steps.pr.outputs.url }}
+ run: gh pr checks "$PR_URL" --required --watch --fail-fast
+
+ - name: Merge
+ if: steps.pr.outputs.url != ''
+ env:
+ GH_TOKEN: ${{ github.token }}
+ PR_URL: ${{ steps.pr.outputs.url }}
+ HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.workflow_run.head_sha }}
+ run: gh pr merge "$PR_URL" --rebase --match-head-commit "$HEAD_SHA"
diff --git a/.github/workflows/interop.yaml b/.github/workflows/interop.yaml
new file mode 100644
index 000000000..2660ec99e
--- /dev/null
+++ b/.github/workflows/interop.yaml
@@ -0,0 +1,26 @@
+# SPDX-FileCopyrightText: 2026 The Pion community
+# SPDX-License-Identifier: MIT
+
+name: Interoperability
+
+on:
+ pull_request:
+ branches:
+ - main
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+concurrency:
+ group: interop-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ interop:
+ uses: pion/dtls-interop/.github/workflows/interop-tests.yaml@main
+ with:
+ dtls-repository: ${{ github.repository }}
+ dtls-ref: ${{ github.event.pull_request.head.sha || github.sha }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7487361c4..60fbe9eb8 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -21,4 +21,4 @@ jobs:
release:
uses: pion/.goassets/.github/workflows/release.reusable.yml@main
with:
- go-version: "1.25" # auto-update/latest-go-version
+ go-version: "1.27" # auto-update/latest-go-version
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 4bf618003..f5e8df647 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -23,7 +23,7 @@ jobs:
uses: pion/.goassets/.github/workflows/test.reusable.yml@main
strategy:
matrix:
- go: ["1.25", "1.24"] # auto-update/supported-go-version-list
+ go: ["1.27", "1.26"] # auto-update/supported-go-version-list
fail-fast: false
with:
go-version: ${{ matrix.go }}
@@ -33,7 +33,7 @@ jobs:
uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@main
strategy:
matrix:
- go: ["1.25", "1.24"] # auto-update/supported-go-version-list
+ go: ["1.27", "1.26"] # auto-update/supported-go-version-list
fail-fast: false
with:
go-version: ${{ matrix.go }}
@@ -42,7 +42,7 @@ jobs:
uses: pion/.goassets/.github/workflows/test-windows.reusable.yml@main
strategy:
matrix:
- go: ["1.25", "1.24"] # auto-update/supported-go-version-list
+ go: ["1.27", "1.26"] # auto-update/supported-go-version-list
fail-fast: false
with:
go-version: ${{ matrix.go }}
@@ -51,7 +51,7 @@ jobs:
uses: pion/.goassets/.github/workflows/test-macos.reusable.yml@main
strategy:
matrix:
- go: ["1.25", "1.24"] # auto-update/supported-go-version-list
+ go: ["1.27", "1.26"] # auto-update/supported-go-version-list
fail-fast: false
with:
go-version: ${{ matrix.go }}
@@ -59,5 +59,5 @@ jobs:
test-wasm:
uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@main
with:
- go-version: "1.25" # auto-update/latest-go-version
+ go-version: "1.27" # auto-update/latest-go-version
secrets: inherit
diff --git a/.github/workflows/tidy-check.yaml b/.github/workflows/tidy-check.yaml
index 347a25cb3..f98323e39 100644
--- a/.github/workflows/tidy-check.yaml
+++ b/.github/workflows/tidy-check.yaml
@@ -22,4 +22,4 @@ jobs:
tidy:
uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@main
with:
- go-version: "1.25" # auto-update/latest-go-version
+ go-version: "1.27" # auto-update/latest-go-version
diff --git a/.golangci.yml b/.golangci.yml
index 6c7ceed69..73a0ce0f0 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -23,10 +23,8 @@ linters:
- forbidigo # Forbids identifiers
- forcetypeassert # finds forced type assertions
- gochecknoglobals # Checks that no globals are present in Go code
- - gocognit # Computes and checks the cognitive complexity of functions
- goconst # Finds repeated strings that could be replaced by a constant
- gocritic # The most opinionated Go source code linter
- - gocyclo # Computes and checks the cyclomatic complexity of functions
- godot # Check if comments end in a period
- godox # Tool for detection of FIXME, TODO and other comment keywords
- goheader # Checks is file header matches to pattern
@@ -38,7 +36,6 @@ linters:
- importas # Enforces consistent import aliases
- ineffassign # Detects when assignments to existing variables are not used
- lll # Reports long lines
- - maintidx # maintidx measures the maintainability index of each function.
- makezero # Finds slice declarations with non-zero initial length
- misspell # Finds commonly misspelled English words in comments
- modernize # Replace and suggests simplifications to code
@@ -78,6 +75,8 @@ linters:
- wrapcheck # Checks that errors returned from external packages are wrapped
- wsl # Whitespace Linter - Forces you to use empty lines!
settings:
+ lll:
+ line-length: 320
staticcheck:
checks:
- all
@@ -105,12 +104,19 @@ linters:
govet:
enable:
- shadow
+ gosec:
+ excludes:
+ - G117 # noisy secret/password field-name checks.
revive:
rules:
# Prefer 'any' type alias over 'interface{}' for Go 1.18+ compatibility
- name: use-any
severity: warning
disabled: false
+ # Keep package documentation consistent and avoid duplicate package comments.
+ - name: package-comments
+ severity: warning
+ disabled: false
misspell:
locale: US
varnamelen:
@@ -130,11 +136,7 @@ linters:
rules:
- linters:
- forbidigo
- - gocognit
path: (examples|main\.go)
- - linters:
- - gocognit
- path: _test\.go
- linters:
- forbidigo
path: cmd
@@ -147,5 +149,5 @@ formatters:
exclusions:
generated: lax
issues:
+ max-same-issues: 0
max-issues-per-linter: 0
- max-same-issues: 0
\ No newline at end of file
diff --git a/README.md b/README.md
index 041d67a1c..155a9fdf3 100644
--- a/README.md
+++ b/README.md
@@ -10,25 +10,25 @@
-
+
-
+
-Native [DTLS 1.2][rfc6347] implementation in the Go programming language.
+Native [DTLS 1.2][rfc6347] and [DTLS 1.3][rfc9147] implementation in the Go
+programming language.
A long term goal is a professional security review, and maybe an inclusion in stdlib.
-> [!WARNING]
-> This branch is currently frozen from tagging while DTLS 1.3 work is underway.
->
-> DTLS 1.3 and breaking changes should target the `main` branch. Bug fixes and DTLS 1.2 improvments should target the `v3` branch, which can be used for tags until DTLS 1.3 is ready.
-
### RFCs
#### Implemented
- **RFC 6347**: [Datagram Transport Layer Security Version 1.2][rfc6347]
+- **RFC 9146**: [Connection Identifier for DTLS 1.2][rfc9146]
+- **RFC 9147**: [The Datagram Transport Layer Security (DTLS) Protocol Version 1.3][rfc9147]
+- **RFC 9849**: [TLS Encrypted Client Hello][rfc9849]
+- **RFC 9853**: [Return Routability Check for DTLS 1.2 and 1.3][rfc9853]
- **RFC 5705**: [Keying Material Exporters for Transport Layer Security (TLS)][rfc5705]
- **RFC 7627**: [Transport Layer Security (TLS) - Session Hash and Extended Master Secret Extension][rfc7627]
- **RFC 7301**: [Transport Layer Security (TLS) - Application-Layer Protocol Negotiation Extension][rfc7301]
@@ -43,24 +43,44 @@ A long term goal is a professional security review, and maybe an inclusion in st
[rfc7627]: https://tools.ietf.org/html/rfc7627
[rfc7905]: https://tools.ietf.org/html/rfc7905
[rfc8422]: https://tools.ietf.org/html/rfc8422
+[rfc8446]: https://tools.ietf.org/html/rfc8446
+[rfc9146]: https://tools.ietf.org/html/rfc9146
[rfc9147]: https://tools.ietf.org/html/rfc9147
+[rfc9849]: https://tools.ietf.org/html/rfc9849
+[rfc9853]: https://tools.ietf.org/html/rfc9853
+
+### Features
-### Goals/Progress
-This will only be targeting DTLS 1.2, and the most modern/common cipher suites.
-We would love contributions that fall under the 'Planned Features' and any bug fixes!
+Pion DTLS supports DTLS 1.2 and DTLS 1.3. We welcome contributions and bug fixes.
#### Current features
-* DTLS 1.2 Client/Server
+
+* DTLS 1.2 and DTLS 1.3 client/server
* Key Exchange via ECDHE(curve25519, nistp256, nistp384) and PSK
+* DTLS 1.3 external PSK with `psk_ke` and `psk_dhe_ke`, supporting SHA-256 and SHA-384
+* DTLS 1.3 offers multiple PSK identities per handshake for server selection
* Packet loss and re-ordering is handled during handshaking
* Key export ([RFC 5705][rfc5705])
-* Serialization and Resumption of sessions
+* DTLS 1.2 and DTLS 1.3 connection state serialization and restoration with `Resume`
+* DTLS 1.3 session ticket resumption and optional 0-RTT early data
+* DTLS 1.3 Encrypted Client Hello (ECH), configured with `WithECH` ([RFC 9849][rfc9849])
* Extended Master Secret extension ([RFC 7627][rfc7627])
* ALPN extension ([RFC 7301][rfc7301])
+* Connection IDs (CIDs) for DTLS 1.2 and DTLS 1.3, including listener routing across peer address changes
+* DTLS 1.3 post-handshake CID updates and spare CIDs for path migration
+* Return Routability Check (RRC) for DTLS 1.2 and DTLS 1.3 ([RFC 9853][rfc9853])
+* DTLS 1.3 local path probing and migration with `Conn.AddPath`, `Path.Probe`, and `Path.Switch`
+* DTLS 1.3 traffic-key updates with `Conn.UpdateKeys`
#### Supported ciphers
-##### ECDHE
+##### DTLS 1.3: Certificate & PSK
+
+* TLS_AES_128_GCM_SHA256 ([RFC 8446][rfc8446])
+* TLS_AES_256_GCM_SHA384 ([RFC 8446][rfc8446])
+* TLS_CHACHA20_POLY1305_SHA256 ([RFC 8446][rfc8446])
+
+##### DTLS 1.2: ECDHE
* TLS_ECDHE_ECDSA_WITH_AES_128_CCM ([RFC 6655][rfc6655])
* TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 ([RFC 6655][rfc6655])
@@ -73,7 +93,7 @@ We would love contributions that fall under the 'Planned Features' and any bug f
* TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 ([RFC 7905][rfc7905])
* TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 ([RFC 7905][rfc7905])
-##### PSK
+##### DTLS 1.2: PSK
* TLS_PSK_WITH_AES_128_CCM ([RFC 6655][rfc6655])
* TLS_PSK_WITH_AES_128_CCM_8 ([RFC 6655][rfc6655])
@@ -82,23 +102,16 @@ We would love contributions that fall under the 'Planned Features' and any bug f
* TLS_PSK_WITH_AES_128_CBC_SHA256 ([RFC 5487][rfc5487])
* TLS_PSK_WITH_CHACHA20_POLY1305_SHA256 ([RFC 7905][rfc7905])
-##### ECDHE & PSK
+##### DTLS 1.2: ECDHE & PSK
* TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 ([RFC 5489][rfc5489])
-
-#### Planned Features
-* DTLS 1.3 ([RFC 9147][rfc9147])
+* TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256 ([RFC 7905][rfc7905])
#### Excluded Features
* DTLS 1.0
* Renegotiation
* Compression
-### Using
-
-This library needs at least Go 1.21, and you should have [Go modules
-enabled](https://github.com/golang/go/wiki/Modules).
-
#### Pion DTLS
For a DTLS 1.2 Server that listens on 127.0.0.1:4444
```sh
@@ -125,8 +138,25 @@ Pion DTLS can connect to itself and OpenSSL.
openssl s_client -dtls1_2 -connect 127.0.0.1:4444 -debug -cert cert.pem -key key.pem
```
+### DTLS 1.3 resumption and early data
+
+DTLS 1.3 supports session ticket resumption with optional 0-RTT early data.
+Resumption only allows `psk_dhe_ke` (PSK with ephemeral ECDHE).
+
+The [resumption example](examples/resumption-dtls13) demonstrate a certificate-authenticated
+first connection, encrypted session storage, and subsequent connections with resumption
+and optional early data. See its README for run instructions and the session store's
+single-use `Claim` requirement for 0-RTT.
+
+The `State`/`Resume` API also supports restoring established DTLS 1.3
+connections without a new handshake. See [`Resume`](resume.go).
### Using with PSK
-Pion DTLS also comes with examples that do key exchange via PSK
+
+DTLS 1.3 supports external PSKs with `psk_ke` (PSK-only) and `psk_dhe_ke`
+(PSK with ephemeral ECDHE), using SHA-256 or SHA-384. Clients can offer multiple
+PSK identities per handshake for the server to select from.
+
+Pion DTLS also comes with examples that do key exchange via DTLS 1.2 PSK.
#### Pion DTLS
```sh
diff --git a/bench_test.go b/bench_test.go
index 0e6f3297e..99847095b 100644
--- a/bench_test.go
+++ b/bench_test.go
@@ -7,73 +7,24 @@ import (
"context"
"fmt"
"testing"
- "time"
- "github.com/pion/dtls/v3/pkg/crypto/selfsign"
- dtlsnet "github.com/pion/dtls/v3/pkg/net"
- "github.com/pion/logging"
- "github.com/pion/transport/v4/dpipe"
- "github.com/pion/transport/v4/test"
+ "github.com/pion/dtls/v4/pkg/crypto/selfsign"
"github.com/stretchr/testify/assert"
)
-func TestSimpleReadWrite(t *testing.T) {
- report := test.CheckRoutines(t)
- defer report()
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- ca, cb := dpipe.Pipe()
- certificate, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
- gotHello := make(chan struct{})
-
- go func() {
- server, sErr := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), []ServerOption{
- WithCertificates(certificate),
- WithLoggerFactory(logging.NewDefaultLoggerFactory()),
- }, false)
- assert.NoError(t, sErr)
-
- buf := make([]byte, 1024)
- _, sErr = server.Read(buf) //nolint:contextcheck
- assert.NoError(t, sErr)
-
- gotHello <- struct{}{}
- assert.NoError(t, server.Close()) //nolint:contextcheck
- }()
-
- client, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), []ClientOption{
- WithLoggerFactory(logging.NewDefaultLoggerFactory()),
- WithInsecureSkipVerify(true),
- }, false)
- assert.NoError(t, err)
- _, err = client.Write([]byte("hello"))
- assert.NoError(t, err)
- select {
- case <-gotHello:
- // OK
- case <-time.After(time.Second * 5):
- assert.Fail(t, "timeout")
- }
- assert.NoError(t, client.Close())
-}
-
func benchmarkConn(b *testing.B, payloadSize int64) {
b.Helper()
b.Run(fmt.Sprintf("%d", payloadSize), func(b *testing.B) {
ctx := context.Background()
- ca, cb := dpipe.Pipe()
+ ca, cb := packetPipe()
certificate, err := selfsign.GenerateSelfSigned()
assert.NoError(b, err)
server := make(chan *Conn)
go func() {
- s, sErr := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), []ServerOption{
- WithCertificates(certificate),
- }, false)
+ s, sErr := testServer(ctx, cb, cb.RemoteAddr(), []ServerOption{WithCertificates(certificate)}, false)
assert.NoError(b, sErr)
server <- s
@@ -83,9 +34,7 @@ func benchmarkConn(b *testing.B, payloadSize int64) {
b.ReportAllocs()
b.SetBytes(int64(len(hw)))
go func() {
- client, cErr := testClient(
- ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), []ClientOption{WithInsecureSkipVerify(true)}, false,
- )
+ client, cErr := testClient(ctx, ca, ca.RemoteAddr(), []ClientOption{WithInsecureSkipVerify(true)}, false)
assert.NoError(b, cErr)
for {
_, cErr = client.Write(hw) //nolint:contextcheck
diff --git a/certificate.go b/certificate.go
index 0dce71287..d4e38156a 100644
--- a/certificate.go
+++ b/certificate.go
@@ -4,18 +4,25 @@
package dtls
import (
- "bytes"
+ "context"
"crypto/tls"
- "crypto/x509"
- "fmt"
- "strings"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
+ "net"
+ "slices"
+
+ dtlsconfig "github.com/pion/dtls/v4/internal/config"
+ cryptosuite "github.com/pion/dtls/v4/pkg/crypto/ciphersuite"
+ "github.com/pion/dtls/v4/pkg/crypto/clientcertificate"
+ "github.com/pion/dtls/v4/pkg/crypto/elliptic"
+ "github.com/pion/dtls/v4/pkg/crypto/signaturehash"
+ "github.com/pion/dtls/v4/pkg/protocol"
+ "github.com/pion/dtls/v4/pkg/protocol/extension"
+ extension12 "github.com/pion/dtls/v4/pkg/protocol/extension/dtls12"
+ extension13 "github.com/pion/dtls/v4/pkg/protocol/extension/dtls13"
+ "github.com/pion/dtls/v4/pkg/protocol/handshake"
)
// ClientHelloInfo contains information from a ClientHello message in order to
-// guide application logic in the GetCertificate.
+// guide certificate and ECH key selection.
type ClientHelloInfo struct {
// ServerName indicates the name of the server requested by the client
// in order to support virtual hosting. ServerName is only set if the
@@ -24,145 +31,105 @@ type ClientHelloInfo struct {
// CipherSuites lists the CipherSuites supported by the client (e.g.
// TLS_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256).
- CipherSuites []CipherSuiteID
+ CipherSuites []cryptosuite.ID
// RandomBytes stores the client hello random bytes
RandomBytes [handshake.RandomBytesLength]byte
-}
-// CertificateRequestInfo contains information from a server's
-// CertificateRequest message, which is used to demand a certificate and proof
-// of control from a client.
-type CertificateRequestInfo struct {
- // AcceptableCAs contains zero or more, DER-encoded, X.501
- // Distinguished Names. These are the names of root or intermediate CAs
- // that the server wishes the returned certificate to be signed by. An
- // empty slice indicates that the server has no preference.
- AcceptableCAs [][]byte
+ // The following offer fields are populated for WithECHGetKeys.
+ SupportedCurves []elliptic.Curve
+ SupportedPoints []elliptic.CurvePointFormat
+ SignatureSchemes []tls.SignatureScheme
+ SupportedProtos []string
+ SupportedVersions []protocol.Version
+ Extensions []extension.Type
+ HelloRetryRequest bool
+ // Conn identifies the DTLS connection. Do not read or write through it
+ // while the handshake callback is running.
+ Conn net.Conn
+ context func() context.Context
}
-// SupportsCertificate returns nil if the provided certificate is supported by
-// the server that sent the CertificateRequest. Otherwise, it returns an error
-// describing the reason for the incompatibility.
-// NOTE: original src:
-// https://github.com/golang/go/blob/29b9a328d268d53833d2cc063d1d8b4bf6852675/src/crypto/tls/common.go#L1273
-func (cri *CertificateRequestInfo) SupportsCertificate(c *tls.Certificate) error {
- if len(cri.AcceptableCAs) == 0 {
+// Context returns the handshake context for ECH key selection. It is canceled
+// when the handshake ends. Other callback paths currently return nil.
+func (c *ClientHelloInfo) Context() context.Context {
+ if c.context == nil {
return nil
}
- for j, cert := range c.Certificate {
- x509Cert := c.Leaf
- // Parse the certificate if this isn't the leaf node, or if
- // chain.Leaf was nil.
- if j != 0 || x509Cert == nil {
- var err error
- if x509Cert, err = x509.ParseCertificate(cert); err != nil {
- return fmt.Errorf("failed to parse certificate #%d in the chain: %w", j, err)
- }
- }
-
- for _, ca := range cri.AcceptableCAs {
- if bytes.Equal(x509Cert.RawIssuer, ca) {
- return nil
- }
- }
- }
-
- return dtlserrors.ErrNotAcceptableCertificateChain
+ return c.context()
}
-func (c *handshakeConfig) setNameToCertificateLocked() {
- nameToCertificate := make(map[string]*tls.Certificate)
- for i := range c.localCertificates {
- cert := &c.localCertificates[i]
- x509Cert := cert.Leaf
- if x509Cert == nil {
- var parseErr error
- x509Cert, parseErr = x509.ParseCertificate(cert.Certificate[0])
- if parseErr != nil {
- continue
- }
- }
- if len(x509Cert.Subject.CommonName) > 0 {
- nameToCertificate[strings.ToLower(x509Cert.Subject.CommonName)] = cert
- }
- for _, san := range x509Cert.DNSNames {
- nameToCertificate[strings.ToLower(san)] = cert
- }
+func newECHClientHelloInfo(info *dtlsconfig.ClientHelloInfo) *ClientHelloInfo {
+ hello := info.Hello
+ result := &ClientHelloInfo{RandomBytes: hello.Random.RandomBytes, Conn: info.Conn, context: info.Context, HelloRetryRequest: info.HelloRetryRequest}
+ for _, id := range hello.CipherSuiteIDs {
+ result.CipherSuites = append(result.CipherSuites, cryptosuite.ID(id))
}
- c.nameToCertificate = nameToCertificate
-}
-
-//nolint:cyclop
-func (c *handshakeConfig) getCertificate(clientHelloInfo *ClientHelloInfo) (*tls.Certificate, error) {
- c.mu.Lock()
- defer c.mu.Unlock()
-
- if c.localGetCertificate != nil &&
- (len(c.localCertificates) == 0 || len(clientHelloInfo.ServerName) > 0) {
- cert, err := c.localGetCertificate(clientHelloInfo)
- if cert != nil || err != nil {
- return cert, err
- }
+ for _, ext := range hello.Extensions {
+ result.Extensions = append(result.Extensions, ext.ExtensionType())
+ result.addECHOfferExtension(ext)
}
-
- if c.nameToCertificate == nil {
- c.setNameToCertificateLocked()
- }
-
- if len(c.localCertificates) == 0 {
- return nil, dtlserrors.ErrNoCertificates
+ if len(result.SupportedVersions) == 0 {
+ result.SupportedVersions = dtlsconfig.SupportedVersionsRange(protocol.Version1_2, hello.Version)
}
- if len(c.localCertificates) == 1 {
- // There's only one choice, so no point doing any work.
- return &c.localCertificates[0], nil
- }
+ return result
+}
- if len(clientHelloInfo.ServerName) == 0 {
- return &c.localCertificates[0], nil
+func (c *ClientHelloInfo) addECHOfferExtension(ext extension.Value) {
+ switch ext := ext.(type) {
+ case *extension.ServerNameOffer:
+ c.ServerName = ext.ServerName
+ case *extension.SupportedGroups:
+ c.SupportedCurves = slices.Clone(ext.Groups)
+ case *extension12.SupportedPointFormats:
+ c.SupportedPoints = slices.Clone(ext.PointFormats)
+ case *extension.SignatureAlgorithms:
+ for _, scheme := range ext.Schemes {
+ c.SignatureSchemes = append(c.SignatureSchemes, tls.SignatureScheme(scheme))
+ }
+ case *extension.ALPNOffer:
+ c.SupportedProtos = slices.Clone(ext.Protocols)
+ case *extension13.OfferedVersions:
+ c.SupportedVersions = slices.Clone(ext.Versions)
+ default:
}
+}
- name := strings.TrimRight(strings.ToLower(clientHelloInfo.ServerName), ".")
-
- if cert, ok := c.nameToCertificate[name]; ok {
- return cert, nil
- }
+// CertificateRequestInfo contains information from a server's
+// CertificateRequest message, which is used to demand a certificate and proof
+// of control from a client.
+type CertificateRequestInfo struct {
+ // CertificateTypes lists the certificate types accepted by the server.
+ // A nil slice indicates no restriction (DTLS 1.3).
+ CertificateTypes []clientcertificate.Type
- // try replacing labels in the name with wildcards until we get a
- // match.
- labels := strings.Split(name, ".")
- for i := range labels {
- labels[i] = "*"
- candidate := strings.Join(labels, ".")
- if cert, ok := c.nameToCertificate[candidate]; ok {
- return cert, nil
- }
- }
+ // AcceptableCAs contains zero or more, DER-encoded, X.501
+ // Distinguished Names. These are the names of root or intermediate CAs
+ // that the server wishes the returned certificate to be signed by. An
+ // empty slice indicates that the server has no preference.
+ AcceptableCAs [][]byte
- // If nothing matches, return the first certificate.
- return &c.localCertificates[0], nil
+ // SignatureSchemes lists the signature schemes that the server is
+ // willing to verify.
+ SignatureSchemes []tls.SignatureScheme
}
+// SupportsCertificate returns nil if the provided certificate is supported by
+// the server that sent the CertificateRequest. Otherwise, it returns an error
+// describing the reason for the incompatibility.
// NOTE: original src:
-// https://github.com/golang/go/blob/29b9a328d268d53833d2cc063d1d8b4bf6852675/src/crypto/tls/handshake_client.go#L974
-func (c *handshakeConfig) getClientCertificate(cri *CertificateRequestInfo) (*tls.Certificate, error) {
- c.mu.Lock()
- defer c.mu.Unlock()
- if c.localGetClientCertificate != nil {
- return c.localGetClientCertificate(cri)
- }
-
- for i := range c.localCertificates {
- chain := c.localCertificates[i]
- if err := cri.SupportsCertificate(&chain); err != nil {
- continue
+// https://github.com/golang/go/blob/29b9a328d268d53833d2cc063d1d8b4bf6852675/src/crypto/tls/common.go#L1273
+func (cri *CertificateRequestInfo) SupportsCertificate(certificate *tls.Certificate) error {
+ var signatureSchemes []signaturehash.Algorithm
+ if len(cri.SignatureSchemes) > 0 {
+ var err error
+ signatureSchemes, err = signaturehash.ParseSignatureSchemes(cri.SignatureSchemes, true)
+ if err != nil {
+ return err
}
-
- return &chain, nil
}
- // No acceptable certificate found. Don't send a certificate.
- return new(tls.Certificate), nil
+ return (&dtlsconfig.CertificateRequestInfo{CertificateTypes: cri.CertificateTypes, AcceptableCAs: cri.AcceptableCAs, SignatureSchemes: signatureSchemes, Version: protocol.Version1_3}).SupportsCertificate(certificate)
}
diff --git a/certificate_test.go b/certificate_test.go
index 8c294d8a0..ec0cf610d 100644
--- a/certificate_test.go
+++ b/certificate_test.go
@@ -7,7 +7,8 @@ import (
"crypto/tls"
"testing"
- "github.com/pion/dtls/v3/pkg/crypto/selfsign"
+ dtlsconfig "github.com/pion/dtls/v4/internal/config"
+ "github.com/pion/dtls/v4/pkg/crypto/selfsign"
"github.com/stretchr/testify/assert"
)
@@ -28,63 +29,28 @@ func TestGetCertificate(t *testing.T) {
expectedCertificate tls.Certificate
getCertificate func(info *ClientHelloInfo) (*tls.Certificate, error)
}{
- {
- desc: "Simple match in CN",
- localCertificates: []tls.Certificate{
- certificateRandom,
- certificateTest,
- certificateWildcard,
- },
- serverName: "test.test",
- expectedCertificate: certificateTest,
- },
- {
- desc: "Simple match in SANs",
- localCertificates: []tls.Certificate{
- certificateRandom,
- certificateTest,
- certificateWildcard,
- },
- serverName: "www.test.test",
- expectedCertificate: certificateTest,
- },
+ {desc: "Simple match in CN", localCertificates: []tls.Certificate{certificateRandom, certificateTest, certificateWildcard}, serverName: "test.test", expectedCertificate: certificateTest},
+ {desc: "Simple match in SANs", localCertificates: []tls.Certificate{certificateRandom, certificateTest, certificateWildcard}, serverName: "www.test.test", expectedCertificate: certificateTest},
- {
- desc: "Wildcard match",
- localCertificates: []tls.Certificate{
- certificateRandom,
- certificateTest,
- certificateWildcard,
- },
- serverName: "foo.test.test",
- expectedCertificate: certificateWildcard,
- },
- {
- desc: "No match return first",
- localCertificates: []tls.Certificate{
- certificateRandom,
- certificateTest,
- certificateWildcard,
- },
- serverName: "foo.bar",
- expectedCertificate: certificateRandom,
- },
- {
- desc: "Get certificate from callback",
- getCertificate: func(*ClientHelloInfo) (*tls.Certificate, error) {
- return &certificateTest, nil
- },
- expectedCertificate: certificateTest,
- },
+ {desc: "Wildcard match", localCertificates: []tls.Certificate{certificateRandom, certificateTest, certificateWildcard}, serverName: "foo.test.test", expectedCertificate: certificateWildcard},
+ {desc: "No match return first", localCertificates: []tls.Certificate{certificateRandom, certificateTest, certificateWildcard}, serverName: "foo.bar", expectedCertificate: certificateRandom},
+ {desc: "Get certificate from callback", getCertificate: func(*ClientHelloInfo) (*tls.Certificate, error) { return &certificateTest, nil }, expectedCertificate: certificateTest},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
- cfg := &handshakeConfig{
- localCertificates: test.localCertificates,
- localGetCertificate: test.getCertificate,
+ getCertificate := func(info *dtlsconfig.ClientHelloInfo) (*tls.Certificate, error) {
+ return test.getCertificate(&ClientHelloInfo{ServerName: info.ServerName, CipherSuites: info.CipherSuites, RandomBytes: info.RandomBytes})
}
- cert, err := cfg.getCertificate(&ClientHelloInfo{ServerName: test.serverName})
+ if test.getCertificate == nil {
+ getCertificate = nil
+ }
+
+ cfg := &dtlsconfig.HandshakeConfig{
+ LocalCertificates: test.localCertificates,
+ LocalGetCertificate: getCertificate,
+ }
+ cert, err := cfg.GetCertificate(&dtlsconfig.ClientHelloInfo{ServerName: test.serverName})
assert.NoError(t, err)
assert.Equal(t, test.expectedCertificate.Leaf, cert.Leaf, "Certificate Leaf should match expected")
})
diff --git a/cipher_suite.go b/cipher_suite.go
deleted file mode 100644
index 9e85d6328..000000000
--- a/cipher_suite.go
+++ /dev/null
@@ -1,498 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "crypto"
- "crypto/ecdsa"
- "crypto/ed25519"
- "crypto/rsa"
- "crypto/tls"
- "fmt"
- "hash"
- "slices"
-
- "github.com/pion/dtls/v3/internal/ciphersuite"
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- "github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
-)
-
-// CipherSuiteID is an ID for our supported CipherSuites.
-type CipherSuiteID = ciphersuite.ID
-
-// Supported Cipher Suites.
-const (
- // TLS 1.3.
- TLS_AES_128_GCM_SHA256 CipherSuiteID = ciphersuite.TLS_AES_128_GCM_SHA256 // nolint: revive,staticcheck,lll
- TLS_AES_256_GCM_SHA384 CipherSuiteID = ciphersuite.TLS_AES_256_GCM_SHA384 // nolint: revive,staticcheck,lll
- TLS_CHACHA20_POLY1305_SHA256 CipherSuiteID = ciphersuite.TLS_CHACHA20_POLY1305_SHA256 // nolint: revive,staticcheck,lll
-
- // nolint: godot
- // AES-128-CCM
- TLS_ECDHE_ECDSA_WITH_AES_128_CCM CipherSuiteID = ciphersuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM // nolint: revive,staticcheck,lll
- TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 CipherSuiteID = ciphersuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 // nolint: revive,staticcheck,lll
-
- // nolint: godot
- // AES-128-GCM-SHA256
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 CipherSuiteID = ciphersuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 // nolint: revive,staticcheck,lll
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 CipherSuiteID = ciphersuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 // nolint: revive,staticcheck,lll
-
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 CipherSuiteID = ciphersuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 // nolint: revive,staticcheck,lll
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 CipherSuiteID = ciphersuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 // nolint: revive,staticcheck,lll
-
- // nolint: godot
- // AES-256-CBC-SHA
- TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA CipherSuiteID = ciphersuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA // nolint: revive,staticcheck,lll
- TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA CipherSuiteID = ciphersuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA // nolint: revive,staticcheck,lll
-
- TLS_PSK_WITH_AES_128_CCM CipherSuiteID = ciphersuite.TLS_PSK_WITH_AES_128_CCM // nolint: revive,staticcheck,lll
- TLS_PSK_WITH_AES_128_CCM_8 CipherSuiteID = ciphersuite.TLS_PSK_WITH_AES_128_CCM_8 // nolint: revive,staticcheck,lll
- TLS_PSK_WITH_AES_256_CCM_8 CipherSuiteID = ciphersuite.TLS_PSK_WITH_AES_256_CCM_8 // nolint: revive,staticcheck,lll
- TLS_PSK_WITH_AES_128_GCM_SHA256 CipherSuiteID = ciphersuite.TLS_PSK_WITH_AES_128_GCM_SHA256 // nolint: revive,staticcheck,lll
- TLS_PSK_WITH_AES_128_CBC_SHA256 CipherSuiteID = ciphersuite.TLS_PSK_WITH_AES_128_CBC_SHA256 // nolint: revive,staticcheck,lll
-
- TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 CipherSuiteID = ciphersuite.TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 // nolint: revive,staticcheck,lll
-
- // nolint: godot
- // ChaCha20-Poly1305-SHA256
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 CipherSuiteID = ciphersuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 // nolint: revive,staticcheck,lll
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 CipherSuiteID = ciphersuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 // nolint: revive,staticcheck,lll
- TLS_PSK_WITH_CHACHA20_POLY1305_SHA256 CipherSuiteID = ciphersuite.TLS_PSK_WITH_CHACHA20_POLY1305_SHA256 // nolint: revive,staticcheck,lll
-)
-
-// CipherSuiteAuthenticationType controls what authentication method is using during the handshake for a CipherSuite.
-type CipherSuiteAuthenticationType = ciphersuite.AuthenticationType
-
-// AuthenticationType Enums.
-const (
- CipherSuiteAuthenticationTypeCertificate CipherSuiteAuthenticationType = ciphersuite.AuthenticationTypeCertificate
- CipherSuiteAuthenticationTypePreSharedKey CipherSuiteAuthenticationType = ciphersuite.AuthenticationTypePreSharedKey
- CipherSuiteAuthenticationTypeAnonymous CipherSuiteAuthenticationType = ciphersuite.AuthenticationTypeAnonymous
-)
-
-// CipherSuiteKeyExchangeAlgorithm controls what exchange algorithm is using during the handshake for a CipherSuite.
-type CipherSuiteKeyExchangeAlgorithm = ciphersuite.KeyExchangeAlgorithm
-
-// CipherSuiteKeyExchangeAlgorithm Bitmask.
-const (
- CipherSuiteKeyExchangeAlgorithmNone CipherSuiteKeyExchangeAlgorithm = ciphersuite.KeyExchangeAlgorithmNone
- CipherSuiteKeyExchangeAlgorithmPsk CipherSuiteKeyExchangeAlgorithm = ciphersuite.KeyExchangeAlgorithmPsk
- CipherSuiteKeyExchangeAlgorithmEcdhe CipherSuiteKeyExchangeAlgorithm = ciphersuite.KeyExchangeAlgorithmEcdhe
-)
-
-// CipherSuite is an interface that all DTLS CipherSuites must satisfy.
-type CipherSuite interface {
- // String of CipherSuite, only used for logging
- String() string
-
- // ID of CipherSuite.
- ID() CipherSuiteID
-
- // What type of Certificate does this CipherSuite use
- CertificateType() clientcertificate.Type
-
- // What Hash function is used during verification
- HashFunc() func() hash.Hash
-
- // AuthenticationType controls what authentication method is using during the handshake
- AuthenticationType() CipherSuiteAuthenticationType
-
- // KeyExchangeAlgorithm controls what exchange algorithm is using during the handshake
- KeyExchangeAlgorithm() CipherSuiteKeyExchangeAlgorithm
-
- // ECC (Elliptic Curve Cryptography) determines whether ECC extesions will be send during handshake.
- // https://datatracker.ietf.org/doc/html/rfc4492#page-10
- ECC() bool
-
- // Called when keying material has been generated, should initialize the internal cipher
- Init(masterSecret, clientRandom, serverRandom []byte, isClient bool) error
- IsInitialized() bool
- Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error)
- Decrypt(h recordlayer.Header, in []byte) ([]byte, error)
-}
-
-// VersionDTLS12 is the DTLS version in the same style as VersionTLSXX from crypto/tls.
-const VersionDTLS12 = 0xfefd
-
-// VersionDTLS13 is the DTLS version in the same style as VersionTLSXX from crypto/tls.
-const VersionDTLS13 = 0xfefc
-
-// CipherSuiteName provides the same functionality as tls.CipherSuiteName
-// that appeared first in Go 1.14.
-//
-// Our implementation differs slightly in that it takes in a CipherSuiteID,
-// like the rest of our library, instead of a uint16 like crypto/tls.
-func CipherSuiteName(id CipherSuiteID) string {
- suite := cipherSuiteForID(id, nil)
- if suite != nil {
- return suite.String()
- }
-
- return fmt.Sprintf("0x%04X", uint16(id))
-}
-
-// Convert from our cipherSuite interface to a tls.CipherSuite struct.
-func toTLSCipherSuite(c CipherSuite) *tls.CipherSuite {
- return &tls.CipherSuite{
- ID: uint16(c.ID()),
- Name: c.String(),
- SupportedVersions: cipherSuiteSupportedVersionIDs(c.ID()),
- Insecure: false,
- }
-}
-
-// CipherSuites returns a list of cipher suites currently known by this
-// package, excluding those with security issues, which are returned by
-// InsecureCipherSuites.
-func CipherSuites() []*tls.CipherSuite {
- suites := allCipherSuites()
- res := make([]*tls.CipherSuite, len(suites))
- for i, c := range suites {
- res[i] = toTLSCipherSuite(c)
- }
-
- return res
-}
-
-// InsecureCipherSuites returns a list of cipher suites currently implemented by
-// this package and which have security issues.
-func InsecureCipherSuites() []*tls.CipherSuite {
- var res []*tls.CipherSuite
-
- return res
-}
-
-// Taken from https://www.iana.org/assignments/tls-parameters/tls-parameters.xml
-// A cipherSuite is a specific combination of key agreement, cipher and MAC
-// function.
-func cipherSuiteForID(id CipherSuiteID, customCiphers func() []CipherSuite) CipherSuite { //nolint:cyclop
- switch id { //nolint:exhaustive
- case TLS_AES_128_GCM_SHA256:
- return ciphersuite.NewTLSAes128GcmSha256()
- case TLS_AES_256_GCM_SHA384:
- return ciphersuite.NewTLSAes256GcmSha384()
- case TLS_CHACHA20_POLY1305_SHA256:
- return ciphersuite.NewTLSChacha20Poly1305Sha256()
- case TLS_ECDHE_ECDSA_WITH_AES_128_CCM:
- return ciphersuite.NewTLSEcdheEcdsaWithAes128Ccm()
- case TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8:
- return ciphersuite.NewTLSEcdheEcdsaWithAes128Ccm8()
- case TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
- return &ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256{}
- case TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
- return &ciphersuite.TLSEcdheRsaWithAes128GcmSha256{}
- case TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:
- return &ciphersuite.TLSEcdheEcdsaWithAes256CbcSha{}
- case TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:
- return &ciphersuite.TLSEcdheRsaWithAes256CbcSha{}
- case TLS_PSK_WITH_AES_128_CCM:
- return ciphersuite.NewTLSPskWithAes128Ccm()
- case TLS_PSK_WITH_AES_128_CCM_8:
- return ciphersuite.NewTLSPskWithAes128Ccm8()
- case TLS_PSK_WITH_AES_256_CCM_8:
- return ciphersuite.NewTLSPskWithAes256Ccm8()
- case TLS_PSK_WITH_AES_128_GCM_SHA256:
- return &ciphersuite.TLSPskWithAes128GcmSha256{}
- case TLS_PSK_WITH_AES_128_CBC_SHA256:
- return &ciphersuite.TLSPskWithAes128CbcSha256{}
- case TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
- return &ciphersuite.TLSEcdheEcdsaWithAes256GcmSha384{}
- case TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:
- return &ciphersuite.TLSEcdheRsaWithAes256GcmSha384{}
- case TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256:
- return ciphersuite.NewTLSEcdhePskWithAes128CbcSha256()
- case TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
- return &ciphersuite.TLSEcdheEcdsaWithChacha20Poly1305Sha256{}
- case TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
- return &ciphersuite.TLSEcdheRsaWithChacha20Poly1305Sha256{}
- case TLS_PSK_WITH_CHACHA20_POLY1305_SHA256:
- return &ciphersuite.TLSPskWithChacha20Poly1305Sha256{}
- }
-
- if customCiphers != nil {
- for _, c := range customCiphers() {
- if c.ID() == id {
- return c
- }
- }
- }
-
- return nil
-}
-
-// TLS 1.3 CipherSuites we support in order of preference.
-func defaultCipherSuites13() []CipherSuite {
- return []CipherSuite{
- ciphersuite.NewTLSAes128GcmSha256(),
- ciphersuite.NewTLSAes256GcmSha384(),
- ciphersuite.NewTLSChacha20Poly1305Sha256(),
- }
-}
-
-// CipherSuites we support in order of preference.
-func defaultCipherSuites() []CipherSuite {
- return []CipherSuite{
- &ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256{},
- &ciphersuite.TLSEcdheRsaWithAes128GcmSha256{},
- &ciphersuite.TLSEcdheEcdsaWithChacha20Poly1305Sha256{},
- &ciphersuite.TLSEcdheRsaWithChacha20Poly1305Sha256{},
- &ciphersuite.TLSEcdheEcdsaWithAes256CbcSha{},
- &ciphersuite.TLSEcdheRsaWithAes256CbcSha{},
- &ciphersuite.TLSEcdheEcdsaWithAes256GcmSha384{},
- &ciphersuite.TLSEcdheRsaWithAes256GcmSha384{},
- }
-}
-
-func allCipherSuites() []CipherSuite {
- return []CipherSuite{
- ciphersuite.NewTLSAes128GcmSha256(),
- ciphersuite.NewTLSAes256GcmSha384(),
- ciphersuite.NewTLSChacha20Poly1305Sha256(),
- ciphersuite.NewTLSEcdheEcdsaWithAes128Ccm(),
- ciphersuite.NewTLSEcdheEcdsaWithAes128Ccm8(),
- &ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256{},
- &ciphersuite.TLSEcdheRsaWithAes128GcmSha256{},
- &ciphersuite.TLSEcdheEcdsaWithAes256CbcSha{},
- &ciphersuite.TLSEcdheRsaWithAes256CbcSha{},
- ciphersuite.NewTLSPskWithAes128Ccm(),
- ciphersuite.NewTLSPskWithAes128Ccm8(),
- ciphersuite.NewTLSPskWithAes256Ccm8(),
- &ciphersuite.TLSPskWithAes128GcmSha256{},
- &ciphersuite.TLSEcdheEcdsaWithAes256GcmSha384{},
- &ciphersuite.TLSEcdheRsaWithAes256GcmSha384{},
- &ciphersuite.TLSEcdheEcdsaWithChacha20Poly1305Sha256{},
- &ciphersuite.TLSEcdheRsaWithChacha20Poly1305Sha256{},
- &ciphersuite.TLSPskWithChacha20Poly1305Sha256{},
- }
-}
-
-func cipherSuiteIDs(cipherSuites []CipherSuite) []uint16 {
- rtrn := []uint16{}
- for _, c := range cipherSuites {
- rtrn = append(rtrn, uint16(c.ID()))
- }
-
- return rtrn
-}
-
-func defaultCipherSuitesForVersions(minVersion, maxVersion protocol.Version) []CipherSuite {
- cipherSuites := []CipherSuite{}
- for _, version := range supportedVersionsRange(minVersion, maxVersion) {
- switch {
- case version.Equal(protocol.Version1_3):
- cipherSuites = append(cipherSuites, defaultCipherSuites13()...)
- case version.Equal(protocol.Version1_2):
- cipherSuites = append(cipherSuites, defaultCipherSuites()...)
- }
- }
-
- return cipherSuites
-}
-
-func knownCipherSuiteSupportedVersions(id CipherSuiteID) ([]protocol.Version, bool) {
- switch id { //nolint:exhaustive
- case TLS_AES_128_GCM_SHA256,
- TLS_AES_256_GCM_SHA384,
- TLS_CHACHA20_POLY1305_SHA256:
- return []protocol.Version{protocol.Version1_3}, true
- case TLS_ECDHE_ECDSA_WITH_AES_128_CCM,
- TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8,
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
- TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
- TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
- TLS_PSK_WITH_AES_128_CCM,
- TLS_PSK_WITH_AES_128_CCM_8,
- TLS_PSK_WITH_AES_256_CCM_8,
- TLS_PSK_WITH_AES_128_GCM_SHA256,
- TLS_PSK_WITH_AES_128_CBC_SHA256,
- TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256,
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
- TLS_PSK_WITH_CHACHA20_POLY1305_SHA256:
- return []protocol.Version{protocol.Version1_2}, true
- default:
- return nil, false
- }
-}
-
-func cipherSuiteSupportedVersions(id CipherSuiteID) []protocol.Version {
- if versions, ok := knownCipherSuiteSupportedVersions(id); ok {
- return versions
- }
-
- return []protocol.Version{protocol.Version1_2}
-}
-
-func cipherSuiteSupportedVersionIDs(id CipherSuiteID) []uint16 {
- versions := cipherSuiteSupportedVersions(id)
- ids := make([]uint16, 0, len(versions))
- for _, version := range versions {
- ids = append(ids, protocolVersionID(version))
- }
-
- return ids
-}
-
-func protocolVersionID(version protocol.Version) uint16 {
- return uint16(version.Major)<<8 | uint16(version.Minor)
-}
-
-func cipherSuiteIDSupportsVersion(id CipherSuiteID, version protocol.Version) bool {
- return slices.ContainsFunc(cipherSuiteSupportedVersions(id), version.Equal)
-}
-
-func cipherSuiteIDSupportsVersions(id CipherSuiteID, minVersion, maxVersion protocol.Version) bool {
- for _, version := range supportedVersionsRange(minVersion, maxVersion) {
- if cipherSuiteIDSupportsVersion(id, version) {
- return true
- }
- }
-
- return false
-}
-
-func filterCipherSuitesForVersion(cipherSuites []CipherSuite, version protocol.Version) []CipherSuite {
- filtered := make([]CipherSuite, 0, len(cipherSuites))
- for _, c := range cipherSuites {
- if cipherSuiteIDSupportsVersion(c.ID(), version) {
- filtered = append(filtered, c)
- }
- }
-
- return filtered
-}
-
-func filterCipherSuitesForVersions(
- cipherSuites []CipherSuite,
- minVersion, maxVersion protocol.Version,
-) []CipherSuite {
- filtered := make([]CipherSuite, 0, len(cipherSuites))
- for _, c := range cipherSuites {
- if cipherSuiteIDSupportsVersions(c.ID(), minVersion, maxVersion) {
- filtered = append(filtered, c)
- }
- }
-
- return filtered
-}
-
-//nolint:cyclop
-func parseCipherSuites(
- userSelectedSuites []CipherSuiteID,
- customCipherSuites func() []CipherSuite,
- includeCertificateSuites, includePSKSuites bool,
-) ([]CipherSuite, error) {
- return parseCipherSuitesForVersions(
- userSelectedSuites,
- customCipherSuites,
- includeCertificateSuites,
- includePSKSuites,
- protocol.Version1_2,
- protocol.Version1_2,
- )
-}
-
-//nolint:cyclop
-func parseCipherSuitesForVersions(
- userSelectedSuites []CipherSuiteID,
- customCipherSuites func() []CipherSuite,
- includeCertificateSuites, includePSKSuites bool,
- minVersion, maxVersion protocol.Version,
-) ([]CipherSuite, error) {
- cipherSuitesForIDs := func(ids []CipherSuiteID) ([]CipherSuite, error) {
- cipherSuites := []CipherSuite{}
- for _, id := range ids {
- c := cipherSuiteForID(id, nil)
- if c == nil {
- return nil, &invalidCipherSuiteError{id}
- }
- cipherSuites = append(cipherSuites, c)
- }
-
- return cipherSuites, nil
- }
-
- var (
- cipherSuites []CipherSuite
- err error
- i int
- )
- if userSelectedSuites != nil {
- cipherSuites, err = cipherSuitesForIDs(userSelectedSuites)
- if err != nil {
- return nil, err
- }
- } else {
- cipherSuites = defaultCipherSuitesForVersions(minVersion, maxVersion)
- }
-
- // Put CustomCipherSuites before ID selected suites
- if customCipherSuites != nil {
- cipherSuites = append(customCipherSuites(), cipherSuites...)
- }
-
- cipherSuites = filterCipherSuitesForVersions(cipherSuites, minVersion, maxVersion)
-
- var foundCertificateSuite, foundPSKSuite, foundAnonymousSuite, foundTLS13Suite bool
- for _, cipher := range cipherSuites {
- switch {
- case includeCertificateSuites && cipher.AuthenticationType() == CipherSuiteAuthenticationTypeCertificate:
- foundCertificateSuite = true
- case includePSKSuites && cipher.AuthenticationType() == CipherSuiteAuthenticationTypePreSharedKey:
- foundPSKSuite = true
- case cipher.AuthenticationType() == CipherSuiteAuthenticationTypeAnonymous:
- foundAnonymousSuite = true
- if cipherSuiteIDSupportsVersion(cipher.ID(), protocol.Version1_3) {
- foundTLS13Suite = true
- }
- default:
- continue
- }
- cipherSuites[i] = cipher
- i++
- }
-
- switch {
- case includeCertificateSuites && !foundCertificateSuite && !foundAnonymousSuite:
- return nil, dtlserrors.ErrNoAvailableCertificateCipherSuite
- case includePSKSuites && !foundPSKSuite && !foundTLS13Suite:
- return nil, dtlserrors.ErrNoAvailablePSKCipherSuite
- case i == 0:
- return nil, dtlserrors.ErrNoAvailableCipherSuites
- }
-
- return cipherSuites[:i], nil
-}
-
-func filterCipherSuitesForCertificate(cert *tls.Certificate, cipherSuites []CipherSuite) []CipherSuite {
- if cert == nil || cert.PrivateKey == nil {
- return cipherSuites
- }
- signer, ok := cert.PrivateKey.(crypto.Signer)
- if !ok {
- return cipherSuites
- }
-
- var certType clientcertificate.Type
- switch signer.Public().(type) {
- case ed25519.PublicKey, *ecdsa.PublicKey:
- certType = clientcertificate.ECDSASign
- case *rsa.PublicKey:
- certType = clientcertificate.RSASign
- }
-
- filtered := []CipherSuite{}
- for _, c := range cipherSuites {
- if c.AuthenticationType() != CipherSuiteAuthenticationTypeCertificate || certType == c.CertificateType() {
- filtered = append(filtered, c)
- }
- }
-
- return filtered
-}
diff --git a/cipher_suite_test.go b/cipher_suite_test.go
deleted file mode 100644
index 9472b24dd..000000000
--- a/cipher_suite_test.go
+++ /dev/null
@@ -1,264 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
- "testing"
- "time"
-
- "github.com/pion/dtls/v3/internal/ciphersuite"
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsnet "github.com/pion/dtls/v3/pkg/net"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/transport/v4/dpipe"
- "github.com/pion/transport/v4/test"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestCipherSuiteName(t *testing.T) {
- testCases := []struct {
- suite CipherSuiteID
- expected string
- }{
- {TLS_AES_128_GCM_SHA256, "TLS_AES_128_GCM_SHA256"},
- {TLS_AES_256_GCM_SHA384, "TLS_AES_256_GCM_SHA384"},
- {TLS_CHACHA20_POLY1305_SHA256, "TLS_CHACHA20_POLY1305_SHA256"},
- {TLS_ECDHE_ECDSA_WITH_AES_128_CCM, "TLS_ECDHE_ECDSA_WITH_AES_128_CCM"},
- {TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256"},
- {TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"},
- {TLS_PSK_WITH_CHACHA20_POLY1305_SHA256, "TLS_PSK_WITH_CHACHA20_POLY1305_SHA256"},
- {CipherSuiteID(0x0000), "0x0000"},
- }
-
- for _, testCase := range testCases {
- assert.Equal(t, testCase.expected, CipherSuiteName(testCase.suite))
- }
-}
-
-func TestAllCipherSuites(t *testing.T) {
- assert.NotEmpty(t, allCipherSuites())
-}
-
-func TestInsecureCipherSuites(t *testing.T) {
- assert.Empty(t, InsecureCipherSuites(), "Expected no insecure ciphersuites")
-}
-
-func TestCipherSuites(t *testing.T) {
- ours := allCipherSuites()
- theirs := CipherSuites()
- assert.Equal(t, len(ours), len(theirs))
-
- for i, s := range ours {
- t.Run(s.String(), func(t *testing.T) {
- cipher := theirs[i]
- assert.Equal(t, cipher.ID, uint16(s.ID()))
- assert.Equal(t, cipher.Name, s.String())
- assert.Equal(t, cipherSuiteSupportedVersionIDs(s.ID()), cipher.SupportedVersions)
- assert.False(t, cipher.Insecure, "Expected Insecure")
- })
- }
-}
-
-func TestCipherSuiteSupportedVersions(t *testing.T) {
- testCases := []struct {
- name string
- suite CipherSuiteID
- expected []protocol.Version
- }{
- {
- name: "TLS 1.3",
- suite: TLS_AES_128_GCM_SHA256,
- expected: []protocol.Version{protocol.Version1_3},
- },
- {
- name: "DTLS 1.2",
- suite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- expected: []protocol.Version{protocol.Version1_2},
- },
- {
- name: "custom suites default to DTLS 1.2",
- suite: 0xffff,
- expected: []protocol.Version{protocol.Version1_2},
- },
- }
-
- for _, testCase := range testCases {
- t.Run(testCase.name, func(t *testing.T) {
- assert.Equal(t, testCase.expected, cipherSuiteSupportedVersions(testCase.suite))
- })
- }
-}
-
-func TestParseCipherSuitesForVersions(t *testing.T) {
- t.Run("default DTLS 1.2", func(t *testing.T) {
- suites, err := parseCipherSuitesForVersions(
- nil,
- nil,
- true,
- false,
- protocol.Version1_2,
- protocol.Version1_2,
- )
- require.NoError(t, err)
- require.NotEmpty(t, suites)
-
- for _, suite := range suites {
- assert.True(t, cipherSuiteIDSupportsVersion(suite.ID(), protocol.Version1_2))
- assert.False(t, cipherSuiteIDSupportsVersion(suite.ID(), protocol.Version1_3))
- }
- })
-
- t.Run("default DTLS 1.3", func(t *testing.T) {
- suites, err := parseCipherSuitesForVersions(
- nil,
- nil,
- true,
- false,
- protocol.Version1_3,
- protocol.Version1_3,
- )
- require.NoError(t, err)
- require.Equal(t, []uint16{
- uint16(TLS_AES_128_GCM_SHA256),
- uint16(TLS_AES_256_GCM_SHA384),
- uint16(TLS_CHACHA20_POLY1305_SHA256),
- }, cipherSuiteIDs(suites))
- })
-
- t.Run("default dual stack", func(t *testing.T) {
- suites, err := parseCipherSuitesForVersions(
- nil,
- nil,
- true,
- false,
- protocol.Version1_2,
- protocol.Version1_3,
- )
- require.NoError(t, err)
- require.Greater(t, len(suites), len(defaultCipherSuites13()))
-
- assert.Equal(t, uint16(TLS_AES_128_GCM_SHA256), cipherSuiteIDs(suites)[0])
- assert.Contains(t, cipherSuiteIDs(suites), uint16(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256))
- })
-
- t.Run("selected suites are filtered by version", func(t *testing.T) {
- suites, err := parseCipherSuitesForVersions(
- []CipherSuiteID{
- TLS_AES_128_GCM_SHA256,
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- },
- nil,
- true,
- false,
- protocol.Version1_2,
- protocol.Version1_2,
- )
- require.NoError(t, err)
- require.Equal(t, []uint16{uint16(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)}, cipherSuiteIDs(suites))
- })
-
- t.Run("selected suite must match version", func(t *testing.T) {
- _, err := parseCipherSuitesForVersions(
- []CipherSuiteID{TLS_AES_128_GCM_SHA256},
- nil,
- true,
- false,
- protocol.Version1_2,
- protocol.Version1_2,
- )
- require.ErrorIs(t, err, dtlserrors.ErrNoAvailableCertificateCipherSuite)
- })
-
- t.Run("TLS 1.3 suites are authentication neutral", func(t *testing.T) {
- suites, err := parseCipherSuitesForVersions(
- []CipherSuiteID{TLS_AES_128_GCM_SHA256},
- nil,
- false,
- true,
- protocol.Version1_3,
- protocol.Version1_3,
- )
- require.NoError(t, err)
- require.Equal(t, []uint16{uint16(TLS_AES_128_GCM_SHA256)}, cipherSuiteIDs(suites))
- })
-
- t.Run("custom anonymous suites do not satisfy PSK configs", func(t *testing.T) {
- _, err := parseCipherSuitesForVersions(
- []CipherSuiteID{},
- func() []CipherSuite {
- return []CipherSuite{&testCustomCipherSuite{authenticationType: CipherSuiteAuthenticationTypeAnonymous}}
- },
- false,
- true,
- protocol.Version1_2,
- protocol.Version1_2,
- )
- require.ErrorIs(t, err, dtlserrors.ErrNoAvailablePSKCipherSuite)
- })
-}
-
-// CustomCipher that is just used to assert Custom IDs work.
-type testCustomCipherSuite struct {
- ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256
- authenticationType CipherSuiteAuthenticationType
-}
-
-func (t *testCustomCipherSuite) ID() CipherSuiteID {
- return 0xFFFF
-}
-
-func (t *testCustomCipherSuite) AuthenticationType() CipherSuiteAuthenticationType {
- return t.authenticationType
-}
-
-// Assert that two connections that pass in a CipherSuite with a CustomID works.
-func TestCustomCipherSuite(t *testing.T) {
- type result struct {
- c *Conn
- err error
- }
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- runTest := func(cipherFactory func() []CipherSuite) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- ca, cb := dpipe.Pipe()
- resultCh := make(chan result)
-
- go func() {
- client, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), []ClientOption{
- WithCustomCipherSuites(cipherFactory),
- }, true)
- resultCh <- result{client, err}
- }()
-
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), []ServerOption{
- WithCustomCipherSuites(cipherFactory),
- }, true)
-
- clientResult := <-resultCh
- assert.NoError(t, err)
- assert.NoError(t, server.Close())
- assert.Nil(t, clientResult.err)
- assert.NoError(t, clientResult.c.Close())
- }
-
- t.Run("Custom ID", func(*testing.T) {
- runTest(func() []CipherSuite {
- return []CipherSuite{&testCustomCipherSuite{authenticationType: CipherSuiteAuthenticationTypeCertificate}}
- })
- })
-
- t.Run("Anonymous Cipher", func(*testing.T) {
- runTest(func() []CipherSuite {
- return []CipherSuite{&testCustomCipherSuite{authenticationType: CipherSuiteAuthenticationTypeAnonymous}}
- })
- })
-}
diff --git a/compression_method.go b/compression_method.go
deleted file mode 100644
index 1b93599f5..000000000
--- a/compression_method.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import "github.com/pion/dtls/v3/pkg/protocol"
-
-func defaultCompressionMethods() []*protocol.CompressionMethod {
- return []*protocol.CompressionMethod{
- {},
- }
-}
diff --git a/config.go b/config.go
deleted file mode 100644
index 8dce180e9..000000000
--- a/config.go
+++ /dev/null
@@ -1,88 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "crypto"
- "crypto/ecdsa"
- "crypto/ed25519"
- "crypto/rsa"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
-)
-
-const keyLogLabelTLS12 = "CLIENT_RANDOM"
-
-const defaultMTU = 1200 // bytes
-
-var defaultCurves = []elliptic.Curve{elliptic.X25519, elliptic.P256, elliptic.P384} //nolint:gochecknoglobals
-
-func (c *dtlsConfig) includeCertificateSuites() bool {
- return c.psk == nil || len(c.Certificates) > 0 || c.getCertificate != nil || c.getClientCertificate != nil
-}
-
-// PSKCallback is called once we have the remote's PSKIdentityHint.
-// If the remote provided none it will be nil.
-type PSKCallback func([]byte) ([]byte, error)
-
-// ClientAuthType declares the policy the server will follow for
-// TLS Client Authentication.
-type ClientAuthType int
-
-// ClientAuthType enums.
-const (
- NoClientCert ClientAuthType = iota
- RequestClientCert
- RequireAnyClientCert
- VerifyClientCertIfGiven
- RequireAndVerifyClientCert
-)
-
-// ExtendedMasterSecretType declares the policy the client and server
-// will follow for the Extended Master Secret extension.
-type ExtendedMasterSecretType int
-
-// ExtendedMasterSecretType enums.
-const (
- RequestExtendedMasterSecret ExtendedMasterSecretType = iota
- RequireExtendedMasterSecret
- DisableExtendedMasterSecret
-)
-
-func validateConfig(config *dtlsConfig) error { //nolint:cyclop
- switch {
- case config == nil:
- return dtlserrors.ErrNoConfigProvided
- case config.PSKIdentityHint != nil && config.psk == nil:
- return dtlserrors.ErrIdentityNoPSK
- }
-
- for _, cert := range config.Certificates {
- if cert.Certificate == nil {
- return dtlserrors.ErrInvalidCertificate
- }
- if cert.PrivateKey != nil {
- signer, ok := cert.PrivateKey.(crypto.Signer)
- if !ok {
- return dtlserrors.ErrInvalidPrivateKey
- }
- switch signer.Public().(type) {
- case ed25519.PublicKey:
- case *ecdsa.PublicKey:
- case *rsa.PublicKey:
- default:
- return dtlserrors.ErrInvalidPrivateKey
- }
- }
- }
-
- minVersion, maxVersion := normalizeProtocolVersionRange(config.MinVersion, config.MaxVersion)
- _, err := parseCipherSuitesForVersions(
- config.CipherSuites, config.customCipherSuites, config.includeCertificateSuites(), config.psk != nil,
- minVersion, maxVersion,
- )
-
- return err
-}
diff --git a/config_test.go b/config_test.go
deleted file mode 100644
index f5dc09cae..000000000
--- a/config_test.go
+++ /dev/null
@@ -1,211 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "crypto/dsa" //nolint:staticcheck
- "crypto/rand"
- "crypto/rsa"
- "crypto/tls"
- "errors"
- "testing"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- "github.com/pion/dtls/v3/pkg/crypto/selfsign"
- dtlsnet "github.com/pion/dtls/v3/pkg/net"
- "github.com/pion/transport/v4/dpipe"
- "github.com/stretchr/testify/assert"
-)
-
-func clientConfigError(t *testing.T, opts ...ClientOption) error {
- t.Helper()
-
- ca, cb := dpipe.Pipe()
- defer func() {
- _ = ca.Close()
- _ = cb.Close()
- }()
-
- client, err := ClientWithOptions(dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), opts...)
- if client != nil {
- _ = client.Close()
- }
-
- return err
-}
-
-func serverConfigError(t *testing.T, opts ...ServerOption) error {
- t.Helper()
-
- ca, cb := dpipe.Pipe()
- defer func() {
- _ = ca.Close()
- _ = cb.Close()
- }()
-
- server, err := ServerWithOptions(dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), opts...)
- if server != nil {
- _ = server.Close()
- }
-
- return err
-}
-
-func TestConfigOptions(t *testing.T) {
- cert, err := selfsign.GenerateSelfSigned()
- if err != nil {
- assert.NoError(t, err, "TestConfigOptions: self signed certificate not generated")
-
- return
- }
- dsaPrivateKey := &dsa.PrivateKey{}
- err = dsa.GenerateParameters(&dsaPrivateKey.Parameters, rand.Reader, dsa.L1024N160)
- if err != nil {
- assert.NoError(t, err, "TestConfigOptions: DSA parameters not generated")
-
- return
- }
- err = dsa.GenerateKey(dsaPrivateKey, rand.Reader)
- if err != nil {
- assert.NoError(t, err, "TestConfigOptions: DSA private key not generated")
-
- return
- }
- rsaPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
- if err != nil {
- assert.NoError(t, err, "TestConfigOptions: RSA private key not generated")
-
- return
- }
- cases := map[string]struct {
- validate func(*testing.T) error
- wantAnyErr bool
- expErr error
- }{
- "psk and Certificate, valid cipher suites": {
- validate: func(t *testing.T) error {
- t.Helper()
-
- return serverConfigError(t,
- WithCipherSuites(TLS_PSK_WITH_AES_128_CCM_8, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithPSK(func([]byte) ([]byte, error) { return nil, nil }),
- WithCertificates(cert),
- )
- },
- },
- "psk and Certificate, no psk cipher suite": {
- validate: func(t *testing.T) error {
- t.Helper()
-
- return serverConfigError(t,
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithPSK(func([]byte) ([]byte, error) { return nil, nil }),
- WithCertificates(cert),
- )
- },
- expErr: dtlserrors.ErrNoAvailablePSKCipherSuite,
- },
- "psk and Certificate, no non-psk cipher suite": {
- validate: func(t *testing.T) error {
- t.Helper()
-
- return serverConfigError(t,
- WithCipherSuites(TLS_PSK_WITH_AES_128_CCM_8),
- WithPSK(func([]byte) ([]byte, error) { return nil, nil }),
- WithCertificates(cert),
- )
- },
- expErr: dtlserrors.ErrNoAvailableCertificateCipherSuite,
- },
- "psk identity hint with not psk": {
- validate: func(t *testing.T) error {
- t.Helper()
-
- return serverConfigError(t,
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithPSKIdentityHint([]byte{}),
- )
- },
- expErr: dtlserrors.ErrIdentityNoPSK,
- },
- "Invalid private key": {
- validate: func(t *testing.T) error {
- t.Helper()
-
- return clientConfigError(t,
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithCertificates(tls.Certificate{Certificate: cert.Certificate, PrivateKey: dsaPrivateKey}),
- )
- },
- expErr: dtlserrors.ErrInvalidPrivateKey,
- },
- "PrivateKey without Certificate": {
- validate: func(t *testing.T) error {
- t.Helper()
-
- return clientConfigError(t,
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithCertificates(tls.Certificate{PrivateKey: cert.PrivateKey}),
- )
- },
- expErr: dtlserrors.ErrInvalidCertificate,
- },
- "Invalid cipher suites": {
- validate: func(t *testing.T) error {
- t.Helper()
-
- return clientConfigError(t, WithCipherSuites(0x0000))
- },
- wantAnyErr: true,
- },
- "Valid config": {
- validate: func(t *testing.T) error {
- t.Helper()
-
- return clientConfigError(t,
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithCertificates(cert, tls.Certificate{Certificate: cert.Certificate, PrivateKey: rsaPrivateKey}),
- )
- },
- },
- "Valid config with get certificate": {
- validate: func(t *testing.T) error {
- t.Helper()
-
- return serverConfigError(t,
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithGetCertificate(func(*ClientHelloInfo) (*tls.Certificate, error) {
- return &tls.Certificate{Certificate: cert.Certificate, PrivateKey: rsaPrivateKey}, nil
- }),
- )
- },
- },
- "Valid config with get client certificate": {
- validate: func(t *testing.T) error {
- t.Helper()
-
- return clientConfigError(t,
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithGetClientCertificate(func(*CertificateRequestInfo) (*tls.Certificate, error) {
- return &tls.Certificate{Certificate: cert.Certificate, PrivateKey: rsaPrivateKey}, nil
- }),
- )
- },
- },
- }
-
- for name, testCase := range cases {
- t.Run(name, func(t *testing.T) {
- err := testCase.validate(t)
- if testCase.expErr != nil || testCase.wantAnyErr {
- if testCase.expErr != nil && !errors.Is(err, testCase.expErr) {
- assert.ErrorIs(t, err, testCase.expErr, "TestConfigOptions")
- }
- assert.Error(t, err, "TestConfigOptions: validation expected an error")
- } else {
- assert.NoError(t, err, "TestConfigOptions")
- }
- })
- }
-}
diff --git a/conn.go b/conn.go
index dfe1e6cff..0c2543d30 100644
--- a/conn.go
+++ b/conn.go
@@ -1,12 +1,12 @@
// SPDX-FileCopyrightText: 2026 The Pion community
// SPDX-License-Identifier: MIT
+// Package dtls implements Datagram Transport Layer Security (DTLS) 1.2 and 1.3.
package dtls
import (
"bytes"
"context"
- "crypto/fips140"
"errors"
"fmt"
"io"
@@ -15,34 +15,58 @@ import (
"sync/atomic"
"time"
- "github.com/pion/dtls/v3/internal/closer"
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
- "github.com/pion/dtls/v3/pkg/crypto/signaturehash"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
+ dtlsciphersuite "github.com/pion/dtls/v4/internal/ciphersuite"
+ "github.com/pion/dtls/v4/internal/closer"
+ dtlsconfig "github.com/pion/dtls/v4/internal/config"
+ "github.com/pion/dtls/v4/internal/ech"
+ dtlserrors "github.com/pion/dtls/v4/internal/errors"
+ dtlsflight "github.com/pion/dtls/v4/internal/flight"
+ dtlsflight12 "github.com/pion/dtls/v4/internal/flight/flight12"
+ dtlsflight13 "github.com/pion/dtls/v4/internal/flight/flight13"
+ dtlsfragmentbuffer "github.com/pion/dtls/v4/internal/fragmentbuffer"
+ dtlshandshake "github.com/pion/dtls/v4/internal/handshake"
+ "github.com/pion/dtls/v4/internal/negotiation"
+ idtlsnet "github.com/pion/dtls/v4/internal/net"
+ "github.com/pion/dtls/v4/internal/net/udp"
+ "github.com/pion/dtls/v4/internal/recordwire"
+ dtlsrrc "github.com/pion/dtls/v4/internal/rrc"
+ dtlsstate "github.com/pion/dtls/v4/internal/state"
+ "github.com/pion/dtls/v4/internal/util"
+ cryptosuite "github.com/pion/dtls/v4/pkg/crypto/ciphersuite"
+ "github.com/pion/dtls/v4/pkg/protocol"
+ "github.com/pion/dtls/v4/pkg/protocol/alert"
+ extension13 "github.com/pion/dtls/v4/pkg/protocol/extension/dtls13"
+ "github.com/pion/dtls/v4/pkg/protocol/handshake"
+ "github.com/pion/dtls/v4/pkg/protocol/recordlayer"
"github.com/pion/logging"
- "github.com/pion/transport/v4/deadline"
- "github.com/pion/transport/v4/netctx"
- "github.com/pion/transport/v4/replaydetector"
+ "github.com/pion/transport/v5/deadline"
+ "github.com/pion/transport/v5/netctx"
+ "github.com/pion/transport/v5/replaydetector"
)
const (
- initialTickerInterval = time.Second
- cookieLength = 20
- sessionLength = 32
- inboundBufferSize = 8192
+ // defaultReceiveBufferSize is the default size of the buffers used to
+ // receive datagrams. Overridable via WithReceiveBufferSize.
+ defaultReceiveBufferSize = 8192
// Default replay protection window is specified by RFC 6347 Section 4.1.2.6.
defaultReplayProtectionWindow = 64
+ maxPlaintextRecordLen = 1 << 14
+ maxCIDInnerPlaintextLen = maxPlaintextRecordLen
+ maxDTLS13InnerPlaintextLen = maxPlaintextRecordLen + 1
// maxAppDataPacketQueueSize is the maximum number of app data packets we will.
// enqueue before the handshake is completed.
maxAppDataPacketQueueSize = 100
)
+var (
+ errRecordAuthentication = errors.New("record authentication failed")
+ errRecordOperational = errors.New("record protection operational failure")
+)
+
+func operationalProtectionError(err error) error {
+ return fmt.Errorf("%w: %w", errRecordOperational, err)
+}
+
func invalidKeyingLabels() map[string]bool {
return map[string]bool{
"client finished": true,
@@ -53,39 +77,155 @@ func invalidKeyingLabels() map[string]bool {
}
type addrPkt struct {
- rAddr net.Addr
- data []byte
+ rAddr net.Addr
+ data []byte
+ datagramContainsCID bool
+ pendingCID bool
+}
+
+// readBufferLease owns a recyclable read buffer for one datagram-processing
+// call. Anything retained beyond that call must take an exact owned copy.
+type readBufferLease struct {
+ conn *Conn
+ pool *sync.Pool
+ recyclableReadBuffer *[]byte
+ datagramContainsCID bool
+ pendingCID bool
+}
+
+func (w *readBufferLease) enqueue(packet addrPkt) bool {
+ packet.datagramContainsCID = w.datagramContainsCID
+ packet.pendingCID = w.pendingCID && protocol.IsDTLS13Ciphertext(protocol.ContentType(packet.data[0]))
+
+ return w.conn.enqueueEncryptedPackets(packet)
+}
+
+func (w *readBufferLease) releaseReadBuffer() {
+ readBuffer := w.recyclableReadBuffer
+ w.recyclableReadBuffer = nil
+ if readBuffer != nil && w.pool != nil {
+ w.pool.Put(readBuffer)
+ }
+}
+
+type incomingPacketState struct {
+ raw []byte
+ content []byte
+ contentType protocol.ContentType
+ number protocol.RecordNumber
+ markPacketAsValid func() bool
+ originalCID bool
+}
+
+type packetOutcome struct {
+ containsHandshake bool
+ retransmit bool
+ retransmitStart bool
+ clientHelloRetransmit bool
+ receivedACK *protocol.ACK
+ responseAlert *alert.Alert
}
-type recvHandshakeState struct {
- done chan struct{}
- isRetransmit bool
+type datagramProcessingSummary struct {
+ containsHandshake bool
+ retransmit bool
+ retransmitStart bool
+ clientHelloRetransmit bool
+ receivedACKs []protocol.ACK
}
+type readLoopErrorAction uint8
+
+const (
+ readLoopStop readLoopErrorAction = iota
+ readLoopContinue
+ readLoopDeliverAndContinue
+ readLoopCloseAndStop
+)
+
type handshakeStart struct {
- flight flightVal
- fsmState handshakeState
- flights []*packet
- transcript13 *handshakeTranscript13
- postSetup func(context.Context)
+ flight12 dtlsflight12.Flight
+ flight13 dtlsflight13.Flight
+ fsmState dtlshandshake.State
+ flights []*dtlsflight.Outbound
+ postSetup func(context.Context)
+}
+
+type handshakeConn struct {
+ conn *Conn
+}
+
+func (c handshakeConn) Notify(ctx context.Context, level alert.Level, desc alert.Description) error {
+ return c.conn.notify(ctx, level, desc)
+}
+
+func (c handshakeConn) WritePackets(ctx context.Context, pkts []*dtlsflight.Outbound) (*dtlshandshake.WriteResult, error) {
+ return c.conn.writePacketsWithResult(ctx, pkts)
+}
+
+func (c handshakeConn) RecvHandshake() <-chan dtlshandshake.RecvHandshakeState {
+ return c.conn.recvHandshake()
+}
+
+func (c handshakeConn) SetLocalEpoch(epoch uint64) {
+ c.conn.setLocalEpoch(epoch)
+}
+
+func (c handshakeConn) CommitLocalKeyUpdate(generation *dtlsstate.TrafficGeneration) error {
+ return c.conn.commitLocalKeyUpdate(generation)
+}
+
+func (c handshakeConn) TakePendingACKs() []protocol.RecordNumber {
+ return c.conn.takePendingACKs()
+}
+
+func (c handshakeConn) HandleQueuedPackets(ctx context.Context) error {
+ return c.conn.handleQueuedPackets(ctx)
+}
+
+func (c handshakeConn) SessionKey() []byte {
+ return c.conn.sessionKey()
+}
+
+func adaptFlightConn(conn *Conn) dtlsflight.Conn {
+ if conn == nil {
+ return nil
+ }
+
+ return handshakeConn{conn}
+}
+
+func srvCliStr(isClient bool) string {
+ if isClient {
+ return "client"
+ }
+
+ return "server"
}
// Conn represents a DTLS connection.
type Conn struct {
- lock sync.RWMutex // Internal lock (must not be public)
- nextConn netctx.PacketConn // Embedded Conn, typically a udpconn we read/write from
- fragmentBuffer *fragmentBuffer // out-of-order and missing fragment handling
- handshakeCache *handshakeCache // caching of handshake messages for verifyData generation
- decrypted chan any // Decrypted Application Data or error, pull by calling `Read`
+ lock sync.RWMutex // Internal lock (must not be public)
+ nextConn netctx.PacketConn // Embedded Conn, typically a udpconn we read/write from
+ packetConn *udp.PacketConn // Listener CID and address routing.
+ fragmentBuffer *dtlsfragmentbuffer.FragmentBuffer // out-of-order and missing fragment handling
+ handshakeCache *dtlsflight.Cache // caching of handshake messages for verifyData generation
+ pendingACKs []protocol.RecordNumber
+ pendingCIDACKs map[protocol.RecordNumber]uint16
+ decrypted chan any // Decrypted Application Data or error, pull by calling `Read`
rAddr net.Addr
- state dtlsstate.State // Internal state
+ state dtlsstate.Active // active DTLS version state
maximumTransmissionUnit int
paddingLengthGenerator func(uint) uint
+ readBufferPool *sync.Pool
- handshakeCompletedSuccessfully atomic.Bool
- handshakeMutex sync.Mutex
- handshakeDone chan struct{}
+ handshakeEstablished *dtlshandshake.Establishment
+ handshakeMutex sync.Mutex
+ handshakeDone chan struct{}
+ handshakeLoops *sync.WaitGroup
+ exporting atomic.Bool
+ writeLock sync.Mutex
encryptedPackets []addrPkt
@@ -99,184 +239,82 @@ type Conn struct {
log logging.LeveledLogger
reading chan struct{}
- handshakeRecv chan recvHandshakeState
+ handshakeRecv chan dtlshandshake.RecvHandshakeState
+ detached *DetachedConn
cancelHandshaker func()
cancelHandshakeReader func()
- fsm handshakeFSM
+ fsm dtlshandshake.FSM
replayProtectionWindow uint
- handshakeConfig *handshakeConfig
+ handshakeConfig *dtlsconfig.HandshakeConfig
+
+ cidPathMigrationPolicy cidPathMigrationPolicy
+ registeredLocalCID []byte
+ registeredReceiveCIDs *dtlsstate.CIDReceiveSet
+ rrc dtlsrrc.Manager
}
// createConn creates a new DTLS connection.
// Caller is responsible for validating the config before calling this function.
-//
-//nolint:cyclop
-func createConn(
- nextConn net.PacketConn,
- rAddr net.Addr,
- config *dtlsConfig,
- isClient bool,
- resumeState *dtlsstate.State,
-) (*Conn, error) {
+func createConn(nextConn net.PacketConn, rAddr net.Addr, config *dtlsConfig, isClient bool, resumeState dtlsstate.Active) (*Conn, error) {
if nextConn == nil {
return nil, dtlserrors.ErrNilNextConn
}
- loggerFactory := config.LoggerFactory
- if loggerFactory == nil {
- loggerFactory = logging.NewDefaultLoggerFactory()
- }
-
- logger := loggerFactory.NewLogger("dtls")
-
- mtu := config.MTU
- if mtu <= 0 {
- mtu = defaultMTU
- }
-
- replayProtectionWindow := config.ReplayProtectionWindow
- if replayProtectionWindow <= 0 {
- replayProtectionWindow = defaultReplayProtectionWindow
- }
-
- paddingLengthGenerator := config.PaddingLengthGenerator
- if paddingLengthGenerator == nil {
- paddingLengthGenerator = func(uint) uint { return 0 }
- }
-
- minVersion, maxVersion := normalizeProtocolVersionRange(config.MinVersion, config.MaxVersion)
-
- cipherSuites, err := parseCipherSuitesForVersions(
- config.CipherSuites,
- config.customCipherSuites,
- config.includeCertificateSuites(),
- config.psk != nil,
- minVersion,
- maxVersion,
- )
- if err != nil {
- return nil, err
- }
-
- signatureSchemes, err := signaturehash.ParseSignatureSchemes(config.SignatureSchemes, config.InsecureHashes)
+ configValues, err := newConnConfigValues(config)
if err != nil {
return nil, err
}
- // Parse certificate signature schemes only if explicitly configured
- var certSignatureSchemes []signaturehash.Algorithm
- if len(config.CertificateSignatureSchemes) > 0 {
- certSignatureSchemes, err = signaturehash.ParseSignatureSchemes(
- config.CertificateSignatureSchemes,
- config.InsecureHashes,
- )
- if err != nil {
- return nil, err
- }
- }
-
- workerInterval := initialTickerInterval
- if config.FlightInterval > 0 {
- workerInterval = config.FlightInterval
- }
-
- serverName := config.ServerName
- // Do not allow the use of an IP address literal as an SNI value.
- // See RFC 6066, Section 3.
- if net.ParseIP(serverName) != nil {
- serverName = ""
- }
+ handshakeConfig := newHandshakeConfig(config, configValues, resumeState)
+ conn := newConn(nextConn, rAddr, configValues, handshakeConfig, isClient)
- curves := config.EllipticCurves
- if len(curves) == 0 {
- curves = defaultCurves
- }
+ conn.setRemoteEpoch(0)
+ conn.setLocalEpoch(0)
- if fips140.Enabled() {
- // On FIPS systems, filter out non-approved curves
- filtered := make([]elliptic.Curve, 0, len(curves))
- for _, c := range curves {
- if c != elliptic.X25519 {
- filtered = append(filtered, c)
- }
- }
- curves = filtered
- }
-
- handshakeConfig := &handshakeConfig{
- localPSKCallback: config.psk,
- localPSKIdentityHint: config.PSKIdentityHint,
- localCipherSuites: cipherSuites,
- localSignatureSchemes: signatureSchemes,
- localCertSignatureSchemes: certSignatureSchemes,
- extendedMasterSecret: ExtendedMasterSecretType(config.ExtendedMasterSecret),
- localSRTPProtectionProfiles: config.SRTPProtectionProfiles,
- localSRTPMasterKeyIdentifier: config.SRTPMasterKeyIdentifier,
- serverName: serverName,
- supportedProtocols: config.SupportedProtocols,
- clientAuth: ClientAuthType(config.ClientAuth),
- localCertificates: config.Certificates,
- insecureSkipVerify: config.InsecureSkipVerify,
- verifyPeerCertificate: config.VerifyPeerCertificate,
- verifyConnection: config.verifyConnection,
- rootCAs: config.RootCAs,
- clientCAs: config.ClientCAs,
- customCipherSuites: config.customCipherSuites,
- initialRetransmitInterval: workerInterval,
- disableRetransmitBackoff: config.DisableRetransmitBackoff,
- log: logger,
- initialEpoch: 0,
- keyLogWriter: config.KeyLogWriter,
- sessionStore: config.sessionStore,
- ellipticCurves: curves,
- localGetCertificate: config.getCertificate,
- localGetClientCertificate: config.getClientCertificate,
- insecureSkipHelloVerify: config.InsecureSkipVerifyHello,
- connectionIDGenerator: config.ConnectionIDGenerator,
- helloRandomBytesGenerator: config.HelloRandomBytesGenerator,
- clientHelloMessageHook: config.ClientHelloMessageHook,
- serverHelloMessageHook: config.ServerHelloMessageHook,
- certificateRequestMessageHook: config.CertificateRequestMessageHook,
- resumeState: resumeState,
- minVersion: minVersion,
- maxVersion: maxVersion,
- }
+ return conn, nil
+}
+func newConn(nextConn net.PacketConn, rAddr net.Addr, configValues connConfigValues, handshakeConfig *dtlsconfig.HandshakeConfig, isClient bool) *Conn {
conn := &Conn{
rAddr: rAddr,
- nextConn: netctx.NewPacketConn(nextConn),
handshakeConfig: handshakeConfig,
- fragmentBuffer: newFragmentBuffer(),
- handshakeCache: newHandshakeCache(),
- maximumTransmissionUnit: mtu,
- paddingLengthGenerator: paddingLengthGenerator,
+ fragmentBuffer: dtlsfragmentbuffer.New(),
+ handshakeCache: dtlsflight.NewCache(),
+ maximumTransmissionUnit: configValues.maximumTransmissionUnit,
+ paddingLengthGenerator: configValues.paddingLengthGenerator,
+ cidPathMigrationPolicy: configValues.cidPathMigrationPolicy,
+ readBufferPool: readBufferPoolForSize(configValues.receiveBufferSize),
decrypted: make(chan any, 1),
- log: logger,
+ log: configValues.logger,
readDeadline: deadline.New(),
writeDeadline: deadline.New(),
reading: make(chan struct{}, 1),
- handshakeRecv: make(chan recvHandshakeState),
+ handshakeRecv: make(chan dtlshandshake.RecvHandshakeState),
+ handshakeEstablished: dtlshandshake.NewEstablishment(),
closed: closer.NewCloser(),
cancelHandshaker: func() {},
cancelHandshakeReader: func() {},
- replayProtectionWindow: uint(replayProtectionWindow), //nolint:gosec // G115
+ replayProtectionWindow: uint(configValues.replayProtectionWindow), //nolint:gosec // G115
- state: dtlsstate.State{
- IsClient: isClient,
- },
+ state: dtlsstate.NewActive(isClient),
+ }
+ dtlsstate.CommonState(conn.state).ServerName = handshakeConfig.ServerName
+ if nextConn != nil {
+ conn.nextConn = netctx.NewPacketConn(nextConn)
+ conn.packetConn, _ = nextConn.(*udp.PacketConn)
+ if conn.cidPathMigrationPolicy == CIDPathMigrationRRC || conn.cidPathMigrationPolicy == CIDPathMigrationUnsafe {
+ conn.nextConn = newPathTransport(conn, conn.nextConn)
+ }
}
- conn.setRemoteEpoch(0)
- conn.setLocalEpoch(0)
-
- return conn, nil
+ return conn
}
// Handshake runs the client or server DTLS handshake
@@ -298,10 +336,14 @@ func (c *Conn) Handshake() error {
// the handshake is complete, the handshake is interrupted and an error is returned.
// Once the handshake has completed, cancellation of the context will not affect the
// connection.
+// Connection read and write deadlines also apply to the corresponding handshake
+// I/O, independently of the context deadline.
//
// Most uses of this package need not call HandshakeContext explicitly: the
// first [Conn.Read] or [Conn.Write] will call it automatically.
-func (c *Conn) HandshakeContext(ctx context.Context) error { //nolint:cyclop
+//
+//nolint:cyclop
+func (c *Conn) HandshakeContext(ctx context.Context) error {
c.handshakeMutex.Lock()
defer c.handshakeMutex.Unlock()
@@ -309,41 +351,34 @@ func (c *Conn) HandshakeContext(ctx context.Context) error { //nolint:cyclop
return nil
}
+ defer c.configureECHKeyCallback(ctx)()
+
handshakeDone := make(chan struct{})
defer close(handshakeDone)
c.closeLock.Lock()
c.handshakeDone = handshakeDone
c.closeLock.Unlock()
- // rfc5246#section-7.4.3
- // In addition, the hash and signature algorithms MUST be compatible
- // with the key in the server's end-entity certificate.
- if !c.state.IsClient {
- cert, err := c.handshakeConfig.getCertificate(&ClientHelloInfo{})
- if err != nil && !errors.Is(err, dtlserrors.ErrNoCertificates) {
- return err
- }
- c.handshakeConfig.localCipherSuites = filterCipherSuitesForCertificate(cert, c.handshakeConfig.localCipherSuites)
- }
-
start, err := c.prepareHandshakeStart(ctx)
if err != nil {
return err
}
- c.handshakeConfig.localCipherSuites = filterCipherSuitesForVersion(
- c.handshakeConfig.localCipherSuites,
- c.state.LocalVersion,
- )
- if len(c.handshakeConfig.localCipherSuites) == 0 {
+ common := dtlsstate.CommonState(c.state)
+ c.handshakeConfig.LocalCipherSuites = filterCipherSuitesForVersion(c.handshakeConfig.LocalCipherSuites, common.LocalVersion)
+ if len(c.handshakeConfig.LocalCipherSuites) == 0 {
return dtlserrors.ErrNoAvailableCipherSuites
}
if err := c.handshake(ctx, start); err != nil {
+ if !c.isHandshakeCompletedSuccessfully() {
+ common.SetSRTPProtectionProfile(0)
+ }
+
return err
}
- if c.state.LocalVersion == protocol.Version1_3 {
+ if common.LocalVersion == protocol.Version1_3 {
c.log.Trace("Handshake DTLS 1.3 Completed")
} else {
c.log.Trace("Handshake Completed")
@@ -352,6 +387,25 @@ func (c *Conn) HandshakeContext(ctx context.Context) error { //nolint:cyclop
return nil
}
+func (c *Conn) configureECHKeyCallback(ctx context.Context) func() {
+ getter := c.handshakeConfig.GetECHKeys
+ if getter == nil {
+ return func() {}
+ }
+ callbackCtx, cancel := context.WithCancel(ctx)
+ c.handshakeConfig.GetECHKeys = func(info *dtlsconfig.ClientHelloInfo) ([]ech.Key, error) {
+ info.Context = func() context.Context { return callbackCtx }
+ info.Conn = c
+
+ return getter(info)
+ }
+
+ return func() {
+ cancel()
+ c.handshakeConfig.GetECHKeys = getter
+ }
+}
+
// prepareHandshakeStart negotiates the DTLS version and decides how the FSM should start.
//
// There are three modes for the version:
@@ -359,70 +413,89 @@ func (c *Conn) HandshakeContext(ctx context.Context) error { //nolint:cyclop
// - DTLS 1.3 only
// - Dual-stack (this mode sends or read handshake messages without starting a FSM)
//
-// In dual-stack client mode, flights holds the already-sent ClientHello and
-// transcript13 carries the same ClientHello into the DTLS 1.3 FSM.
-// nolint:cyclop
+// In dual-stack client mode, flights holds the already-sent ClientHello. If
+// DTLS 1.3 is selected, the DTLS 1.3 FSM imports those packets into its
+// transcript.
func (c *Conn) prepareHandshakeStart(ctx context.Context) (handshakeStart, error) {
- switch {
- // DTLS 1.2 only
- case c.state.IsClient && c.handshakeConfig.maxVersion == protocol.Version1_2:
- c.state.LocalVersion = protocol.Version1_2
- if c.handshakeConfig.resumeState != nil {
- c.state = *c.handshakeConfig.resumeState
- c.state.LocalVersion = protocol.Version1_2
-
- return handshakeStart{flight: flight5, fsmState: handshakeFinished}, nil
+ if dtlsstate.CommonState(c.state).IsClient {
+ if err := c.handshakeConfig.ValidateECHVersions(); err != nil {
+ return handshakeStart{}, err
+ }
+ }
+ if resumed := c.handshakeConfig.ResumeState; resumed != nil {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ c.state = resumed
+ start := handshakeStart{fsmState: dtlshandshake.StateFinished}
+ if dtlsstate.CommonState(resumed).IsClient {
+ start.flight12, start.flight13 = dtlsflight12.Flight5, dtlsflight13.Flight5
+ } else {
+ start.flight12, start.flight13 = dtlsflight12.Flight6, dtlsflight13.Flight4
}
- return handshakeStart{flight: flight1, fsmState: handshakePreparing}, nil
- case !c.state.IsClient && c.handshakeConfig.maxVersion == protocol.Version1_2:
- c.state.LocalVersion = protocol.Version1_2
- if c.handshakeConfig.resumeState != nil {
- c.state = *c.handshakeConfig.resumeState
- c.state.LocalVersion = protocol.Version1_2
+ return start, c.registerLocalCID()
+ }
+ if c.handshakeConfig.MaxVersion == protocol.Version1_2 {
+ start := c.prepareHandshakeStart12()
+ c.lock.Lock()
+ defer c.lock.Unlock()
- return handshakeStart{flight: flight6, fsmState: handshakeFinished}, nil
- }
+ return start, c.registerLocalCID()
+ }
+ if c.handshakeConfig.MinVersion == protocol.Version1_3 {
+ return c.prepareHandshakeStart13(), nil
+ }
+ if dtlsstate.CommonState(c.state).IsClient {
+ return c.prepareDualStackClientHandshakeStart(ctx)
+ }
- return handshakeStart{flight: flight0, fsmState: handshakePreparing}, nil
+ return c.prepareDualStackServerHandshakeStart(ctx)
+}
- // DTLS 1.3 only
- case c.state.IsClient && c.handshakeConfig.minVersion == protocol.Version1_3:
- c.state.LocalVersion = protocol.Version1_3
+func (c *Conn) prepareHandshakeStart12() handshakeStart {
+ isClient := dtlsstate.CommonState(c.state).IsClient
+ state := dtlsstate.Activate12(c.state)
+ c.state = state
+ state.LocalVersion = protocol.Version1_2
+ if isClient {
+ return handshakeStart{flight12: dtlsflight12.Flight1, fsmState: dtlshandshake.StatePreparing}
+ }
- return handshakeStart{flight: flightVal(flight13_1), fsmState: handshakePreparing}, nil
- case !c.state.IsClient && c.handshakeConfig.minVersion == protocol.Version1_3:
- c.state.LocalVersion = protocol.Version1_3
+ return handshakeStart{flight12: dtlsflight12.Flight0, fsmState: dtlshandshake.StatePreparing}
+}
- return handshakeStart{flight: flightVal(flight13_0), fsmState: handshakePreparing}, nil
+func (c *Conn) prepareHandshakeStart13() handshakeStart {
+ state := dtlsstate.Activate13(c.state)
+ c.state = state
+ state.LocalVersion = protocol.Version1_3
+ if state.IsClient {
+ return handshakeStart{flight13: dtlsflight13.Flight1, fsmState: dtlshandshake.StatePreparing}
+ }
- // Dual-stack
- // This mode sends or read handshake messages to decide version without starting a FSM
- case c.state.IsClient:
- initialFlights, initialTranscript13, err := c.negotiateVersionClient(ctx)
- if err != nil {
- return handshakeStart{}, err
- }
+ return handshakeStart{flight13: dtlsflight13.Flight0, fsmState: dtlshandshake.StatePreparing}
+}
- primer := func(ctx context.Context) {
- go c.primeHandshakeRecv(ctx)
- }
+func (c *Conn) prepareDualStackClientHandshakeStart(ctx context.Context) (handshakeStart, error) {
+ initialFlights, err := c.negotiateVersionClient(ctx)
+ if err != nil {
+ return handshakeStart{}, err
+ }
- return handshakeStart{
- flight: flightVal(flight13_1),
- fsmState: handshakeWaiting,
- flights: initialFlights,
- transcript13: initialTranscript13,
- postSetup: primer,
- }, nil
- default:
- err := c.negotiateVersionServer(ctx)
- if err != nil {
- return handshakeStart{}, err
- }
+ return handshakeStart{flight12: dtlsflight12.Flight1, flight13: dtlsflight13.Flight1, fsmState: dtlshandshake.StateWaiting, flights: initialFlights, postSetup: func(ctx context.Context) { c.primeHandshakeRecv(ctx) }}, nil
+}
- return handshakeStart{flight: flightVal(flight13_0), fsmState: handshakePreparing}, nil
+func (c *Conn) prepareDualStackServerHandshakeStart(ctx context.Context) (handshakeStart, error) {
+ err := c.negotiateVersionServer(ctx)
+ if err != nil {
+ return handshakeStart{}, err
}
+
+ return handshakeStart{
+ flight12: dtlsflight12.Flight0,
+ flight13: dtlsflight13.Flight0,
+ fsmState: dtlshandshake.StatePreparing,
+ postSetup: func(ctx context.Context) { c.primeHandshakeRecv(ctx) },
+ }, nil
}
func dialWithConfig(network string, rAddr *net.UDPAddr, config *dtlsConfig) (*Conn, error) {
@@ -437,8 +510,8 @@ func dialWithConfig(network string, rAddr *net.UDPAddr, config *dtlsConfig) (*Co
return clientWithConfig(pConn, rAddr, config)
}
-// DialWithOptions connects to the given network address and establishes a DTLS connection on top.
-func DialWithOptions(network string, rAddr *net.UDPAddr, opts ...ClientOption) (*Conn, error) {
+// Dial connects to the given network address and establishes a DTLS connection on top.
+func Dial(network string, rAddr *net.UDPAddr, opts ...ClientOption) (*Conn, error) {
config, err := buildClientConfig(opts...)
if err != nil {
return nil, err
@@ -448,11 +521,8 @@ func DialWithOptions(network string, rAddr *net.UDPAddr, opts ...ClientOption) (
}
func clientWithConfig(conn net.PacketConn, rAddr net.Addr, config *dtlsConfig) (*Conn, error) {
- switch {
- case config == nil:
+ if config == nil {
return nil, dtlserrors.ErrNoConfigProvided
- case config.psk != nil && config.PSKIdentityHint == nil:
- return nil, dtlserrors.ErrPSKAndIdentityMustBeSetForClient
}
if err := validateConfig(config); err != nil {
@@ -462,14 +532,14 @@ func clientWithConfig(conn net.PacketConn, rAddr net.Addr, config *dtlsConfig) (
return createConn(conn, rAddr, config, true, nil)
}
-// ClientWithOptions establishes a DTLS connection over an existing connection.
-func ClientWithOptions(conn net.PacketConn, rAddr net.Addr, opts ...ClientOption) (*Conn, error) {
+// Client establishes a DTLS connection over an existing packet connection.
+func Client(conn net.PacketConn, raddr net.Addr, opts ...ClientOption) (*Conn, error) {
config, err := buildClientConfig(opts...)
if err != nil {
return nil, err
}
- return clientWithConfig(conn, rAddr, config)
+ return clientWithConfig(conn, raddr, config)
}
func serverWithConfig(conn net.PacketConn, rAddr net.Addr, config *dtlsConfig) (*Conn, error) {
@@ -485,29 +555,24 @@ func serverWithConfig(conn net.PacketConn, rAddr net.Addr, config *dtlsConfig) (
return createConn(conn, rAddr, config, false, nil)
}
-func serverWithValidatedConfig(conn net.PacketConn, rAddr net.Addr, config *dtlsConfig) (*Conn, error) {
- if config == nil {
- return nil, dtlserrors.ErrNoConfigProvided
- }
-
- if err := validateConfig(config); err != nil {
+// Server establishes a server-side DTLS connection over an existing packet connection.
+func Server(conn net.PacketConn, raddr net.Addr, opts ...ServerOption) (*Conn, error) {
+ config, err := buildServerConfig(opts...)
+ if err != nil {
return nil, err
}
- return serverWithConfig(conn, rAddr, config)
-}
-
-// ServerWithOptions listens for incoming DTLS connections.
-func ServerWithOptions(conn net.PacketConn, rAddr net.Addr, opts ...ServerOption) (*Conn, error) {
- config, err := buildServerConfig(opts...)
- if err != nil {
+ if err := validateConfig(config); err != nil {
return nil, err
}
- return serverWithValidatedConfig(conn, rAddr, config)
+ return serverWithConfig(conn, raddr, config)
}
// Read reads data from the connection.
+// Before the handshake completes, Read also performs handshake reads and writes.
+// Set both read and write deadlines (or use SetDeadline) to bound that I/O,
+// or call HandshakeContext explicitly. Deadlines are not reset after the handshake.
func (c *Conn) Read(buff []byte) (n int, err error) { //nolint:cyclop
if err := c.Handshake(); err != nil {
return 0, err
@@ -521,6 +586,8 @@ func (c *Conn) Read(buff []byte) (n int, err error) { //nolint:cyclop
for {
select {
+ case <-c.closed.Done():
+ return 0, io.EOF
case <-c.readDeadline.Done():
return 0, dtlserrors.ErrDeadlineExceeded
case out, ok := <-c.decrypted:
@@ -543,6 +610,9 @@ func (c *Conn) Read(buff []byte) (n int, err error) { //nolint:cyclop
}
// Write writes len(payload) bytes from payload to the DTLS connection.
+// Before the handshake completes, Write also performs handshake reads and writes.
+// Set both read and write deadlines (or use SetDeadline) to bound that I/O,
+// or call HandshakeContext explicitly. Deadlines are not reset after the handshake.
func (c *Conn) Write(payload []byte) (int, error) {
if c.isConnectionClosed() {
return 0, ErrConnClosed
@@ -558,28 +628,122 @@ func (c *Conn) Write(payload []byte) (int, error) {
return 0, err
}
- //nolint:godox
- // TODO: check for version
- return len(payload), c.writePackets(c.writeDeadline, []*packet{
- {
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Epoch: c.state.GetLocalEpoch(),
- Version: protocol.Version1_2,
- },
- Content: &protocol.ApplicationData{
- Data: payload,
- },
- },
- shouldWrapCID: len(c.state.RemoteConnectionID) > 0,
- shouldEncrypt: true,
- },
+ ctx, cancel := c.contextWithClose(c.writeDeadline.Context())
+ defer cancel()
+
+ err := c.writeApplicationData(ctx, []*dtlsflight.Outbound{
+ c.newApplicationDataPacket(payload),
})
+ if errors.Is(err, context.Canceled) && errors.Is(context.Cause(ctx), context.DeadlineExceeded) {
+ return 0, dtlserrors.ErrDeadlineExceeded
+ }
+ if err != nil {
+ return 0, err
+ }
+
+ return len(payload), nil
+}
+
+func (c *Conn) newApplicationDataPacket(payload []byte) *dtlsflight.Outbound {
+ return &dtlsflight.Outbound{
+ Content: &protocol.ApplicationData{
+ // The DTLS 1.3 FSM may retain this packet after Write returns on
+ // cancellation, so take ownership before queueing it.
+ Data: bytes.Clone(payload),
+ },
+ Protection: dtlsflight.ProtectionCiphertext,
+ }
+}
+
+// KeyUpdateOptions controls a DTLS 1.3 application traffic-key update.
+type KeyUpdateOptions struct {
+ // RequestPeerUpdate asks the peer to update its sending keys in response.
+ RequestPeerUpdate bool
+}
+
+// UpdateKeys requests a DTLS 1.3 application traffic-key update. It returns
+// only after the peer acknowledges the KeyUpdate and the next local write
+// generation has been committed.
+func (c *Conn) UpdateKeys(ctx context.Context, options KeyUpdateOptions) error {
+ updater, err := c.keyUpdateFSM(ctx)
+ if err != nil {
+ return err
+ }
+ request := handshake.KeyUpdateNotRequested
+ if options.RequestPeerUpdate {
+ request = handshake.KeyUpdateRequested
+ }
+
+ operationCtx, cancel := c.contextWithCloseAndWriteDeadline(ctx)
+ defer cancel()
+
+ return c.normalizeKeyUpdateError(ctx, operationCtx, updater.UpdateKeys(operationCtx, request))
+}
+
+func (c *Conn) keyUpdateFSM(ctx context.Context) (dtlshandshake.KeyUpdater, error) {
+ if c.isConnectionClosed() {
+ return nil, ErrConnClosed
+ }
+ select {
+ case <-c.writeDeadline.Done():
+ return nil, dtlserrors.ErrDeadlineExceeded
+ default:
+ }
+ if err := c.HandshakeContext(ctx); err != nil {
+ return nil, err
+ }
+ if dtlsstate.CommonState(c.state).LocalVersion != protocol.Version1_3 {
+ return nil, dtlserrors.ErrUnsupportedProtocolVersion
+ }
+
+ updater, ok := c.fsm.(dtlshandshake.KeyUpdater)
+ if !ok {
+ return nil, dtlserrors.ErrNotImplemented
+ }
+
+ return updater, nil
+}
+
+func (c *Conn) normalizeKeyUpdateError(ctx, operationCtx context.Context, err error) error {
+ if errors.Is(err, context.Canceled) {
+ switch {
+ case c.isConnectionClosed():
+ return ErrConnClosed
+ case errors.Is(context.Cause(operationCtx), context.DeadlineExceeded):
+ return dtlserrors.ErrDeadlineExceeded
+ case ctx.Err() != nil:
+ return ctx.Err()
+ }
+ }
+
+ return err
+}
+
+func (c *Conn) writeApplicationData(ctx context.Context, pkts []*dtlsflight.Outbound) error {
+ if dtlsstate.CommonState(c.state).LocalVersion == protocol.Version1_3 {
+ writer, ok := c.fsm.(dtlshandshake.ApplicationDataWriter)
+ if !ok {
+ return dtlserrors.ErrNotImplemented
+ }
+
+ return writer.WriteApplicationData(ctx, pkts)
+ }
+
+ epoch := dtlsstate.CommonState(c.state).LocalEpoch()
+ for _, pkt := range pkts {
+ pkt.Epoch = epoch
+ }
+ _, err := c.writePacketsWithResult(ctx, pkts)
+
+ return err
}
// Close closes the connection.
func (c *Conn) Close() error {
- err := c.close(true) //nolint:contextcheck
+ err := c.close(true)
+ if c.detached != nil {
+ c.detached.terminate(ErrConnClosed, false)
+ }
c.closeLock.Lock()
handshakeDone := c.handshakeDone
c.closeLock.Unlock()
@@ -593,19 +757,24 @@ func (c *Conn) Close() error {
// ConnectionState returns basic DTLS details about the connection.
// Note that this replaced the `Export` function of v1.
func (c *Conn) ConnectionState() (State, bool) {
+ c.writeLock.Lock()
+ defer c.writeLock.Unlock()
c.lock.RLock()
defer c.lock.RUnlock()
- state, err := generateState(&c.state)
+ state, err := generateState(c.state)
if err != nil {
return State{}, false
}
+ if state13, ok := c.state.(*dtlsstate.State13); ok && c.handshakeEstablished != nil && c.isHandshakeCompletedSuccessfully() {
+ state.state13 = snapshotState13(state13)
+ }
return *state, true
}
// SelectedSRTPProtectionProfile returns the selected SRTPProtectionProfile.
func (c *Conn) SelectedSRTPProtectionProfile() (SRTPProtectionProfile, bool) {
- profile := c.state.GetSRTPProtectionProfile()
+ profile := dtlsstate.CommonState(c.state).SRTPProtectionProfile()
if profile == 0 {
return 0, false
}
@@ -615,659 +784,1839 @@ func (c *Conn) SelectedSRTPProtectionProfile() (SRTPProtectionProfile, bool) {
// RemoteSRTPMasterKeyIdentifier returns the MasterKeyIdentifier value from the use_srtp.
func (c *Conn) RemoteSRTPMasterKeyIdentifier() ([]byte, bool) {
- if profile := c.state.GetSRTPProtectionProfile(); profile == 0 {
+ common := dtlsstate.CommonState(c.state)
+ if profile := common.SRTPProtectionProfile(); profile == 0 {
return nil, false
}
- return c.state.RemoteSRTPMasterKeyIdentifier, true
+ return bytes.Clone(common.RemoteSRTPMasterKeyIdentifier), true
}
-func (c *Conn) writePackets(ctx context.Context, pkts []*packet) error {
- c.lock.Lock()
- defer c.lock.Unlock()
+func (c *Conn) writePackets(ctx context.Context, pkts []*dtlsflight.Outbound) error {
+ _, err := c.writePacketsWithResult(ctx, pkts)
- var rawPackets [][]byte
+ return err
+}
- for _, pkt := range pkts {
- if dtlsHandshake, ok := pkt.record.Content.(*handshake.Handshake); ok {
- handshakeRaw, err := pkt.record.Marshal()
- if err != nil {
- return err
- }
+func (c *Conn) writePacketsWithResult(ctx context.Context, pkts []*dtlsflight.Outbound) (*dtlshandshake.WriteResult, error) {
+ c.writeLock.Lock()
+ defer c.writeLock.Unlock()
- c.log.Tracef("[handshake:%v] -> %s (epoch: %d, seq: %d)",
- srvCliStr(c.state.IsClient), dtlsHandshake.Header.Type.String(),
- pkt.record.Header.Epoch, dtlsHandshake.Header.MessageSequence)
+ return c.writePacketsWithResultLocked(ctx, pkts)
+}
- c.handshakeCache.push(
- handshakeRaw[recordlayer.FixedHeaderSize:],
- pkt.record.Header.Epoch,
- dtlsHandshake.Header.MessageSequence,
- dtlsHandshake.Header.Type,
- c.state.IsClient,
- )
+func (c *Conn) writePacketsWithResultLocked(ctx context.Context, pkts []*dtlsflight.Outbound) (*dtlshandshake.WriteResult, error) {
+ datagrams, rAddr, err := c.prepareRawPacketsTracked(pkts)
+ if err != nil {
+ return nil, err
+ }
- rawHandshakePackets, err := c.processHandshakePacket(pkt, dtlsHandshake)
- if err != nil {
- return err
- }
- rawPackets = append(rawPackets, rawHandshakePackets...)
- } else {
- rawPacket, err := c.processPacket(pkt)
- if err != nil {
- return err
- }
- rawPackets = append(rawPackets, rawPacket)
+ result := &dtlshandshake.WriteResult{}
+ if c.detached != nil {
+ if len(datagrams) == 0 {
+ return result, nil
}
+ raw := make([][]byte, len(datagrams))
+ for i := range datagrams {
+ raw[i] = datagrams[i].raw
+ result.TrackedRecords = append(result.TrackedRecords, datagrams[i].tracked...)
+ }
+ c.detached.publishDatagrams(raw, rAddr)
+
+ return result, nil
}
- if len(rawPackets) == 0 {
- return nil
- }
- compactedRawPackets := c.compactRawPackets(rawPackets)
+ for _, datagram := range datagrams {
+ if err = c.writeDatagram(ctx, datagram.raw, rAddr); err != nil {
+ if errors.Is(err, context.Canceled) && c.isConnectionClosed() {
+ return nil, ErrConnClosed
+ }
- for _, compactedRawPackets := range compactedRawPackets {
- if _, err := c.nextConn.WriteToContext(ctx, compactedRawPackets, c.rAddr); err != nil {
- return netError(err)
+ return nil, netError(err)
}
+ result.TrackedRecords = append(result.TrackedRecords, datagram.tracked...)
}
- return nil
+ return result, nil
}
-func (c *Conn) compactRawPackets(rawPackets [][]byte) [][]byte {
- // avoid a useless copy in the common case
- if len(rawPackets) == 1 {
- return rawPackets
+func (c *Conn) writeDatagram(ctx context.Context, b []byte, addr net.Addr) error {
+ writeCtx, cancel := c.contextWithHandshakeDeadline(ctx, c.writeDeadline)
+ defer cancel()
+
+ err := writeCtx.Err()
+ if err == nil {
+ _, err = c.nextConn.WriteToContext(writeCtx, b, addr)
+ }
+ if errors.Is(err, context.Canceled) && ctx.Err() == nil {
+ return context.Cause(writeCtx)
}
- combinedRawPackets := make([][]byte, 0)
- currentCombinedRawPacket := make([]byte, 0)
+ return err
+}
- for _, rawPacket := range rawPackets {
- if len(currentCombinedRawPacket) > 0 && len(currentCombinedRawPacket)+len(rawPacket) >= c.maximumTransmissionUnit {
- combinedRawPackets = append(combinedRawPackets, currentCombinedRawPacket)
- currentCombinedRawPacket = []byte{}
+// contextWithHandshakeDeadline applies a directional deadline only while the
+// initial handshake is in progress.
+func (c *Conn) contextWithHandshakeDeadline(ctx context.Context, d *deadline.Deadline) (context.Context, context.CancelFunc) {
+ if c.isHandshakeCompletedSuccessfully() {
+ return ctx, func() {}
+ }
+ ioCtx, cancel := context.WithCancelCause(ctx)
+ expire := func() {
+ if !c.isHandshakeCompletedSuccessfully() {
+ cancel(context.DeadlineExceeded)
}
- currentCombinedRawPacket = append(currentCombinedRawPacket, rawPacket...)
}
+ stop := context.AfterFunc(d.Context(), expire)
+ if d.Err() != nil {
+ expire()
+ }
+
+ return ioCtx, func() {
+ stop()
+ cancel(context.Canceled)
+ }
+}
+
+func (c *Conn) readDatagram(ctx context.Context, b []byte) (int, net.Addr, error) {
+ for {
+ readCtx, cancel := c.contextWithHandshakeDeadline(ctx, c.readDeadline)
+ err := readCtx.Err()
+ var n int
+ var addr net.Addr
+ if err == nil {
+ n, addr, err = c.nextConn.ReadFromContext(readCtx, b)
+ }
+ deadlineCanceled := errors.Is(err, context.Canceled) && ctx.Err() == nil &&
+ errors.Is(context.Cause(readCtx), context.DeadlineExceeded)
+ if errors.Is(err, context.Canceled) && ctx.Err() == nil {
+ err = context.Cause(readCtx)
+ }
+ cancel()
+ if n == 0 && deadlineCanceled && c.isHandshakeCompletedSuccessfully() {
+ continue
+ }
- combinedRawPackets = append(combinedRawPackets, currentCombinedRawPacket)
+ return n, addr, err
+ }
+}
- return combinedRawPackets
+type preparedDatagram struct {
+ raw []byte
+ tracked []dtlshandshake.SentHandshakeRecord
}
-func (c *Conn) processPacket(pkt *packet) ([]byte, error) { //nolint:cyclop
- epoch := pkt.record.Header.Epoch
- for len(c.state.LocalSequenceNumber) <= int(epoch) {
- c.state.LocalSequenceNumber = append(c.state.LocalSequenceNumber, uint64(0))
+func (c *Conn) prepareRawPacketsTracked(pkts []*dtlsflight.Outbound) ([]preparedDatagram, net.Addr, error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ records, err := c.prepareRecordsTracked(pkts)
+ if err != nil {
+ return nil, nil, err
}
- seq := atomic.AddUint64(&c.state.LocalSequenceNumber[epoch], 1) - 1
- if seq > recordlayer.MaxSequenceNumber {
- // RFC 6347 Section 4.1.0
- // The implementation must either abandon an association or rehandshake
- // prior to allowing the sequence number to wrap.
- return nil, dtlserrors.ErrSequenceNumberOverflow
+ if len(records) == 0 {
+ return nil, nil, nil
}
- pkt.record.Header.SequenceNumber = seq
- var rawPacket []byte
- if pkt.shouldWrapCID { //nolint:nestif
- // Record must be marshaled to populate fields used in inner plaintext.
- if _, err := pkt.record.Marshal(); err != nil {
- return nil, err
- }
- content, err := pkt.record.Content.Marshal()
+ return c.compactPreparedRecords(records), c.rAddr, nil
+}
+
+func (c *Conn) prepareRecordsTracked(pkts []*dtlsflight.Outbound) ([]preparedRecord, error) {
+ var records []preparedRecord
+ for _, pkt := range pkts {
+ prepared, err := c.prepareOutbound(pkt)
if err != nil {
return nil, err
}
- inner := &recordlayer.InnerPlaintext{
- Content: content,
- RealType: pkt.record.Header.ContentType,
- }
- rawInner, err := inner.Marshal() //nolint:govet
- if err != nil {
- return nil, err
- }
- cidHeader := &recordlayer.Header{
- Version: pkt.record.Header.Version,
- ContentType: protocol.ContentTypeConnectionID,
- Epoch: pkt.record.Header.Epoch,
- ContentLen: uint16(len(rawInner)), //nolint:gosec //G115
- ConnectionID: c.state.RemoteConnectionID,
- SequenceNumber: pkt.record.Header.SequenceNumber,
- }
- rawPacket, err = cidHeader.Marshal()
- if err != nil {
+ records = append(records, prepared...)
+ }
+
+ return records, nil
+}
+
+func (c *Conn) prepareOutbound(outbound *dtlsflight.Outbound) ([]preparedRecord, error) {
+ if outbound == nil || outbound.Content == nil || !validProtection(outbound.Protection) {
+ return nil, dtlserrors.ErrInvalidPacket
+ }
+ if dtlsHandshake, ok := outbound.Content.(*handshake.Handshake); ok {
+ if err := c.cacheHandshake(outbound, dtlsHandshake); err != nil {
return nil, err
}
- pkt.record.Header = *cidHeader
- rawPacket = append(rawPacket, rawInner...)
- } else {
- var err error
- rawPacket, err = pkt.record.Marshal()
- if err != nil {
+ if err := c.registerLocalCID(); err != nil {
return nil, err
}
+
+ return c.prepareHandshakeRecords(outbound, dtlsHandshake)
}
- if pkt.shouldEncrypt {
- var err error
- rawPacket, err = c.state.CipherSuite.Encrypt(pkt.record, rawPacket)
- if err != nil {
- return nil, err
- }
+ raw, err := c.prepareRecord(outbound)
+ if err != nil {
+ return nil, err
}
- return rawPacket, nil
+ return []preparedRecord{{raw: raw}}, nil
}
-//nolint:cyclop
-func (c *Conn) processHandshakePacket(pkt *packet, dtlsHandshake *handshake.Handshake) ([][]byte, error) {
- rawPackets := make([][]byte, 0)
-
- handshakeFragments, err := c.fragmentHandshake(dtlsHandshake)
+func (c *Conn) cacheHandshake(outbound *dtlsflight.Outbound, dtlsHandshake *handshake.Handshake) error {
+ handshakeRaw, err := dtlsHandshake.Marshal()
if err != nil {
- return nil, err
- }
- epoch := pkt.record.Header.Epoch
- for len(c.state.LocalSequenceNumber) <= int(epoch) {
- c.state.LocalSequenceNumber = append(c.state.LocalSequenceNumber, uint64(0))
+ return err
}
- for _, handshakeFragment := range handshakeFragments {
- seq := atomic.AddUint64(&c.state.LocalSequenceNumber[epoch], 1) - 1
- if seq > recordlayer.MaxSequenceNumber {
- return nil, dtlserrors.ErrSequenceNumberOverflow
- }
+ c.log.Tracef("[handshake:%v] -> %s (epoch: %d, seq: %d)", srvCliStr(dtlsstate.CommonState(c.state).IsClient), dtlsHandshake.Header.Type.String(), outbound.Epoch, dtlsHandshake.Header.MessageSequence)
- var rawPacket []byte
- if pkt.shouldWrapCID {
- inner := &recordlayer.InnerPlaintext{
- Content: handshakeFragment,
- RealType: protocol.ContentTypeHandshake,
- Zeros: c.paddingLengthGenerator(uint(len(handshakeFragment))),
- }
- rawInner, err := inner.Marshal() //nolint:govet
- if err != nil {
- return nil, err
- }
- cidHeader := &recordlayer.Header{
- Version: pkt.record.Header.Version,
- ContentType: protocol.ContentTypeConnectionID,
- Epoch: pkt.record.Header.Epoch,
- ContentLen: uint16(len(rawInner)), //nolint:gosec //G115
- ConnectionID: c.state.RemoteConnectionID,
- SequenceNumber: pkt.record.Header.SequenceNumber,
- }
- rawPacket, err = cidHeader.Marshal()
- if err != nil {
- return nil, err
- }
- pkt.record.Header = *cidHeader
- rawPacket = append(rawPacket, rawInner...)
- } else {
- recordlayerHeader := &recordlayer.Header{
- Version: pkt.record.Header.Version,
- ContentType: pkt.record.Header.ContentType,
- ContentLen: uint16(len(handshakeFragment)), //nolint:gosec // G115
- Epoch: pkt.record.Header.Epoch,
- SequenceNumber: seq,
- }
+ return c.pushHandshake(handshakeRaw, outbound.Epoch, dtlsHandshake.Header.MessageSequence, dtlsHandshake.Header.Type, dtlsstate.CommonState(c.state).IsClient)
+}
- rawPacket, err = recordlayerHeader.Marshal()
- if err != nil {
- return nil, err
- }
+func (c *Conn) contextWithClose(ctx context.Context) (context.Context, context.CancelFunc) {
+ closeCtx, cancel := context.WithCancelCause(ctx)
- pkt.record.Header = *recordlayerHeader
- rawPacket = append(rawPacket, handshakeFragment...)
+ detachLifetime := context.AfterFunc(c.closed, func() {
+ err := c.closed.Err()
+ if err == nil {
+ err = context.Canceled
}
+ cancel(err)
+ })
- if pkt.shouldEncrypt {
- var err error
- rawPacket, err = c.state.CipherSuite.Encrypt(pkt.record, rawPacket)
- if err != nil {
- return nil, err
- }
+ detachDeadline := context.AfterFunc(c.writeDeadline.Context(), func() {
+ err := c.writeDeadline.Err()
+ if err == nil {
+ err = context.DeadlineExceeded
}
+ cancel(err)
+ })
- rawPackets = append(rawPackets, rawPacket)
+ return closeCtx, func() {
+ detachLifetime()
+ detachDeadline()
+ cancel(context.Canceled)
}
-
- return rawPackets, nil
}
-func (c *Conn) fragmentHandshake(dtlsHandshake *handshake.Handshake) ([][]byte, error) {
- content, err := dtlsHandshake.Message.Marshal()
- if err != nil {
- return nil, err
- }
+func (c *Conn) contextWithCloseAndWriteDeadline(ctx context.Context) (context.Context, context.CancelFunc) {
+ operationCtx, cancel := context.WithCancelCause(context.Background())
+
+ detachLifetime := context.AfterFunc(c.closed, func() {
+ err := c.closed.Err()
+ if err == nil {
+ err = context.Canceled
+ }
+ cancel(err)
+ })
- fragmentedHandshakes := make([][]byte, 0)
+ detachDeadline := context.AfterFunc(c.writeDeadline.Context(), func() {
+ err := c.writeDeadline.Err()
+ if err == nil {
+ err = context.DeadlineExceeded
+ }
+ cancel(err)
+ })
- contentFragments := splitBytes(content, c.maximumTransmissionUnit)
- if len(contentFragments) == 0 {
- contentFragments = [][]byte{
- {},
+ detachCtx := context.AfterFunc(ctx, func() {
+ err := ctx.Err()
+ if err == nil {
+ err = context.Canceled
}
+ cancel(err)
+ })
+
+ return operationCtx, func() {
+ detachLifetime()
+ detachDeadline()
+ detachCtx()
+ cancel(context.Canceled)
+ }
+}
+
+func (c *Conn) compactPreparedRecords(records []preparedRecord) []preparedDatagram {
+ if len(records) == 0 {
+ return []preparedDatagram{}
+ }
+
+ totalSize := 0
+ for _, record := range records {
+ totalSize += len(record.raw)
}
+ datagrams := make([]preparedDatagram, len(records))
+ flatRaw := make([]byte, 0, totalSize)
+
+ datagramIndex := 0
+ currentSize := 0
offset := 0
- for _, contentFragment := range contentFragments {
- contentFragmentLen := len(contentFragment)
- headerFragment := &handshake.Header{
- Type: dtlsHandshake.Header.Type,
- Length: dtlsHandshake.Header.Length,
- MessageSequence: dtlsHandshake.Header.MessageSequence,
- FragmentOffset: uint32(offset), //nolint:gosec // G115
- FragmentLength: uint32(contentFragmentLen), //nolint:gosec // G115
+ for _, record := range records {
+ recordSize := len(record.raw)
+
+ flatRaw = append(flatRaw, record.raw...)
+
+ if currentSize > 0 && currentSize+recordSize >= c.maximumTransmissionUnit {
+ end := offset + currentSize
+ datagrams[datagramIndex].raw = flatRaw[offset:end:end]
+ datagramIndex++
+ offset += currentSize
+ currentSize = 0
}
- offset += contentFragmentLen
+ currentSize += recordSize
- fragmentedHandshake, err := headerFragment.Marshal()
- if err != nil {
- return nil, err
+ if record.tracked != nil {
+ datagrams[datagramIndex].tracked = append(
+ datagrams[datagramIndex].tracked,
+ *record.tracked,
+ )
}
+ }
+
+ end := offset + currentSize
+ datagrams[datagramIndex].raw = flatRaw[offset:end:end]
+
+ return datagrams[:datagramIndex+1]
+}
+
+func (c *Conn) prepareRecord(outbound *dtlsflight.Outbound) ([]byte, error) {
+ if outbound == nil || outbound.Content == nil || !validProtection(outbound.Protection) {
+ return nil, dtlserrors.ErrInvalidPacket
+ }
+ contentType, plaintext, err := marshalRecordContent(outbound.Content)
+ if err != nil {
+ return nil, err
+ }
- fragmentedHandshake = append(fragmentedHandshake, contentFragment...)
- fragmentedHandshakes = append(fragmentedHandshakes, fragmentedHandshake)
+ epoch := outbound.Epoch
+ seq, err := c.allocateLocalSequenceNumber(epoch)
+ if err != nil {
+ return nil, err
}
- return fragmentedHandshakes, nil
+ return c.encodeRecord(epoch, seq, contentType, plaintext, outbound.Protection)
}
-var poolReadBuffer = sync.Pool{ //nolint:gochecknoglobals
- New: func() any {
- b := make([]byte, inboundBufferSize)
+func (c *Conn) allocateLocalSequenceNumber(epoch uint64) (uint64, error) {
+ if c.exporting.Load() {
+ return 0, ErrConnClosed
+ }
+ common := dtlsstate.CommonState(c.state)
+ if common.LocalVersion != protocol.Version1_3 && epoch > 0xffff {
+ return 0, dtlserrors.ErrEpochOverflow
+ }
- return &b
- },
+ return common.AllocateLocalSequenceNumber(epoch, recordlayer.MaxSequenceNumber)
}
-func (c *Conn) readAndBuffer(ctx context.Context) error { //nolint:cyclop,gocognit
- bufptr, ok := poolReadBuffer.Get().(*[]byte)
- if !ok {
- return dtlserrors.ErrFailedToAccessPoolReadBuffer
+func marshalRecordContent(content protocol.Content) (protocol.ContentType, []byte, error) {
+ switch content.(type) {
+ case *handshake.Handshake, *alert.Alert, *protocol.ApplicationData, *protocol.ACK,
+ *protocol.ChangeCipherSpec,
+ *protocol.ReturnRoutabilityCheck:
+ default:
+ return 0, nil, dtlserrors.ErrCipherSuiteRecordProtectionNotImplemented
}
- defer poolReadBuffer.Put(bufptr)
- b := *bufptr
- i, rAddr, err := c.nextConn.ReadFromContext(ctx, b)
+ plaintext, err := content.Marshal()
if err != nil {
- return netError(err)
+ return 0, nil, err
+ }
+ if len(plaintext) > maxPlaintextRecordLen {
+ return 0, nil, dtlserrors.ErrInvalidPacketLength
}
- pkts, err := recordlayer.ContentAwareUnpackDatagram(b[:i], len(c.state.GetLocalConnectionID()))
- if err != nil {
- return err
+ return content.ContentType(), plaintext, nil
+}
+
+func validProtection(protection dtlsflight.Protection) bool {
+ return protection == dtlsflight.ProtectionPlaintext || protection == dtlsflight.ProtectionCiphertext
+}
+
+func (c *Conn) encodeRecord( //nolint:cyclop
+ epoch uint64,
+ seq uint64,
+ contentType protocol.ContentType,
+ plaintext []byte,
+ protection dtlsflight.Protection,
+) ([]byte, error) {
+ if !validProtection(protection) {
+ return nil, dtlserrors.ErrInvalidPacket
+ }
+ if len(plaintext) > maxPlaintextRecordLen {
+ return nil, dtlserrors.ErrInvalidPacketLength
+ }
+ common := dtlsstate.CommonState(c.state)
+ if protection == dtlsflight.ProtectionCiphertext && common.LocalVersion == protocol.Version1_3 {
+ return c.sealRecordContent(epoch, seq, contentType, plaintext)
}
- var hasHandshake, isRetransmit bool
- for _, p := range pkts {
- //nolint:godox
- // TODO: check version
- hs, rtx, alert, err := c.handleIncomingPacket(ctx, p, rAddr, true)
- if alert != nil {
- if alertErr := c.notify(ctx, alert.Level, alert.Description); alertErr != nil {
- if err == nil {
- err = alertErr
- }
- }
+ if epoch > 0xffff {
+ return nil, dtlserrors.ErrEpochOverflow
+ }
+ header := recordlayer.RecordConfig{
+ Version: protocol.Version1_2,
+ ContentType: contentType,
+ Epoch: uint16(epoch), //nolint:gosec // Checked before fixed-header encoding.
+ SequenceNumber: seq,
+ }
+ payload := plaintext
+ if protection == dtlsflight.ProtectionCiphertext && common.LocalVersion == protocol.Version1_2 && c.state.ShouldWrapConnectionID() {
+ if len(plaintext)+1 > maxCIDInnerPlaintextLen {
+ return nil, dtlserrors.ErrInvalidPacketLength
}
-
- var e *alertError
- if errors.As(err, &e) && e.IsFatalOrCloseNotify() {
- return e
+ paddingLen := c.paddingLengthGenerator(uint(len(plaintext)))
+ if paddingLen > uint(maxCIDInnerPlaintextLen-len(plaintext)-1) { //nolint:gosec // Non-negative and bounded.
+ return nil, dtlserrors.ErrInvalidPacketLength
}
+ var err error
+ payload, err = recordlayer.MarshalInnerPlaintext(plaintext, contentType, int(paddingLen)) //nolint:gosec // Bounded by maxCIDInnerPlaintextLen above.
if err != nil {
- return err
- }
- if hs {
- hasHandshake = true
+ return nil, err
}
- if rtx {
- isRetransmit = true
+ if len(payload) > maxCIDInnerPlaintextLen {
+ return nil, dtlserrors.ErrInvalidPacketLength
}
+ header.ContentType = protocol.ContentTypeConnectionID
+ header.ConnectionID = bytes.Clone(common.RemoteConnectionID)
}
- if hasHandshake {
- s := recvHandshakeState{
- done: make(chan struct{}),
- isRetransmit: isRetransmit,
- }
- select {
- case c.handshakeRecv <- s:
- // If the other party may retransmit the flight,
- // we should respond even if it not a new message.
- <-s.done
- case <-c.fsm.Done():
- }
+ if protection != dtlsflight.ProtectionCiphertext {
+ return recordlayer.MarshalRecord(header, payload)
+ }
+ if common.CipherSuite == nil {
+ return nil, dtlserrors.ErrCipherSuiteNotInit
+ }
+ state12, ok := c.state.(*dtlsstate.State12)
+ if !ok || state12.Protection == nil {
+ return nil, dtlserrors.ErrCipherSuiteNotInit
+ }
+ metadata, err := dtlsciphersuite.NewLegacyRecord(header.ContentType, header.Version, header.Epoch, header.SequenceNumber, header.ConnectionID)
+ if err != nil {
+ return nil, err
+ }
+ expectedLen, err := common.CipherSuite.Capabilities().ProtectedLen(len(payload))
+ if err != nil {
+ return nil, err
+ }
+ protected, err := state12.Seal(metadata, payload)
+ if err != nil {
+ return nil, err
+ }
+ if len(protected) != expectedLen {
+ return nil, cryptosuite.ErrInvalidCapabilities
}
- return nil
+ return recordlayer.MarshalRecord(header, protected)
}
-func (c *Conn) handleQueuedPackets(ctx context.Context) error {
- c.lock.Lock()
- pkts := c.encryptedPackets
- c.encryptedPackets = nil
- c.lock.Unlock()
+func (c *Conn) sealRecordContent( //nolint:cyclop
+ epoch uint64,
+ seq uint64,
+ contentType protocol.ContentType,
+ plaintext []byte,
+) ([]byte, error) {
+ if epoch == dtlsflight13.EpochEarlyData &&
+ (!dtlsstate.CommonState(c.state).IsClient || contentType != protocol.ContentTypeApplicationData) {
+ return nil, dtlserrors.ErrInvalidContentType
+ }
+ generation, err := c.writeTrafficGeneration(epoch)
+ if err != nil {
+ return nil, err
+ }
- for _, p := range pkts {
- //nolint:godox
- // TODO: check version
- _, _, alert, err := c.handleIncomingPacket(ctx, p.data, p.rAddr, false) // don't re-enqueue
- if alert != nil {
- if alertErr := c.notify(ctx, alert.Level, alert.Description); alertErr != nil {
- if err == nil {
- err = alertErr
- }
- }
+ innerPlaintext, err := recordlayer.MarshalInnerPlaintext(plaintext, contentType, 0)
+ if err != nil {
+ return nil, err
+ }
+ if len(innerPlaintext) > maxDTLS13InnerPlaintextLen {
+ return nil, dtlserrors.ErrInvalidPacketLength
+ }
+ header := recordlayer.CiphertextConfig{EpochLow: uint8(epoch & 0x3), SequenceNumber: uint16(seq & 0xffff), TwoByteSequence: true, LengthPresent: true}
+ if state13, ok := c.state.(*dtlsstate.State13); ok &&
+ state13.CID.Negotiated && state13.CID.Send.UseCID {
+ header.ConnectionID = bytes.Clone(state13.CID.Send.Active)
+ }
+
+ capabilities, err := generation.Capabilities(dtlsstate.CommonState(c.state).CipherSuite)
+ if err != nil {
+ return nil, err
+ }
+ if !capabilities.SupportsVersion(protocol.Version1_3) {
+ return nil, cryptosuite.ErrInvalidCapabilities
+ }
+ protectedLen, err := capabilities.ProtectedLen(len(innerPlaintext))
+ if err != nil {
+ return nil, err
+ }
+ // Record-number encryption requires at least 16 bytes of ciphertext, so
+ // senders MUST pad short plaintexts to produce a suitable-length ciphertext.
+ //
+ // https://www.rfc-editor.org/rfc/rfc9147#section-4.2.3
+ sampleLen := capabilities.MaskLen()
+ if sampleLen <= 0 {
+ return nil, cryptosuite.ErrInvalidCapabilities
+ }
+ if protectedLen < sampleLen {
+ innerPlaintext, err = recordlayer.MarshalInnerPlaintext(plaintext, contentType, sampleLen-protectedLen)
+ if err != nil {
+ return nil, err
}
- var e *alertError
- if errors.As(err, &e) && e.IsFatalOrCloseNotify() {
- return e
+ if len(innerPlaintext) > maxDTLS13InnerPlaintextLen {
+ return nil, dtlserrors.ErrInvalidPacketLength
}
+ protectedLen, err = capabilities.ProtectedLen(len(innerPlaintext))
if err != nil {
- return err
+ return nil, err
}
}
+ var headerBuffer [260]byte
+ clearHeader, err := recordwire.AppendUnifiedHeader(headerBuffer[:0], header.EpochLow, header.SequenceNumber, header.TwoByteSequence, header.ConnectionID, header.LengthPresent, protectedLen)
+ if err != nil {
+ return nil, err
+ }
+ metadata, err := dtlsciphersuite.NewUnifiedRecord(epoch, seq, clearHeader, protectedLen)
+ if err != nil {
+ return nil, err
+ }
+ protected, err := generation.Seal(metadata, innerPlaintext)
+ if err != nil {
+ return nil, err
+ }
+ if len(protected) != protectedLen {
+ return nil, cryptosuite.ErrInvalidCapabilities
+ }
+ if len(protected) < sampleLen {
+ return nil, dtlserrors.ErrBufferTooSmall
+ }
+ mask, err := generation.Protection.Mask(protected[:sampleLen])
+ if err != nil {
+ return nil, err
+ }
+ header.SequenceNumber, err = applySequenceNumberMask(
+ header.SequenceNumber,
+ true,
+ mask,
+ )
+ if err != nil {
+ return nil, err
+ }
- return nil
+ return recordlayer.MarshalCiphertext(header, protected)
}
-func (c *Conn) enqueueEncryptedPackets(packet addrPkt) bool {
- c.lock.Lock()
- defer c.lock.Unlock()
-
- if len(c.encryptedPackets) < maxAppDataPacketQueueSize {
- c.encryptedPackets = append(c.encryptedPackets, packet)
+func applySequenceNumberMask(
+ sequenceNumber uint16,
+ twoBytes bool,
+ mask []byte,
+) (uint16, error) {
+ if !twoBytes {
+ if len(mask) < 1 {
+ return 0, dtlserrors.ErrBufferTooSmall
+ }
- return true
+ return (sequenceNumber ^ uint16(mask[0])) & 0xff, nil
+ }
+ if len(mask) < 2 {
+ return 0, dtlserrors.ErrBufferTooSmall
}
- return false
+ return sequenceNumber ^ uint16(mask[0])<<8 ^ uint16(mask[1]), nil
}
-// nolint:unused
-func (c *Conn) handleIncomingPacket13(
- ctx context.Context,
- buf []byte,
- rAddr net.Addr,
- enqueue bool,
-) (bool, bool, *alert.Alert, error) {
- // Placeholder function
- return false, false, nil, nil
+func (c *Conn) writeTrafficGeneration(epoch uint64) (*dtlsstate.TrafficGeneration, error) {
+ state13, ok := c.state.(*dtlsstate.State13)
+ if !ok || state13.TrafficKeys == nil {
+ return nil, dtlserrors.ErrCipherSuiteRecordProtectionNotImplemented
+ }
+ generation, ok := state13.TrafficKeys.Write(epoch)
+ if !ok || generation.Protection == nil {
+ return nil, dtlserrors.ErrCipherSuiteRecordProtectionNotImplemented
+ }
+
+ return generation, nil
}
-//nolint:gocognit,gocyclo,cyclop,maintidx
-func (c *Conn) handleIncomingPacket(
- ctx context.Context,
- buf []byte,
- rAddr net.Addr,
- enqueue bool,
-) (bool, bool, *alert.Alert, error) {
- header := &recordlayer.Header{}
- // Set connection ID size so that records of content type tls12_cid will
- // be parsed correctly.
- if len(c.state.GetLocalConnectionID()) > 0 {
- header.ConnectionID = make([]byte, len(c.state.GetLocalConnectionID()))
- }
- if err := header.Unmarshal(buf); err != nil {
- // Decode error must be silently discarded
- // [RFC6347 Section-4.1.2.7]
- c.log.Debugf("discarded broken packet: %v", err)
+type preparedRecord struct {
+ raw []byte
+ tracked *dtlshandshake.SentHandshakeRecord
+}
- return false, false, nil, nil
+func (c *Conn) prepareHandshakeRecords(outbound *dtlsflight.Outbound, dtlsHandshake *handshake.Handshake) ([]preparedRecord, error) {
+ handshakeFragments, err := c.fragmentHandshake(dtlsHandshake)
+ if err != nil {
+ return nil, err
}
- // Validate epoch
- remoteEpoch := c.state.GetRemoteEpoch()
- if header.Epoch > remoteEpoch {
- if header.Epoch > remoteEpoch+1 {
- c.log.Debugf("discarded future packet (epoch: %d, seq: %d)",
- header.Epoch, header.SequenceNumber,
- )
- return false, false, nil, nil
+ rawPackets := make([]preparedRecord, 0, len(handshakeFragments))
+ epoch := outbound.Epoch
+ for _, handshakeFragment := range handshakeFragments {
+ selected, err := selectHandshakeFragment(outbound.HandshakeFragmentOffsets, handshakeFragment)
+ if err != nil {
+ return nil, err
}
- if enqueue {
- if ok := c.enqueueEncryptedPackets(addrPkt{rAddr, buf}); ok {
- c.log.Debug("received packet of next epoch, queuing packet")
- }
+ if !selected {
+ continue
+ }
+ seq, err := c.allocateLocalSequenceNumber(epoch)
+ if err != nil {
+ return nil, err
+ }
+ rawPacket, err := c.encodeRecord(
+ epoch,
+ seq,
+ protocol.ContentTypeHandshake,
+ handshakeFragment,
+ outbound.Protection,
+ )
+ if err != nil {
+ return nil, err
}
- return false, false, nil, nil
+ prepared := preparedRecord{raw: rawPacket}
+ if outbound.TrackACK {
+ fragmentHeader := &handshake.Header{}
+ if err = fragmentHeader.Unmarshal(handshakeFragment); err != nil {
+ return nil, err
+ }
+ prepared.tracked = &dtlshandshake.SentHandshakeRecord{
+ Number: protocol.RecordNumber{Epoch: epoch, SequenceNumber: seq},
+ Fragments: []dtlshandshake.SentHandshakeFragment{{MessageSequence: fragmentHeader.MessageSequence, Offset: fragmentHeader.FragmentOffset, Length: fragmentHeader.FragmentLength}},
+ }
+ }
+ rawPackets = append(rawPackets, prepared)
}
- // Anti-replay protection
- for len(c.state.ReplayDetector) <= int(header.Epoch) {
- c.state.ReplayDetector = append(c.state.ReplayDetector,
- replaydetector.New(c.replayProtectionWindow, recordlayer.MaxSequenceNumber),
- )
- }
- markPacketAsValid, ok := c.state.ReplayDetector[int(header.Epoch)].Check(header.SequenceNumber)
- if !ok {
- c.log.Debugf("discarded duplicated packet (epoch: %d, seq: %d)",
- header.Epoch, header.SequenceNumber,
- )
+ return rawPackets, nil
+}
- return false, false, nil, nil
+func selectHandshakeFragment(offsets map[uint32]uint32, raw []byte) (bool, error) {
+ if offsets == nil {
+ return true, nil
}
+ header := &handshake.Header{}
+ if err := header.Unmarshal(raw); err != nil {
+ return false, err
+ }
+ length, ok := offsets[header.FragmentOffset]
- // originalCID indicates whether the original record had content type
- // Connection ID.
- originalCID := false
+ return ok && length == header.FragmentLength, nil
+}
- // Decrypt
- if header.Epoch != 0 { //nolint:nestif
- if c.state.CipherSuite == nil || !c.state.CipherSuite.IsInitialized() {
- if enqueue {
- if ok := c.enqueueEncryptedPackets(addrPkt{rAddr, buf}); ok {
- c.log.Debug("handshake not finished, queuing packet")
- }
- }
+var noContentFragments = [][]byte{ //nolint:gochecknoglobals
+ {},
+}
- return false, false, nil, nil
- }
+func (c *Conn) fragmentHandshake(dtlsHandshake *handshake.Handshake) ([][]byte, error) {
+ messageSize := dtlsHandshake.Message.MarshalSize()
+ numFragments := max(1, (messageSize-1)/c.maximumTransmissionUnit+1)
- // If a connection identifier had been negotiated and encryption is
- // enabled, the connection identifier MUST be sent.
- if len(c.state.GetLocalConnectionID()) > 0 && header.ContentType != protocol.ContentTypeConnectionID {
- c.log.Debug("discarded packet missing connection ID after value negotiated")
+ fragmentedHandshakes := make([][]byte, numFragments)
- return false, false, nil, nil
- }
+ if numFragments == 1 {
+ fragmentedHandshake := make([]byte, handshake.HeaderLength+messageSize)
- var err error
- var hdr recordlayer.Header
- if header.ContentType == protocol.ContentTypeConnectionID {
- hdr.ConnectionID = make([]byte, len(c.state.GetLocalConnectionID()))
+ headerFragment := handshake.Header{
+ Type: dtlsHandshake.Header.Type,
+ Length: dtlsHandshake.Header.Length,
+ MessageSequence: dtlsHandshake.Header.MessageSequence,
+ FragmentOffset: uint32(0),
+ FragmentLength: uint32(messageSize), //nolint:gosec // G115
}
- buf, err = c.state.CipherSuite.Decrypt(hdr, buf)
+
+ _, err := headerFragment.MarshalTo(fragmentedHandshake)
if err != nil {
- c.log.Debugf("%s: decrypt failed: %s", srvCliStr(c.state.IsClient), err)
+ return nil, err
+ }
- return false, false, nil, nil
+ _, err = dtlsHandshake.Message.MarshalTo(fragmentedHandshake[handshake.HeaderLength:])
+ if err != nil {
+ return nil, err
}
- // If this is a connection ID record, make it look like a normal record for
- // further processing.
- if header.ContentType == protocol.ContentTypeConnectionID {
- originalCID = true
- ip := &recordlayer.InnerPlaintext{}
- if err := ip.Unmarshal(buf[header.Size():]); err != nil { //nolint:govet
- c.log.Debugf("unpacking inner plaintext failed: %s", err)
- return false, false, nil, nil
- }
- unpacked := &recordlayer.Header{
- ContentType: ip.RealType,
- ContentLen: uint16(len(ip.Content)), //nolint:gosec // G115
- Version: header.Version,
- Epoch: header.Epoch,
- SequenceNumber: header.SequenceNumber,
- }
- buf, err = unpacked.Marshal()
- if err != nil {
- c.log.Debugf("converting CID record to inner plaintext failed: %s", err)
+ fragmentedHandshakes[0] = fragmentedHandshake
- return false, false, nil, nil
- }
- buf = append(buf, ip.Content...)
- }
+ return fragmentedHandshakes, nil
+ }
+
+ content, err := dtlsHandshake.Message.Marshal()
+ if err != nil {
+ return nil, err
+ }
+
+ contentFragments := util.SplitBytes(content, c.maximumTransmissionUnit)
+ if len(contentFragments) == 0 {
+ contentFragments = noContentFragments
+ }
+
+ offset := 0
+ for i, contentFragment := range contentFragments {
+ contentFragmentLen := len(contentFragment)
+
+ headerFragment := handshake.Header{
+ Type: dtlsHandshake.Header.Type,
+ Length: dtlsHandshake.Header.Length,
+ MessageSequence: dtlsHandshake.Header.MessageSequence,
+ FragmentOffset: uint32(offset),
+ FragmentLength: uint32(contentFragmentLen), //nolint:gosec // G115
+ }
+
+ fragmentedHandshake := make([]byte, handshake.HeaderLength+contentFragmentLen)
+
+ offset += contentFragmentLen
+
+ _, err := headerFragment.MarshalTo(fragmentedHandshake)
+ if err != nil {
+ return nil, err
+ }
+
+ copy(fragmentedHandshake[handshake.HeaderLength:], contentFragment)
+
+ fragmentedHandshakes[i] = fragmentedHandshake
+ }
+
+ return fragmentedHandshakes, nil
+}
+
+// readBufferPools caches read buffer pools by size so buffers are reused across
+// connections. Only a small bounded set of distinct sizes is expected in a
+// program.
+var readBufferPools sync.Map //nolint:gochecknoglobals // map[int]*sync.Pool
+
+// readBufferPoolForSize returns the shared read buffer pool for size.
+func readBufferPoolForSize(size int) *sync.Pool {
+ pool, _ := readBufferPools.LoadOrStore(size, &sync.Pool{
+ New: func() any {
+ b := make([]byte, size)
+
+ return &b
+ },
+ })
+
+ return pool.(*sync.Pool) //nolint:forcetypeassert // only *sync.Pool values are stored
+}
+
+func (c *Conn) readAndBuffer(ctx context.Context) error {
+ summary, err := c.readAndProcessDatagram(ctx)
+ if err != nil {
+ return err
+ }
+ if !summary.containsHandshake && len(summary.receivedACKs) == 0 {
+ c.signalHandshakeQuiescent()
+
+ return nil
+ }
+
+ received := dtlshandshake.RecvHandshakeState{
+ Done: make(chan struct{}),
+ HasHandshake: summary.containsHandshake,
+ IsRetransmit: summary.retransmit,
+ IsRetransmitStart: summary.retransmitStart,
+ IsClientHelloRetransmit: summary.clientHelloRetransmit,
+ ACKs: summary.receivedACKs,
+ RecordsToACK: c.takePendingACKs(),
+ }
+ select {
+ case c.handshakeRecv <- received:
+ // If the other party may retransmit the flight,
+ // we should respond even if it not a new message.
+ <-received.Done
+ case <-c.fsm.Done():
+ }
+
+ return nil
+}
+
+func (c *Conn) readAndProcessDatagram(ctx context.Context) (datagramProcessingSummary, error) {
+ bufptr, ok := c.readBufferPool.Get().(*[]byte)
+ if !ok {
+ return datagramProcessingSummary{}, dtlserrors.ErrFailedToAccessPoolReadBuffer
+ }
+ bufferLease := readBufferLease{conn: c, pool: c.readBufferPool, recyclableReadBuffer: bufptr}
+ defer bufferLease.releaseReadBuffer()
+
+ b := *bufptr
+ var i int
+ var rAddr net.Addr
+ var err error
+ if c.detached != nil {
+ i, rAddr, err = c.detached.readDatagram(ctx, b)
+ } else {
+ i, rAddr, err = c.readDatagram(ctx, b)
+ }
+ if idtlsnet.IsShortBuffer(err) {
+ c.log.Debugf("receive buffer too small (%d bytes); received %d bytes from %v: %v", len(b), i, rAddr, err)
+ // windows UDP reads can return a truncated prefix without its sender address.
+ if i == 0 || rAddr == nil {
+ return datagramProcessingSummary{}, nil
+ }
+ } else if err != nil {
+ return datagramProcessingSummary{}, netError(err)
+ }
+
+ return c.processDatagram(ctx, b[:i], rAddr, &bufferLease)
+}
+
+func (c *Conn) processDatagram(ctx context.Context, datagram []byte, rAddr net.Addr, bufferLease *readBufferLease) (datagramProcessingSummary, error) {
+ pkts, err := c.unpackDatagram(datagram)
+ if len(pkts) == 0 {
+ // Discards incomplete records or missing CIDs without terminating the handshake.
+ if errors.Is(err, recordlayer.ErrInvalidPacketLength) || errors.Is(err, dtlserrors.ErrInvalidCiphertextHeader) {
+ c.log.Debugf("discarded datagram: %v", err)
+
+ return datagramProcessingSummary{}, nil
+ }
+
+ return datagramProcessingSummary{}, err
+ }
+
+ if err != nil {
+ c.log.Debugf("discarded malformed datagram suffix: %v", err)
+ }
+
+ return c.processDatagramPackets(ctx, pkts, rAddr, bufferLease)
+}
+
+func (c *Conn) processDatagramPackets(ctx context.Context, pkts [][]byte, rAddr net.Addr, bufferLease *readBufferLease) (datagramProcessingSummary, error) {
+ datagramContainsCID := recordsContainCID(pkts)
+ bufferLease.pendingCID = c.pendingCIDNegotiation()
+ if bufferLease.pendingCID {
+ datagramContainsCID = recordsContainDTLS13CID(pkts)
+ }
+ bufferLease.datagramContainsCID = datagramContainsCID
+
+ var summary datagramProcessingSummary
+ for _, p := range pkts {
+ outcome, err := c.processIncomingPacket(ctx, p, rAddr, bufferLease, datagramContainsCID)
+ if err != nil {
+ return datagramProcessingSummary{}, err
+ }
+ summary.containsHandshake = summary.containsHandshake || outcome.containsHandshake
+ summary.retransmit = summary.retransmit || outcome.retransmit
+ summary.retransmitStart = summary.retransmitStart || outcome.retransmitStart
+ summary.clientHelloRetransmit = summary.clientHelloRetransmit || outcome.clientHelloRetransmit
+ if outcome.receivedACK != nil {
+ summary.receivedACKs = append(summary.receivedACKs, *outcome.receivedACK)
+ }
+ }
+
+ return summary, nil
+}
+
+func (c *Conn) handleQueuedPackets(ctx context.Context) error {
+ if c.pendingCIDNegotiation() {
+ return nil
+ }
+
+ c.lock.Lock()
+ pkts := c.encryptedPackets
+ c.encryptedPackets = nil
+ c.lock.Unlock()
+
+ for _, p := range pkts {
+ if p.pendingCID && c.inboundCIDRequired() && !p.datagramContainsCID {
+ continue
+ }
+ _, err := c.processIncomingPacket(
+ ctx,
+ p.data,
+ p.rAddr,
+ nil, // don't re-enqueue
+ p.datagramContainsCID,
+ )
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (c *Conn) enqueueEncryptedPackets(packet addrPkt) bool {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ if len(c.encryptedPackets) >= maxAppDataPacketQueueSize {
+ return false
+ }
+
+ // scanned records borrow the read datagram
+ packet.data = bytes.Clone(packet.data)
+ packet.data = packet.data[:len(packet.data):len(packet.data)]
+ c.encryptedPackets = append(c.encryptedPackets, packet)
+
+ return true
+}
+
+func (c *Conn) maxQueueableFutureEpoch(remoteEpoch uint64) uint64 {
+ if remoteEpoch == ^uint64(0) {
+ return remoteEpoch
+ }
+ maxEpoch := remoteEpoch + 1
+ if remoteEpoch >= dtlsflight13.EpochHandshake {
+ return maxEpoch
+ }
+ if dtlsstate.CommonState(c.state).LocalVersion == protocol.Version1_3 {
+ return dtlsflight13.EpochHandshake
+ }
+ if dtlsstate.CommonState(c.state).LocalVersion != 0 {
+ return maxEpoch
+ }
+ if c.handshakeConfig != nil && c.handshakeConfig.MaxVersion == protocol.Version1_3 {
+ return dtlsflight13.EpochHandshake
+ }
+
+ return maxEpoch
+}
+
+func (c *Conn) unpackDatagram(buf []byte) ([][]byte, error) {
+ if len(buf) == 0 {
+ return nil, nil
+ }
+
+ common := dtlsstate.CommonState(c.state)
+ localCID := common.LocalConnectionIDForInboundRecords()
+ cidLength := len(localCID)
+ if state, ok := c.state.(*dtlsstate.State13); ok && state.CID.Negotiated {
+ cidLength = state.CID.Receive.Length
+ }
+ config := recordlayer.UnpackDatagramConfig{TargetVersion: common.LocalVersion, CIDLength: cidLength, CIDRequired: c.inboundCIDRequired()}
+ records, err := recordlayer.UnpackDatagram(buf, config)
+ if cidLength == 0 {
+ return records, err
+ }
+
+ for i, record := range records {
+ cid := recordConnectionID(record, cidLength)
+ if len(cid) == 0 {
+ continue
+ }
+ if !c.acceptsInboundCID(cid) {
+ // Without a matching CID, protected siblings cannot inherit this
+ // association from an unrecognized later record.
+ if config.CIDRequired && !recordsContainCID(records[:i]) {
+ return nil, dtlserrors.ErrInvalidCiphertextHeader
+ }
+
+ return records[:i], nil
+ }
+ }
+
+ return records, err
+}
+
+func (c *Conn) inboundCIDRequired() bool {
+ common := dtlsstate.CommonState(c.state)
+ if common.LocalVersion == protocol.Version1_3 {
+ state13, ok := c.state.(*dtlsstate.State13)
+ // because early records precede CID negotiation. but per-record CID validation still
+ // applies to handshake and application epochs in unmarshalCiphertextRecord.
+ if ok && state13.EarlyDataStatus == dtlsstate.EarlyDataAccepted && !c.isHandshakeCompletedSuccessfully() {
+ return false
+ }
+
+ return ok && state13.CID.Negotiated && state13.CID.Receive.Expected
+ }
+ if common.LocalVersion == protocol.Version1_2 {
+ return len(common.LocalConnectionID()) > 0
+ }
+
+ return false
+}
+
+func recordsContainDTLS13CID(records [][]byte) bool {
+ for _, record := range records {
+ if protocol.IsDTLS13Ciphertext(protocol.ContentType(record[0])) && record[0]&recordwire.CIDBit != 0 {
+ return true
+ }
+ }
+
+ return false
+}
+
+func recordsContainCID(records [][]byte) bool {
+ for _, record := range records {
+ contentType := protocol.ContentType(record[0])
+ if contentType == protocol.ContentTypeConnectionID || protocol.IsDTLS13Ciphertext(contentType) && record[0]&recordwire.CIDBit != 0 {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (c *Conn) queueableCiphertextEpoch(epochLow uint8, remoteEpoch uint64) bool {
+ maximum := c.maxQueueableFutureEpoch(remoteEpoch)
+ for epoch := remoteEpoch; epoch < maximum; {
+ epoch++
+ if uint8(epoch&recordwire.EpochMask) == epochLow {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (c *Conn) unmarshalCiphertextRecord(
+ buf []byte,
+ datagramContainsCID bool,
+) (recordlayer.ParsedRecord, error) {
+ record := recordlayer.ParsedRecord{}
+ hasCID := buf[0]&recordwire.CIDBit != 0
+ localCID := dtlsstate.CommonState(c.state).LocalConnectionIDForInboundRecords()
+ cidExpected, cidAllowed, err := c.ciphertextCIDPolicy(localCID)
+ if err != nil {
+ return record, err
+ }
+ if hasCID && !cidAllowed {
+ return record, dtlserrors.ErrInvalidCiphertextHeader
+ }
+ record, err = recordlayer.ParseRecord(buf, len(localCID))
+ if err != nil {
+ return record, err
+ }
+ if cidExpected && !hasCID {
+ if !c.allowsMissingCiphertextCID(record, datagramContainsCID) {
+ return record, dtlserrors.ErrInvalidCiphertextHeader
+ }
+ }
+ if hasCID && !c.acceptsInboundCID(record.ConnectionID()) {
+ return record, dtlserrors.ErrInvalidCiphertextHeader
+ }
+
+ return record, nil
+}
+
+func (c *Conn) allowsMissingCiphertextCID(record recordlayer.ParsedRecord, datagramContainsCID bool) bool {
+ return datagramContainsCID || record.EpochLow() == uint8(dtlsflight13.EpochEarlyData) &&
+ dtlsstate.CommonState(c.state).RemoteEpoch() <= dtlsflight13.EpochApplication
+}
+
+func (c *Conn) ciphertextCIDPolicy(localCID []byte) (expected, allowed bool, err error) {
+ state13, ok := c.state.(*dtlsstate.State13)
+ if !ok || !state13.CID.Negotiated {
+ return false, len(localCID) > 0, nil
+ }
+ if state13.CID.Receive.Length != len(localCID) {
+ return false, false, dtlserrors.ErrInvalidCiphertextHeader
+ }
+
+ return state13.CID.Receive.Expected, state13.CID.Receive.Expected, nil
+}
+
+type openedRecord struct {
+ Content []byte
+ RealType protocol.ContentType
+}
+
+func (c *Conn) openCiphertextRecord(record recordlayer.ParsedRecord) (openedRecord, uint64, uint64, error) {
+ state13, ok := c.state.(*dtlsstate.State13)
+ if !ok || state13.TrafficKeys == nil {
+ return openedRecord{}, 0, 0, dtlserrors.ErrCipherSuiteRecordProtectionNotImplemented
+ }
+ generation, ok := state13.TrafficKeys.ReadCandidate(record.EpochLow(), max(state13.RemoteEpoch(), dtlsflight13.EpochEarlyData))
+ if !ok {
+ return openedRecord{}, 0, 0, dtlserrors.ErrInvalidEpoch
+ }
+ if generation.Protection == nil {
+ return openedRecord{}, 0, 0, operationalProtectionError(dtlserrors.ErrCipherSuiteRecordProtectionNotImplemented)
+ }
+ plaintext, sequence, err := c.openCiphertextWithGeneration(record, generation)
+ if err != nil {
+ return openedRecord{}, 0, 0, err
+ }
+
+ return plaintext, sequence, generation.Epoch, nil
+}
+
+func (c *Conn) openCiphertextWithGeneration( //nolint:cyclop
+ record recordlayer.ParsedRecord,
+ generation *dtlsstate.TrafficGeneration,
+) (openedRecord, uint64, error) {
+ common := dtlsstate.CommonState(c.state)
+ capabilities, err := generation.Capabilities(common.CipherSuite)
+ if err != nil {
+ return openedRecord{}, 0, operationalProtectionError(err)
+ }
+ _, err = capabilities.PlaintextLenUpperBound(len(record.Payload()))
+ if err != nil {
+ return openedRecord{}, 0, errRecordAuthentication
+ }
+ sampleLen := capabilities.MaskLen()
+ if sampleLen <= 0 || len(record.Payload()) < sampleLen {
+ return openedRecord{}, 0, errRecordAuthentication
+ }
+ mask, err := generation.Protection.Mask(record.Payload()[:sampleLen])
+ if err != nil {
+ return openedRecord{}, 0, operationalProtectionError(err)
+ }
+ clearSequence, err := applySequenceNumberMask(uint16(record.SequenceNumber()&0xffff), record.SequenceBytes() == 2, mask)
+ if err != nil {
+ return openedRecord{}, 0, operationalProtectionError(err)
+ }
+ var headerBuffer [260]byte
+ clearHeader := headerBuffer[:len(record.HeaderBytes())]
+ copy(clearHeader, record.HeaderBytes())
+ sequenceOffset := 1 + len(record.ConnectionID())
+ if record.SequenceBytes() == 2 {
+ clearHeader[sequenceOffset] = byte(clearSequence >> 8)
+ sequenceOffset++
+ }
+ clearHeader[sequenceOffset] = byte(clearSequence & 0xff)
+ highest, _ := common.HighestRemoteSequenceNumber(generation.Epoch)
+ sequenceNumber := reconstructSequenceNumber(clearSequence, record.SequenceBytes() == 2, highest)
+ metadata, err := dtlsciphersuite.NewUnifiedRecord(generation.Epoch, sequenceNumber, clearHeader, len(record.Payload()))
+ if err != nil {
+ return openedRecord{}, 0, operationalProtectionError(err)
+ }
+ plaintext, err := generation.Open(metadata, record.Payload())
+ if errors.Is(err, cryptosuite.ErrAuthenticationFailed) {
+ return openedRecord{}, 0, errRecordAuthentication
+ }
+ if err != nil {
+ return openedRecord{}, 0, operationalProtectionError(err)
+ }
+ if len(plaintext) > maxDTLS13InnerPlaintextLen {
+ return openedRecord{}, 0, errRecordAuthentication
+ }
+ if lengthErr := capabilities.ValidatePlaintextLen(len(record.Payload()), len(plaintext)); lengthErr != nil {
+ return openedRecord{}, 0, operationalProtectionError(lengthErr)
+ }
+
+ common.UpdateRemoteSequenceNumber(generation.Epoch, sequenceNumber)
+
+ content, realType, _, err := recordlayer.ParseInnerPlaintext(plaintext)
+ innerPlaintext := openedRecord{Content: content, RealType: realType}
+ if err != nil {
+ return openedRecord{}, 0, err
+ }
+ if len(innerPlaintext.Content) > maxPlaintextRecordLen {
+ return openedRecord{}, 0, dtlserrors.ErrInvalidPacketLength
+ }
+ if generation.Epoch == dtlsflight13.EpochEarlyData &&
+ (common.IsClient || innerPlaintext.RealType != protocol.ContentTypeApplicationData) {
+ return openedRecord{}, 0, dtlserrors.ErrInvalidContentType
+ }
+
+ switch innerPlaintext.RealType {
+ case protocol.ContentTypeAlert,
+ protocol.ContentTypeHandshake,
+ protocol.ContentTypeApplicationData,
+ protocol.ContentTypeACK,
+ protocol.ContentTypeReturnRoutabilityCheck:
+ return innerPlaintext, sequenceNumber, nil
+ default:
+ return openedRecord{}, 0, dtlserrors.ErrInvalidContentType
+ }
+}
+
+func reconstructSequenceNumber(partial uint16, seqBit bool, highest uint64) uint64 {
+ bits := uint(8)
+ if seqBit {
+ bits = 16
+ }
+
+ window := uint64(1) << bits
+ halfWindow := window / 2
+ mask := window - 1
+ expected := highest + 1
+ candidate := (expected & ^mask) | (uint64(partial) & mask)
+ if candidate+halfWindow <= expected {
+ return candidate + window
+ }
+ if candidate > expected+halfWindow && candidate >= window {
+ return candidate - window
+ }
+
+ return candidate
+}
+
+func (c *Conn) prepareIncomingPacket(buf []byte, rAddr net.Addr, bufferLease *readBufferLease, datagramContainsCID bool) (incomingPacketState, bool, error) {
+ if protocol.IsDTLS13Ciphertext(protocol.ContentType(buf[0])) {
+ version := dtlsstate.CommonState(c.state).LocalVersion
+ if version != 0 && version != protocol.Version1_3 {
+ c.log.Debug("discarded DTLS 1.3 ciphertext on a DTLS 1.2 connection")
+
+ return incomingPacketState{}, false, nil
+ }
+
+ return c.prepareCiphertextPacket(buf, rAddr, bufferLease, datagramContainsCID)
+ }
+ if dtlsstate.CommonState(c.state).LocalVersion == protocol.Version1_3 && (!isPlaintextRecord13ContentType(protocol.ContentType(buf[0])) || len(buf) < recordlayer.FixedHeaderSize || buf[3] != 0 || buf[4] != 0) {
+ c.log.Debug("discarded invalid DTLS 1.3 plaintext record")
+
+ return incomingPacketState{}, false, nil
+ }
+
+ return c.prepareLegacyPacket(buf, rAddr, bufferLease)
+}
+
+func isPlaintextRecord13ContentType(contentType protocol.ContentType) bool {
+ return contentType == protocol.ContentTypeAlert || contentType == protocol.ContentTypeHandshake || contentType == protocol.ContentTypeACK
+}
+
+func (c *Conn) prepareCiphertextPacket(buf []byte, rAddr net.Addr, bufferLease *readBufferLease, datagramContainsCID bool) (incomingPacketState, bool, error) {
+ ciphertext, err := c.unmarshalCiphertextRecord(buf, datagramContainsCID)
+ if err != nil {
+ c.log.Debugf("discarded broken ciphertext packet: %v", err)
+
+ return incomingPacketState{}, false, nil
+ }
+
+ if c.queueIfCipherSuiteUninitialized(
+ rAddr,
+ buf,
+ bufferLease,
+ "handshake not finished, queuing ciphertext packet",
+ ) {
+ return incomingPacketState{}, false, nil
+ }
+
+ innerPlaintext, sequenceNumber, epoch, err := c.openCiphertextRecord(ciphertext)
+ if err != nil {
+ if errors.Is(err, errRecordOperational) {
+ return incomingPacketState{}, false, err
+ }
+ if errors.Is(err, dtlserrors.ErrInvalidEpoch) {
+ c.handleFutureCiphertextPacket(ciphertext.EpochLow(), dtlsstate.CommonState(c.state).RemoteEpoch(), rAddr, buf, bufferLease)
+ }
+ c.log.Debugf("%s: decrypt failed: %s", srvCliStr(dtlsstate.CommonState(c.state).IsClient), err)
+
+ return incomingPacketState{}, false, nil
+ }
+
+ markPacketAsValid, ok := c.replayMarker(epoch, sequenceNumber, ^uint64(0))
+ if !ok {
+ return incomingPacketState{}, false, nil
+ }
+
+ prepared := incomingPacketState{
+ raw: buf,
+ content: innerPlaintext.Content,
+ contentType: innerPlaintext.RealType,
+ number: protocol.RecordNumber{Epoch: epoch, SequenceNumber: sequenceNumber},
+ markPacketAsValid: markPacketAsValid,
+ // The datagram's source address remains a candidate until the CID and
+ // ciphertext have both been authenticated and replay checks confirm this
+ // is the latest valid record.
+ // https://datatracker.ietf.org/doc/html/rfc9146#section-6
+ originalCID: len(ciphertext.ConnectionID()) > 0,
+ }
+
+ return prepared, true, nil
+}
+
+func (c *Conn) handleFutureCiphertextPacket(epochLow uint8, remoteEpoch uint64, rAddr net.Addr, buf []byte, bufferLease *readBufferLease) {
+ if !c.queueableCiphertextEpoch(epochLow, remoteEpoch) {
+ c.log.Debugf("discarded future ciphertext packet (epoch low: %d)", epochLow)
+
+ return
+ }
+ if bufferLease != nil {
+ if ok := bufferLease.enqueue(addrPkt{rAddr: rAddr, data: buf}); ok {
+ c.log.Debug("received ciphertext packet of next epoch, queuing packet")
+ }
+ }
+}
+
+//nolint:cyclop // Apply replay checks for both record formats and restored state.
+func (c *Conn) replayMarker(epoch, sequenceNumber, maximum uint64) (func() bool, bool) {
+ if state, ok := c.state.(*dtlsstate.State12); ok && epoch != 0 && sequenceNumber < state.RemoteReplayCutoff {
+ return nil, false
+ }
+ if state, ok := c.state.(*dtlsstate.State13); ok {
+ if cutoff, restored := state.ReplayCutoff[epoch]; restored && sequenceNumber <= cutoff {
+ return nil, false
+ }
+ }
+ common := dtlsstate.CommonState(c.state)
+ if common.ReplayDetector == nil {
+ common.ReplayDetector = make(map[uint64]replaydetector.ReplayDetector)
+ }
+ if common.ReplayDetector[epoch] == nil {
+ common.ReplayDetector[epoch] = replaydetector.New(c.replayProtectionWindow, maximum)
+ }
+ accept, ok := common.ReplayDetector[epoch].Check(sequenceNumber)
+ if !ok {
+ c.log.Debugf("discarded duplicated packet (epoch: %d, seq: %d)", epoch, sequenceNumber)
+
+ return nil, false
+ }
+
+ if _, ok := c.state.(*dtlsstate.State12); ok && epoch != 0 {
+ return func() bool {
+ latest := accept()
+ common.UpdateRemoteSequenceNumber(epoch, sequenceNumber)
+
+ return latest
+ }, true
+ }
+
+ return accept, true
+}
+
+func (c *Conn) queueIfCipherSuiteUninitialized(rAddr net.Addr, buf []byte, bufferLease *readBufferLease, message string) bool {
+ if c.hasInboundRecordProtection() {
+ return false
+ }
+ if bufferLease != nil {
+ if ok := bufferLease.enqueue(addrPkt{rAddr: rAddr, data: buf}); ok {
+ c.log.Debug(message)
+ }
+ }
+
+ return true
+}
+
+func (c *Conn) hasInboundRecordProtection() bool {
+ common := dtlsstate.CommonState(c.state)
+ if state13, ok := c.state.(*dtlsstate.State13); ok && common.LocalVersion == protocol.Version1_3 {
+ if state13.TrafficKeys == nil {
+ return false
+ }
+ generation, found := state13.TrafficKeys.Read(max(common.RemoteEpoch(), dtlsflight13.EpochEarlyData))
+
+ return found && generation.Protection != nil
+ }
+
+ state12, ok := c.state.(*dtlsstate.State12)
+
+ return ok && common.CipherSuite != nil && state12.Protection != nil
+}
+
+//nolint:cyclop
+func (c *Conn) prepareLegacyPacket(buf []byte, rAddr net.Addr, bufferLease *readBufferLease) (incomingPacketState, bool, error) {
+ raw := buf
+ header, ok := c.unmarshalLegacyHeader(buf)
+ if !ok {
+ return incomingPacketState{}, false, nil
+ }
+ // Discard old epoch-zero alerts following the earlier-epoch discard recommendation:
+ // https://datatracker.ietf.org/doc/html/rfc9147#section-4.2.1
+ // DTLS 1.2 requires accepting old epochs until the handshake completes:
+ // https://datatracker.ietf.org/doc/html/rfc6347#section-4.1
+ common := dtlsstate.CommonState(c.state)
+ if common.LocalVersion == protocol.Version1_2 && header.Epoch() > 1 {
+ return incomingPacketState{}, false, nil
+ }
+ if header.Epoch() == 0 && header.ContentType() == protocol.ContentTypeAlert && common.RemoteEpoch() != 0 &&
+ (common.LocalVersion == protocol.Version1_3 || c.isHandshakeCompletedSuccessfully()) {
+ c.log.Debug("discarded alert from the old plaintext epoch")
+
+ return incomingPacketState{}, false, nil
+ }
+ if c.handleFutureLegacyPacket(header, rAddr, buf, bufferLease) {
+ return incomingPacketState{}, false, nil
+ }
+
+ markPacketAsValid, ok := c.replayMarker(uint64(header.Epoch()), header.SequenceNumber(), recordlayer.MaxSequenceNumber)
+ if !ok {
+ return incomingPacketState{}, false, nil
+ }
+
+ contentType := header.ContentType()
+ content := header.Payload()
+ originalCID := false
+ if header.Epoch() != 0 {
+ var decryptOK bool
+ var err error
+ contentType, content, originalCID, decryptOK, err = c.decryptLegacyPacket(header, buf, rAddr, bufferLease)
+ if err != nil {
+ return incomingPacketState{}, false, err
+ }
+ if !decryptOK {
+ return incomingPacketState{}, false, nil
+ }
+ }
+
+ return incomingPacketState{raw: raw, content: content, contentType: contentType, number: protocol.RecordNumber{Epoch: uint64(header.Epoch()), SequenceNumber: header.SequenceNumber()}, markPacketAsValid: markPacketAsValid, originalCID: originalCID}, true, nil
+}
+
+func (c *Conn) unmarshalLegacyHeader(buf []byte) (recordlayer.ParsedRecord, bool) {
+ localCID := dtlsstate.CommonState(c.state).LocalConnectionIDForInboundRecords()
+ header, err := recordlayer.ParseRecord(buf, len(localCID))
+ if err != nil {
+ // Decode error must be silently discarded
+ // [RFC6347 Section-4.1.2.7]
+ c.log.Debugf("discarded broken packet: %v", err)
+
+ return recordlayer.ParsedRecord{}, false
+ }
+
+ return header, true
+}
+
+func (c *Conn) handleFutureLegacyPacket(header recordlayer.ParsedRecord, rAddr net.Addr, buf []byte, bufferLease *readBufferLease) bool {
+ remoteEpoch := dtlsstate.CommonState(c.state).RemoteEpoch()
+ if uint64(header.Epoch()) <= remoteEpoch {
+ return false
+ }
+ if uint64(header.Epoch()) > c.maxQueueableFutureEpoch(remoteEpoch) {
+ c.log.Debugf("discarded future packet (epoch: %d, seq: %d)",
+ header.Epoch(), header.SequenceNumber(),
+ )
+
+ return true
+ }
+ if bufferLease != nil {
+ if ok := bufferLease.enqueue(addrPkt{rAddr: rAddr, data: buf}); ok {
+ c.log.Debug("received packet of next epoch, queuing packet")
+ }
+ }
+
+ return true
+}
+
+func (c *Conn) decryptLegacyPacket(header recordlayer.ParsedRecord, buf []byte, rAddr net.Addr, bufferLease *readBufferLease) (protocol.ContentType, []byte, bool, bool, error) {
+ if c.queueIfCipherSuiteUninitialized(
+ rAddr,
+ buf,
+ bufferLease,
+ "handshake not finished, queuing packet",
+ ) {
+ return 0, nil, false, false, nil
+ }
+
+ if !c.validateLegacyCIDPresence(header) {
+ return 0, nil, false, false, nil
+ }
+
+ decrypted, err := c.decryptLegacyRecord(header, header.Payload())
+ if err != nil {
+ if errors.Is(err, errRecordOperational) {
+ return 0, nil, false, false, err
+ }
+ c.log.Debugf("%s: decrypt failed: %s", srvCliStr(dtlsstate.CommonState(c.state).IsClient), err)
+
+ return 0, nil, false, false, nil
+ }
+ content := decrypted
+
+ if header.ContentType() == protocol.ContentTypeConnectionID {
+ innerContent, realType, _, err := recordlayer.ParseInnerPlaintext(content)
+ if err != nil {
+ c.log.Debugf("unpacking inner plaintext failed: %s", err)
+
+ return 0, nil, false, false, nil
+ }
+ if len(innerContent) > maxPlaintextRecordLen {
+ c.log.Debug("discarded oversized inner plaintext")
+
+ return 0, nil, false, false, nil
+ }
+
+ return realType, innerContent, true, c.validateLegacyCID(header), nil
+ }
+
+ return header.ContentType(), content, false, c.validateLegacyCID(header), nil
+}
+
+func (c *Conn) validateLegacyCIDPresence(header recordlayer.ParsedRecord) bool {
+ common := dtlsstate.CommonState(c.state)
+ if len(common.LocalConnectionIDForInboundRecords()) == 0 || header.ContentType() == protocol.ContentTypeConnectionID {
+ return true
+ }
+
+ c.log.Debug("discarded packet missing connection ID after value negotiated")
+
+ return false
+}
+
+func (c *Conn) decryptLegacyRecord( //nolint:cyclop
+ header recordlayer.ParsedRecord,
+ protected []byte,
+) ([]byte, error) {
+ state12, ok := c.state.(*dtlsstate.State12)
+ common := dtlsstate.CommonState(c.state)
+ if !ok || common.CipherSuite == nil || state12.Protection == nil {
+ return nil, operationalProtectionError(dtlserrors.ErrCipherSuiteNotInit)
+ }
+ metadata, err := dtlsciphersuite.NewLegacyRecord(header.ContentType(), header.Version(), header.Epoch(), header.SequenceNumber(), header.ConnectionID())
+ if err != nil {
+ return nil, errRecordAuthentication
+ }
+ capabilities := common.CipherSuite.Capabilities()
+ _, err = capabilities.PlaintextLenUpperBound(len(protected))
+ if err != nil {
+ return nil, errRecordAuthentication
+ }
+ plaintext, err := state12.Open(metadata, protected)
+ if errors.Is(err, cryptosuite.ErrAuthenticationFailed) {
+ return nil, errRecordAuthentication
+ }
+ if err != nil {
+ return nil, operationalProtectionError(err)
+ }
+ if len(plaintext) > maxPlaintextRecordLen {
+ return nil, errRecordAuthentication
+ }
+ if err = capabilities.ValidatePlaintextLen(len(protected), len(plaintext)); err != nil {
+ return nil, operationalProtectionError(err)
+ }
+
+ return plaintext, nil
+}
+
+func (c *Conn) validateLegacyCID(header recordlayer.ParsedRecord) bool {
+ if bytes.Equal(dtlsstate.CommonState(c.state).LocalConnectionIDForInboundRecords(), header.ConnectionID()) {
+ return true
+ }
+
+ c.log.Debug("unexpected connection ID")
+
+ return false
+}
+
+//nolint:cyclop // Reassemble, acknowledge, and cache each protocol version.
+func (c *Conn) bufferHandshakeRecord(content []byte, number protocol.RecordNumber, markPacketAsValid func() bool) (packetOutcome, bool, error) {
+ if len(content) == 0 || !c.validHandshakeRecordEpoch(content, number.Epoch) {
+ return packetOutcome{}, false, nil
+ }
+ c.syncFragmentBufferHandshakeSequence()
+ result, err := c.fragmentBuffer.Push(number.Epoch, content)
+ if err != nil {
+ // Decode error must be silently discarded
+ // [RFC6347 Section-4.1.2.7]
+ c.log.Debugf("defragment failed: %s", err)
+
+ return packetOutcome{}, false, nil
+ }
+
+ isLatestSeqNum := markPacketAsValid()
+ if dtlsstate.CommonState(c.state).LocalVersion == protocol.Version1_3 &&
+ number.Epoch >= dtlsflight13.EpochHandshake {
+ c.lock.Lock()
+ c.queueHandshakeACK(content, number)
+ c.lock.Unlock()
+ }
+
+ for out, epoch := c.fragmentBuffer.Pop(); out != nil; out, epoch = c.fragmentBuffer.Pop() {
+ header := &handshake.Header{}
+ if err := header.Unmarshal(out); err != nil {
+ c.log.Debugf("%s: handshake parse failed: %s", srvCliStr(dtlsstate.CommonState(c.state).IsClient), err)
+
+ continue
+ }
+ if err := c.pushHandshake(out, epoch, header.MessageSequence, header.Type, !dtlsstate.CommonState(c.state).IsClient); err != nil {
+ return packetOutcome{responseAlert: &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}}, isLatestSeqNum, err
+ }
+ }
+
+ common := dtlsstate.CommonState(c.state)
+
+ return packetOutcome{
+ containsHandshake: true,
+ retransmit: result.IsRetransmit,
+ retransmitStart: result.IsRetransmitStart,
+ clientHelloRetransmit: !common.IsClient && common.LocalVersion == protocol.Version1_2 &&
+ number.Epoch == 0 && result.IsRetransmit && c.handshakeCache.IsClientHelloRetransmit(content),
+ }, isLatestSeqNum, nil
+}
+
+// Validate every fragment before any can advance reassembly or enter the cache.
+// https://www.rfc-editor.org/rfc/rfc9147.html#section-6.1
+func (c *Conn) validHandshakeRecordEpoch(content []byte, epoch uint64) bool {
+ common := dtlsstate.CommonState(c.state)
+ if common.LocalVersion != protocol.Version1_3 {
+ return true
+ }
+ for len(content) != 0 {
+ var header handshake.Header
+ if header.Unmarshal(content) != nil || !c.validHandshakeFragmentEpoch(header, epoch) {
+ return false
+ }
+ end := handshake.HeaderLength + int(header.FragmentLength)
+ if end > len(content) {
+ return false
+ }
+ content = content[end:]
+ }
+
+ return true
+}
+
+func (c *Conn) validHandshakeFragmentEpoch(header handshake.Header, epoch uint64) bool {
+ switch header.Type {
+ case handshake.TypeClientHello, handshake.TypeServerHello:
+ if epoch != 0 {
+ return false
+ }
+ common := dtlsstate.CommonState(c.state)
+ if max(common.LocalEpoch(), common.RemoteEpoch()) >= dtlsflight13.EpochHandshake {
+ // Old hellos may signal retransmission, but cannot consume a new sequence.
+ return int(header.MessageSequence) < dtlsstate.HandshakeRecvSequence(c.state)
+ }
+
+ // Before keys are installed, only the expected hello or an old one
+ // can be received. Future hellos must not reserve encrypted sequences.
+ return int(header.MessageSequence) <= dtlsstate.HandshakeRecvSequence(c.state)
+ case handshake.TypeNewSessionTicket, handshake.TypeKeyUpdate,
+ handshake.TypeNewConnectionID, handshake.TypeRequestConnectionID:
+ return epoch >= dtlsflight13.EpochApplication
+ default:
+ return epoch == dtlsflight13.EpochHandshake
+ }
+}
+
+func (c *Conn) handleChangeCipherSpecRecord(prepared incomingPacketState, rAddr net.Addr, bufferLease *readBufferLease) bool {
+ common := dtlsstate.CommonState(c.state)
+ if prepared.number.Epoch != 0 || common.LocalVersion == protocol.Version1_3 {
+ return false
+ }
+ if !c.hasInboundRecordProtection() {
+ if bufferLease != nil {
+ if ok := bufferLease.enqueue(addrPkt{rAddr: rAddr, data: prepared.raw}); ok {
+ c.log.Debugf("CipherSuite not initialized, queuing packet")
+ }
+ }
+
+ return false
+ }
+
+ if prepared.number.Epoch >= 0xffff {
+ return false
+ }
+ newRemoteEpoch := prepared.number.Epoch + 1
+ c.log.Tracef("%s: <- ChangeCipherSpec (epoch: %d)", srvCliStr(common.IsClient), newRemoteEpoch)
+ if common.RemoteEpoch() != prepared.number.Epoch {
+ return false
+ }
+
+ c.setRemoteEpoch(newRemoteEpoch)
+
+ return prepared.markPacketAsValid()
+}
+
+//nolint:cyclop // Route early, detached, and established application data.
+func (c *Conn) handleApplicationDataRecord(ctx context.Context, content *protocol.ApplicationData, prepared incomingPacketState) (bool, packetOutcome, error) {
+ if prepared.number.Epoch == 0 {
+ return false, packetOutcome{responseAlert: &alert.Alert{Level: alert.Fatal, Description: alert.UnexpectedMessage}}, dtlserrors.ErrApplicationDataEpochZero
+ }
+ if dtlsstate.CommonState(c.state).LocalVersion == protocol.Version1_3 && prepared.number.Epoch == dtlsflight13.EpochHandshake {
+ return false, packetOutcome{responseAlert: &alert.Alert{Level: alert.Fatal, Description: alert.UnexpectedMessage}}, dtlserrors.ErrInvalidContentType
+ }
+ if ok, err := c.acceptEarlyApplicationData(prepared.number.Epoch, uint64(len(content.Data))); !ok || err != nil {
+ outcome := packetOutcome{}
+ if err != nil {
+ outcome.responseAlert = &alert.Alert{Level: alert.Fatal, Description: alert.UnexpectedMessage}
+ }
+
+ return false, outcome, err
+ }
+
+ isLatestSeqNum := prepared.markPacketAsValid()
+ if c.detached != nil {
+ early := dtlsstate.CommonState(c.state).LocalVersion == protocol.Version1_3 && prepared.number.Epoch == dtlsflight13.EpochEarlyData
+ c.detached.publishApplicationData(content.Data, early)
+
+ return isLatestSeqNum, packetOutcome{}, nil
+ }
+ // Read waits for handshake completion; keep the reader free to process Finished.
+ if !c.isHandshakeCompletedSuccessfully() {
+ select {
+ case c.decrypted <- content.Data:
+ default:
+ }
+
+ return isLatestSeqNum, packetOutcome{}, nil
+ }
+ select {
+ case c.decrypted <- content.Data:
+ case <-c.closed.Done():
+ case <-ctx.Done():
+ }
+
+ return isLatestSeqNum, packetOutcome{}, nil
+}
+
+func (c *Conn) handleRecordContent(ctx context.Context, content protocol.Content, prepared incomingPacketState, rAddr net.Addr, bufferLease *readBufferLease) (bool, packetOutcome, error) {
+ switch content := content.(type) {
+ case *protocol.ACK:
+ // The ACK's epoch must be at least that of every acknowledged record.
+ // Validate the entire ACK before applying it.
+ // https://datatracker.ietf.org/doc/html/rfc9147#section-7
+ for _, record := range content.Records {
+ if record.Epoch > prepared.number.Epoch {
+ c.log.Debug("discarded ACK for a record from a higher epoch")
+
+ return false, packetOutcome{}, nil
+ }
+ }
+ isLatestSeqNum := prepared.markPacketAsValid()
+
+ return isLatestSeqNum, packetOutcome{receivedACK: &protocol.ACK{Records: append([]protocol.RecordNumber(nil), content.Records...)}}, nil
+ case *alert.Alert:
+ c.log.Tracef("%s: <- %s", srvCliStr(dtlsstate.CommonState(c.state).IsClient), content.String())
+ var responseAlert *alert.Alert
+ if content.Description == alert.CloseNotify {
+ // Respond with a close_notify [RFC5246 Section 7.2.1]
+ responseAlert = &alert.Alert{Level: alert.Warning, Description: alert.CloseNotify}
+ }
+ prepared.markPacketAsValid()
+
+ return false, packetOutcome{responseAlert: responseAlert}, &alertError{content}
+ case *protocol.ChangeCipherSpec:
+ return c.handleChangeCipherSpecRecord(prepared, rAddr, bufferLease), packetOutcome{}, nil
+ case *protocol.ApplicationData:
+ return c.handleApplicationDataRecord(ctx, content, prepared)
+ case *protocol.ReturnRoutabilityCheck:
+ return returnRoutabilityConn{conn: c}.HandleRecord(ctx, content, prepared, rAddr)
+ default:
+ return false, packetOutcome{responseAlert: &alert.Alert{Level: alert.Fatal, Description: alert.UnexpectedMessage}}, fmt.Errorf("%w: %d", dtlserrors.ErrUnhandledContextType, content.ContentType())
+ }
+}
+
+func unmarshalRecordContent(contentType protocol.ContentType, data []byte) (protocol.Content, error) {
+ var content protocol.Content
+ switch contentType {
+ case protocol.ContentTypeChangeCipherSpec:
+ content = &protocol.ChangeCipherSpec{}
+ case protocol.ContentTypeAlert:
+ content = &alert.Alert{}
+ case protocol.ContentTypeApplicationData:
+ content = &protocol.ApplicationData{}
+ case protocol.ContentTypeACK:
+ content = &protocol.ACK{}
+ case protocol.ContentTypeReturnRoutabilityCheck:
+ content = &protocol.ReturnRoutabilityCheck{}
+ default:
+ return nil, dtlserrors.ErrInvalidContentType
+ }
+ if err := content.Unmarshal(data); err != nil {
+ return nil, err
+ }
- // If connection ID does not match discard the packet.
- if !bytes.Equal(c.state.GetLocalConnectionID(), header.ConnectionID) {
- c.log.Debug("unexpected connection ID")
+ return content, nil
+}
- return false, false, nil, nil
- }
+func (c *Conn) handleIncomingPacket(ctx context.Context, buf []byte, rAddr net.Addr, bufferLease *readBufferLease, datagramContainsCID bool) (packetOutcome, error) {
+ if len(buf) == 0 {
+ return packetOutcome{}, nil
}
- isHandshake, isRetransmit, err := c.fragmentBuffer.push(append([]byte{}, buf...))
+ prepared, ok, err := c.prepareIncomingPacket(buf, rAddr, bufferLease, datagramContainsCID)
if err != nil {
- // Decode error must be silently discarded
- // [RFC6347 Section-4.1.2.7]
- c.log.Debugf("defragment failed: %s", err)
-
- return false, false, nil, nil
- } else if isHandshake {
- markPacketAsValid()
-
- for out, epoch := c.fragmentBuffer.pop(); out != nil; out, epoch = c.fragmentBuffer.pop() {
- header := &handshake.Header{}
- if err := header.Unmarshal(out); err != nil {
- c.log.Debugf("%s: handshake parse failed: %s", srvCliStr(c.state.IsClient), err)
+ return packetOutcome{}, err
+ }
+ if !ok {
+ return packetOutcome{}, nil
+ }
+ prepared.markPacketAsValid = c.rrc.WrapReplayMarker(prepared.markPacketAsValid, rAddr, len(buf), c.RemoteAddr, c.cidPathMigrationPolicy == CIDPathMigrationRRC && dtlsstate.CommonState(c.state).RRCNegotiated)
+ if prepared.contentType == protocol.ContentTypeHandshake {
+ outcome, isLatestSeqNum, cacheErr := c.bufferHandshakeRecord(
+ prepared.content,
+ prepared.number,
+ prepared.markPacketAsValid,
+ )
+ returnRoutabilityConn{conn: c}.HandleCandidate(ctx, dtlsstate.CommonState(c.state).RRCNegotiated, prepared.originalCID, isLatestSeqNum, rAddr)
- continue
- }
- c.handshakeCache.push(out, epoch, header.MessageSequence, header.Type, !c.state.IsClient)
- }
+ return outcome, cacheErr
+ }
- return true, isRetransmit, nil, nil
+ content, err := unmarshalRecordContent(prepared.contentType, prepared.content)
+ if err != nil {
+ return packetOutcome{
+ responseAlert: &alert.Alert{Level: alert.Fatal, Description: alert.DecodeError},
+ }, err
}
- r := &recordlayer.RecordLayer{}
- if err := r.Unmarshal(buf); err != nil {
- return false, false, &alert.Alert{Level: alert.Fatal, Description: alert.DecodeError}, err
+ isLatestSeqNum, outcome, err := c.handleRecordContent(ctx, content, prepared, rAddr, bufferLease)
+ if err != nil || outcome.responseAlert != nil {
+ return outcome, err
}
- isLatestSeqNum := false
- switch content := r.Content.(type) {
- case *alert.Alert:
- c.log.Tracef("%s: <- %s", srvCliStr(c.state.IsClient), content.String())
- var a *alert.Alert
- if content.Description == alert.CloseNotify {
- // Respond with a close_notify [RFC5246 Section 7.2.1]
- a = &alert.Alert{Level: alert.Warning, Description: alert.CloseNotify}
- }
- _ = markPacketAsValid()
+ returnRoutabilityConn{conn: c}.HandleCandidate(ctx, dtlsstate.CommonState(c.state).RRCNegotiated, prepared.originalCID, isLatestSeqNum, rAddr)
- return false, false, a, &alertError{content}
- case *protocol.ChangeCipherSpec:
- if c.state.CipherSuite == nil || !c.state.CipherSuite.IsInitialized() {
- if enqueue {
- if ok := c.enqueueEncryptedPackets(addrPkt{rAddr, buf}); ok {
- c.log.Debugf("CipherSuite not initialized, queuing packet")
- }
- }
+ return outcome, nil
+}
- return false, false, nil, nil
+func (c *Conn) processIncomingPacket(ctx context.Context, buf []byte, rAddr net.Addr, bufferLease *readBufferLease, datagramContainsCID bool) (packetOutcome, error) {
+ outcome, err := c.handleIncomingPacket(ctx, buf, rAddr, bufferLease, datagramContainsCID)
+ if outcome.responseAlert != nil {
+ responseAlert := outcome.responseAlert
+ if alertErr := c.notify(ctx, responseAlert.Level, responseAlert.Description); alertErr != nil && err == nil {
+ err = alertErr
}
+ }
- newRemoteEpoch := header.Epoch + 1
- c.log.Tracef("%s: <- ChangeCipherSpec (epoch: %d)", srvCliStr(c.state.IsClient), newRemoteEpoch)
+ var receivedAlert *alertError
+ if errors.As(err, &receivedAlert) && receivedAlert.IsFatalOrCloseNotify() {
+ return packetOutcome{}, receivedAlert
+ }
- if c.state.GetRemoteEpoch()+1 == newRemoteEpoch {
- c.setRemoteEpoch(newRemoteEpoch)
- isLatestSeqNum = markPacketAsValid()
- }
- case *protocol.ApplicationData:
- if header.Epoch == 0 {
- return false, false, &alert.Alert{
- Level: alert.Fatal, Description: alert.UnexpectedMessage,
- }, dtlserrors.ErrApplicationDataEpochZero
- }
+ return outcome, err
+}
- isLatestSeqNum = markPacketAsValid()
+func (c *Conn) syncFragmentBufferHandshakeSequence() {
+ handshakeRecvSequence := dtlsstate.HandshakeRecvSequence(c.state)
+ if c.fragmentBuffer == nil || handshakeRecvSequence <= 0 ||
+ handshakeRecvSequence > int(^uint16(0)) {
+ return
+ }
- select {
- case c.decrypted <- content.Data:
- case <-c.closed.Done():
- case <-ctx.Done():
- }
+ c.fragmentBuffer.AdvanceTo(uint16(handshakeRecvSequence))
+}
- default:
- return false, false, &alert.Alert{
- Level: alert.Fatal, Description: alert.UnexpectedMessage,
- }, fmt.Errorf("%w: %d", dtlserrors.ErrUnhandledContextType, content.ContentType())
+func (c *Conn) recvHandshake() <-chan dtlshandshake.RecvHandshakeState {
+ if c.detached != nil && !c.detached.quiescentSkip.CompareAndSwap(true, false) {
+ c.detached.markQuiescent()
}
- // Any valid connection ID record is a candidate for updating the remote
- // address if it is the latest record received.
- // https://datatracker.ietf.org/doc/html/rfc9146#peer-address-update
- if originalCID && isLatestSeqNum {
- if rAddr != c.RemoteAddr() {
- c.lock.Lock()
- c.rAddr = rAddr
- c.lock.Unlock()
- }
- }
+ return c.handshakeRecv
+}
- return false, false, nil, nil
+func (c *Conn) signalHandshakeQuiescent() {
+ if c.detached != nil {
+ c.detached.markQuiescent()
+ }
}
-func (c *Conn) recvHandshake() <-chan recvHandshakeState {
- return c.handshakeRecv
+func (c *Conn) signalHandshakeTerminated(err error) {
+ if c.detached != nil {
+ c.detached.connectionTerminated(err)
+ }
}
func (c *Conn) notify(ctx context.Context, level alert.Level, desc alert.Description) error {
- if level == alert.Fatal && len(c.state.SessionID) > 0 { //nolint:nestif
- if c.state.LocalVersion == protocol.Version1_2 {
+ common := dtlsstate.CommonState(c.state)
+ if level == alert.Fatal && len(common.SessionID) > 0 { //nolint:nestif
+ if common.LocalVersion == protocol.Version1_2 {
// According to the RFC, we need to delete the stored session.
// https://datatracker.ietf.org/doc/html/rfc5246#section-7.2
- if ss := c.fsm.(*handshakeFSM12).cfg.sessionStore; ss != nil { //nolint:forcetypeassert
- c.log.Tracef("clean invalid session: %s", c.state.SessionID)
- if err := ss.Del(c.sessionKey()); err != nil {
+ if c.handshakeConfig.HasSessionStore {
+ c.log.Tracef("clean invalid session: %s", common.SessionID)
+ if err := c.handshakeConfig.DelSession(c.sessionKey()); err != nil {
return err
}
}
}
}
- // This should be updated with DTLS 1.3 record encoding.
- return c.writePackets(ctx, []*packet{
- {
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Epoch: c.state.GetLocalEpoch(),
- Version: protocol.Version1_2,
- },
- Content: &alert.Alert{
- Level: level,
- Description: desc,
- },
- },
- shouldWrapCID: len(c.state.RemoteConnectionID) > 0,
- shouldEncrypt: c.isHandshakeCompletedSuccessfully(),
+ outbound := &dtlsflight.Outbound{
+ Epoch: common.LocalEpoch(),
+ Content: &alert.Alert{
+ Level: level,
+ Description: desc,
},
- })
-}
+ }
+ if c.isHandshakeCompletedSuccessfully() {
+ outbound.Protection = dtlsflight.ProtectionCiphertext
+ }
-func (c *Conn) setHandshakeCompletedSuccessfully() bool {
- return c.handshakeCompletedSuccessfully.CompareAndSwap(false, true)
+ return c.writePackets(ctx, []*dtlsflight.Outbound{outbound})
}
func (c *Conn) isHandshakeCompletedSuccessfully() bool {
- return c.handshakeCompletedSuccessfully.Load()
+ return c.handshakeEstablished != nil && c.handshakeEstablished.Established()
}
func (c *Conn) negotiateVersionServer(ctx context.Context) error {
for {
+ c.signalHandshakeQuiescent()
if err := c.readAndBufferNoFSM(ctx); err != nil {
return err
}
if ok, err := c.pickVersionFromClientHello(); err != nil {
+ var negotiationAlert *alert.Alert
+ errors.As(err, &negotiationAlert)
+ if alertErr := c.notify(ctx, negotiationAlert.Level, negotiationAlert.Description); alertErr != nil {
+ return errors.Join(err, alertErr)
+ }
+
return err
} else if ok {
return nil
@@ -1277,65 +2626,130 @@ func (c *Conn) negotiateVersionServer(ctx context.Context) error {
}
//nolint:cyclop
-func (c *Conn) negotiateVersionClient(ctx context.Context) ([]*packet, *handshakeTranscript13, error) {
- transcript := newHandshakeTranscript13()
- pkts, dtlsAlert, err := flight13_1Generate(c, &handshakeContext13{
- state: &c.state,
- cache: c.handshakeCache,
- cfg: c.handshakeConfig,
- transcript: transcript,
- })
+func (c *Conn) negotiateVersionClient(ctx context.Context) ([]*dtlsflight.Outbound, error) {
+ gen, _, ok := dtlsflight13.GetGenerator(dtlsflight13.Flight1)
+ if !ok {
+ return nil, dtlserrors.ErrFlightUnimplemented13
+ }
+ state13 := dtlsstate.Activate13(c.state)
+ c.state = state13
+ pkts, dtlsAlert, err := gen(adaptFlightConn(c), state13, c.handshakeCache, c.handshakeConfig,
+ dtlshandshake.NewTranscript().FinalizeClientHello)
if dtlsAlert != nil {
if alertErr := c.notify(ctx, dtlsAlert.Level, dtlsAlert.Description); alertErr != nil && err == nil {
err = alertErr
}
}
if err != nil {
- return nil, nil, err
+ return nil, err
}
c.stampHandshakeSequence(pkts)
- if appended, err := appendClientHelloInitialFlights13(transcript, pkts); err != nil {
- return nil, nil, err
- } else if !appended {
- return nil, nil, dtlserrors.ErrHandshakeTranscriptMissingClientHello
+ if err := dtlshandshake.ValidateClientHelloInitialFlights(pkts); err != nil {
+ return nil, err
}
if err := c.writePackets(ctx, pkts); err != nil {
- return nil, nil, err
+ return nil, err
}
+ interval := c.handshakeConfig.InitialRetransmitInterval
+ // Preserve the flight's retry deadline across reads, including discarded datagrams.
+ // DTLS 1.2: partial reads do not reset the timer:
+ // https://www.rfc-editor.org/rfc/rfc6347.html#section-4.2.4
+ // DTLS 1.3: Figure 11 keeps ordinary record reception in WAITING and the timer
+ // is armed on sending only:
+ // https://www.rfc-editor.org/rfc/rfc9147.html#section-5.8.1
+ retryDeadline := time.Now().Add(interval)
for {
- if err := c.readAndBufferNoFSM(ctx); err != nil {
- return nil, nil, err
+ timedOut, err := c.readVersionNegotiationResponse(ctx, time.Until(retryDeadline))
+ if err != nil {
+ return nil, err
+ }
+ if timedOut {
+ if err := c.writePackets(ctx, pkts); err != nil {
+ return nil, err
+ }
+ if !c.handshakeConfig.DisableRetransmitBackoff {
+ interval = min(2*interval, 60*time.Second)
+ }
+ retryDeadline = time.Now().Add(interval)
+
+ continue
}
if ok, err := c.pickVersionFromServerResponse(); err != nil {
- return nil, nil, err
+ var negotiationAlert *alert.Alert
+ errors.As(err, &negotiationAlert)
+ if alertErr := c.notify(ctx, negotiationAlert.Level, negotiationAlert.Description); alertErr != nil {
+ return nil, errors.Join(err, alertErr)
+ }
+
+ return nil, err
} else if ok {
- return pkts, transcript, nil
+ return pkts, nil
}
// ServerHello or HelloVerifyRequest not yet (fully) received; keep reading.
}
}
+// readVersionNegotiationResponse keeps the initial ClientHello flight retryable
+// before version negotiation has selected a handshake state machine.
+func (c *Conn) readVersionNegotiationResponse(ctx context.Context, timeout time.Duration) (bool, error) {
+ if err := ctx.Err(); err != nil {
+ return false, err
+ }
+ // An Incoming datagrams shouldn't postpone an already-due retransmission.
+ if timeout <= 0 {
+ return true, nil
+ }
+ c.signalHandshakeQuiescent()
+ readCtx, cancel := context.WithCancelCause(ctx)
+ timer := c.handshakeConfig.NewTimer(timeout)
+ done := make(chan struct{})
+ go func() {
+ defer close(done)
+ select {
+ case <-timer.C():
+ cancel(context.DeadlineExceeded)
+ case <-readCtx.Done():
+ }
+ }()
+ err := c.readAndBufferNoFSM(readCtx)
+ timer.Stop()
+ cause := context.Cause(readCtx)
+ cancel(nil)
+ <-done
+ if ctx.Err() != nil {
+ return false, ctx.Err()
+ }
+ // A read may deliver a datagram or another error concurrently with expiry.
+ if errors.Is(err, context.Canceled) && errors.Is(cause, context.DeadlineExceeded) {
+ return true, nil
+ }
+
+ return false, err
+}
+
// pickVersionFromClientHello inspects the handshake cache for incoming
// ClientHello and, if found, sets localVersion and remoteVersions.
// Returns true once the version can be decided.
func (c *Conn) pickVersionFromClientHello() (bool, error) {
- _, msgs, ok := c.handshakeCache.fullPullMap(0, c.state.CipherSuite,
- handshakeCachePullRule{handshake.TypeClientHello, c.handshakeConfig.initialEpoch, true, false},
+ pull := c.handshakeCache.FullPullMapItems(0, dtlsstate.CommonState(c.state).CipherSuite,
+ dtlsflight.HandshakeCachePullRule{Typ: handshake.TypeClientHello, Epoch: c.handshakeConfig.InitialEpoch, IsClient: true, Optional: false},
)
- if !ok {
+ if pull.Err != nil {
+ return false, pull.Err
+ }
+ if !pull.Ready {
return false, nil
}
- ch, ok := msgs[handshake.TypeClientHello].(*handshake.MessageClientHello)
+ ch, ok := pull.Messages[handshake.TypeClientHello].(*handshake.MessageClientHello)
if !ok {
return false, nil
}
-
var remote []protocol.Version
seenSupportedVersions := false
for _, e := range ch.Extensions {
- if sv, ok := e.(*extension.SupportedVersions); ok { //nolint:govet
+ if sv, ok := e.(*extension13.OfferedVersions); ok { //nolint:govet
seenSupportedVersions = true
remote = sv.Versions
@@ -1346,13 +2760,12 @@ func (c *Conn) pickVersionFromClientHello() (bool, error) {
remote = []protocol.Version{ch.Version}
}
- chosen, ok := selectVersion(remote, c.handshakeConfig.minVersion, c.handshakeConfig.maxVersion)
+ chosen, ok := dtlsconfig.SelectVersion(remote, c.handshakeConfig.MinVersion, c.handshakeConfig.MaxVersion)
if !ok {
- return false, dtlserrors.ErrNoCommonProtocolVersion
+ return false, fmt.Errorf("%w: %w", dtlserrors.ErrNoCommonProtocolVersion, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion})
}
- c.state.RemoteVersions = remote
- c.state.LocalVersion = chosen
+ c.setNegotiatedVersion(remote, chosen)
return true, nil
}
@@ -1366,7 +2779,24 @@ func (c *Conn) pickVersionFromClientHello() (bool, error) {
// - ServerHello without supported_versions: fall back to ServerHello.Version.
// - HelloVerifyRequest (1.2 cookie request): version is 1.2.
func (c *Conn) pickVersionFromServerResponse() (bool, error) {
- if sh, ok := c.findCachedServerMessage(handshake.TypeServerHello).(*handshake.MessageServerHello); ok {
+ pull := c.handshakeCache.FullPullMapOneOfItems(
+ 0,
+ dtlsstate.CommonState(c.state).CipherSuite,
+ dtlsflight.HandshakeCachePullRule{
+ Typ: handshake.TypeServerHello, Epoch: c.handshakeConfig.InitialEpoch, IsClient: false,
+ },
+ dtlsflight.HandshakeCachePullRule{
+ Typ: handshake.TypeHelloVerifyRequest, Epoch: c.handshakeConfig.InitialEpoch, IsClient: false,
+ },
+ )
+ if pull.Err != nil {
+ return false, pull.Err
+ }
+ if !pull.Ready {
+ return false, nil
+ }
+
+ if sh, ok := pull.Messages[handshake.TypeServerHello].(*handshake.MessageServerHello); ok {
if err := c.pickVersionFromServerHello(sh); err != nil {
return false, err
}
@@ -1374,8 +2804,8 @@ func (c *Conn) pickVersionFromServerResponse() (bool, error) {
return true, nil
}
- if hvr, ok := c.findCachedServerMessage(handshake.TypeHelloVerifyRequest).(*handshake.MessageHelloVerifyRequest); ok {
- if err := c.pickVersionFromHelloVerifyRequest(hvr); err != nil {
+ if hvr, ok := pull.Messages[handshake.TypeHelloVerifyRequest].(*handshake.MessageHelloVerifyRequest); ok {
+ if err := c.selectRemoteVersion([]protocol.Version{hvr.Version}); err != nil {
return false, err
}
@@ -1386,6 +2816,13 @@ func (c *Conn) pickVersionFromServerResponse() (bool, error) {
}
func (c *Conn) pickVersionFromServerHello(sh *handshake.MessageServerHello) error {
+ common := dtlsstate.CommonState(c.state)
+ if err := negotiation.ValidateServerHelloResponse(
+ common.LocalClientHelloSnapshots.Current(),
+ sh,
+ ); err != nil {
+ return err
+ }
remote, err := remoteVersionsFromServerHello(sh)
if err != nil {
return err
@@ -1394,19 +2831,17 @@ func (c *Conn) pickVersionFromServerHello(sh *handshake.MessageServerHello) erro
return c.selectRemoteVersion(remote)
}
-func (c *Conn) pickVersionFromHelloVerifyRequest(hvr *handshake.MessageHelloVerifyRequest) error {
- c.state.LocalVersion = protocol.Version1_2
-
- return c.selectRemoteVersion([]protocol.Version{hvr.Version})
-}
-
func remoteVersionsFromServerHello(sh *handshake.MessageServerHello) ([]protocol.Version, error) {
- remote, seenSupportedVersions, err := serverHelloSelectedVersions(sh.Extensions)
- if isHelloRetryRequest(sh) {
+ remote, seenSupportedVersions, err := dtlsflight13.ServerHelloSelectedVersions(sh.Extensions)
+ if dtlsflight13.IsHelloRetryRequest(sh) {
return remoteVersionsFromHelloRetryRequest(remote, seenSupportedVersions, err)
}
if err != nil {
- return nil, err
+ return nil, fmt.Errorf(
+ "%w: %w",
+ err,
+ &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter},
+ )
}
if !seenSupportedVersions {
return []protocol.Version{sh.Version}, nil
@@ -1415,56 +2850,66 @@ func remoteVersionsFromServerHello(sh *handshake.MessageServerHello) ([]protocol
return remote, nil
}
-func remoteVersionsFromHelloRetryRequest(
- remote []protocol.Version,
- seenSupportedVersions bool,
- err error,
-) ([]protocol.Version, error) {
- if err != nil || !seenSupportedVersions {
- return nil, dtlserrors.ErrInvalidHelloRetryRequest
+func remoteVersionsFromHelloRetryRequest(remote []protocol.Version, seenSupportedVersions bool, err error) ([]protocol.Version, error) {
+ if err != nil {
+ return nil, fmt.Errorf("%w: %w", dtlserrors.ErrInvalidHelloRetryRequest, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter})
}
- if !remote[0].Equal(protocol.Version1_3) {
- return nil, dtlserrors.ErrUnsupportedProtocolVersion
+ if !seenSupportedVersions {
+ return nil, fmt.Errorf("%w: %w", dtlserrors.ErrInvalidHelloRetryRequest, &alert.Alert{Level: alert.Fatal, Description: alert.MissingExtension})
+ }
+ if remote[0] != protocol.Version1_3 {
+ return nil, fmt.Errorf("%w: %w", dtlserrors.ErrUnsupportedProtocolVersion, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion})
}
return remote, nil
}
func (c *Conn) selectRemoteVersion(remote []protocol.Version) error {
- chosen, ok := selectVersion(remote, c.handshakeConfig.minVersion, c.handshakeConfig.maxVersion)
+ chosen, ok := dtlsconfig.SelectVersion(remote, c.handshakeConfig.MinVersion, c.handshakeConfig.MaxVersion)
if !ok {
- return dtlserrors.ErrNoCommonProtocolVersion
+ return fmt.Errorf("%w: %w", dtlserrors.ErrNoCommonProtocolVersion, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion})
+ }
+ if dtlsstate.CommonState(c.state).IsClient && c.handshakeConfig.ECHConfigList != nil && chosen != protocol.Version1_3 {
+ return fmt.Errorf("%w: %w", dtlserrors.ErrUnsupportedProtocolVersion, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion})
}
- c.state.RemoteVersions = remote
- c.state.LocalVersion = chosen
+ c.setNegotiatedVersion(remote, chosen)
return nil
}
-// findCachedServerMessage pulls the most recent handshake message of the
-// given type sent by the peer from the cache, if any.
-func (c *Conn) findCachedServerMessage(t handshake.Type) handshake.Message {
- _, msgs, ok := c.handshakeCache.fullPullMap(0, c.state.CipherSuite,
- handshakeCachePullRule{t, c.handshakeConfig.initialEpoch, false, true},
- )
- if !ok {
- return nil
+func (c *Conn) setNegotiatedVersion(remote []protocol.Version, chosen protocol.Version) {
+ common := dtlsstate.CommonState(c.state)
+ wasUnselected := common.LocalVersion == 0
+ common.RemoteVersions = remote
+ common.LocalVersion = chosen
+ if chosen == protocol.Version1_3 {
+ c.state = dtlsstate.Activate13(c.state)
+ if wasUnselected && c.handshakeCache != nil {
+ // Only the peer's initial hello was needed to choose the version.
+ // Drop speculative later messages and fragments, including any
+ // sequence progress they made before epoch checks were active.
+ c.handshakeCache.DiscardRemoteAfter(0, !common.IsClient)
+ c.fragmentBuffer = dtlsfragmentbuffer.New()
+ if _, ok := c.handshakeCache.PullExact(0, !common.IsClient); ok {
+ c.fragmentBuffer.AdvanceTo(1)
+ }
+ }
+
+ return
}
- return msgs[t]
+ c.state = dtlsstate.Activate12(c.state)
}
// stampHandshakeSequence assigns the DTLS message_sequence to each handshake
-// record in pkts, using and advancing state.handshakeSendSequence. This is
-// the subset of handshakeFSM.prepare()'s bookkeeping that generated dual-stack
-// packets need before being passed to writePackets.
-func (c *Conn) stampHandshakeSequence(pkts []*packet) {
- epoch := c.handshakeConfig.initialEpoch
+// record in pkts. This is the subset of handshakeFSM.prepare()'s bookkeeping
+// that generated dual-stack packets need before being passed to writePackets.
+func (c *Conn) stampHandshakeSequence(pkts []*dtlsflight.Outbound) {
+ epoch := c.handshakeConfig.InitialEpoch
for _, p := range pkts {
- p.record.Header.Epoch += epoch
- if h, ok := p.record.Content.(*handshake.Handshake); ok {
- h.Header.MessageSequence = uint16(c.state.HandshakeSendSequence) //nolint:gosec // G115
- c.state.HandshakeSendSequence++
+ p.Epoch += epoch
+ if h, ok := p.Content.(*handshake.Handshake); ok {
+ h.Header.MessageSequence = dtlsstate.NextHandshakeSendSequence(c.state)
}
}
}
@@ -1475,14 +2920,14 @@ func (c *Conn) stampHandshakeSequence(pkts []*packet) {
// its retransmit timer fires, since readAndBufferNoFSM does not signal.
// The send blocks until the FSM reaches wait() or the handshake is torn down.
func (c *Conn) primeHandshakeRecv(ctx context.Context) {
- s := recvHandshakeState{
- done: make(chan struct{}),
- isRetransmit: false,
+ s := dtlshandshake.RecvHandshakeState{
+ Done: make(chan struct{}),
+ IsRetransmit: false,
}
select {
case c.handshakeRecv <- s:
select {
- case <-s.done:
+ case <-s.Done:
case <-ctx.Done():
case <-c.fsm.Done():
}
@@ -1492,95 +2937,81 @@ func (c *Conn) primeHandshakeRecv(ctx context.Context) {
}
// readAndBufferNoFSM is a variant of readAndBuffer used during the dual-stack
-// version negotiation phase. It reads a datagram and pushes any handshake
-// fragments into handshakeCache, but does not signal an FSM (there is none
-// yet) or wait for its Done channel.
-func (c *Conn) readAndBufferNoFSM(ctx context.Context) error { //nolint:cyclop
- bufptr, ok := poolReadBuffer.Get().(*[]byte)
- if !ok {
- return dtlserrors.ErrFailedToAccessPoolReadBuffer
+// version negotiation phase. It reads and processes a datagram, but does not
+// signal an FSM (there is none yet) or wait for its Done channel.
+func (c *Conn) readAndBufferNoFSM(ctx context.Context) error {
+ _, err := c.readAndProcessDatagram(ctx)
+
+ return err
+}
+
+func (c *Conn) classifyReadLoopError(err error) readLoopErrorAction {
+ if c.detached != nil && c.isConnectionClosed() {
+ return readLoopStop
+ }
+ var receivedAlert *alertError
+ if errors.As(err, &receivedAlert) {
+ return c.classifyReadAlert(receivedAlert)
}
- defer poolReadBuffer.Put(bufptr)
- b := *bufptr
- i, rAddr, err := c.nextConn.ReadFromContext(ctx, b)
- if err != nil {
- return netError(err)
+ switch {
+ case errors.Is(err, recordlayer.ErrInvalidPacketLength):
+ // Decode error must be silently discarded [RFC6347 Section-4.1.2.7].
+ return readLoopContinue
+ case errors.Is(err, context.Canceled) && !c.isConnectionClosed():
+ return readLoopCloseAndStop
+ case errors.Is(err, context.DeadlineExceeded),
+ errors.Is(err, context.Canceled),
+ errors.Is(err, io.EOF),
+ errors.Is(err, net.ErrClosed):
+ return readLoopStop
+ case c.isHandshakeCompletedSuccessfully():
+ return readLoopDeliverAndContinue
+ default:
+ return readLoopStop
}
+}
- pkts, err := recordlayer.ContentAwareUnpackDatagram(b[:i], len(c.state.GetLocalConnectionID()))
- if err != nil {
- return err
+func (c *Conn) classifyReadAlert(receivedAlert *alertError) readLoopErrorAction {
+ if receivedAlert.IsFatalOrCloseNotify() {
+ return readLoopCloseAndStop
+ }
+ if c.isHandshakeCompletedSuccessfully() {
+ return readLoopDeliverAndContinue
}
- for _, p := range pkts {
- // nolint:godox
- // TODO: check version
- _, _, alert, err := c.handleIncomingPacket(ctx, p, rAddr, true)
- if alert != nil {
- if alertErr := c.notify(ctx, alert.Level, alert.Description); alertErr != nil {
- if err == nil {
- err = alertErr
- }
- }
- }
+ return readLoopContinue
+}
- var e *alertError
- if errors.As(err, &e) && e.IsFatalOrCloseNotify() {
- return e
- }
- if err != nil {
- return err
- }
+func (c *Conn) deliverReadError(ctx context.Context, err error) {
+ // Detached connections have no Read consumer. Fatal errors use the terminal event.
+ if c.detached != nil {
+ return
+ }
+ select {
+ case c.decrypted <- err:
+ case <-c.closed.Done():
+ case <-ctx.Done():
}
-
- return nil
}
-//nolint:gocyclo,cyclop,gocognit,contextcheck
+//nolint:cyclop,contextcheck
func (c *Conn) handshake(ctx context.Context, start handshakeStart) error {
- done := make(chan struct{})
- if c.state.LocalVersion == protocol.Version1_3 {
- fsm, err := newHandshakeFSM13(
- &c.state,
- c.handshakeCache,
- c.handshakeConfig,
- flightVal13(start.flight),
- start.flights,
- start.transcript13,
- )
- if err != nil {
+ if dtlsstate.CommonState(c.state).LocalVersion == protocol.Version1_3 {
+ if err := c.setupHandshakeFSM13(start); err != nil {
return err
}
- c.fsm = fsm
- c.handshakeConfig.onFlightState13 = func(_ flightVal13, s handshakeState) {
- // The ACK for the last flights has been received and we are in a Finished state.
- // nolint:godox
- // TODO: should be moved to FSM.
- if s == handshakeFinished && c.setHandshakeCompletedSuccessfully() {
- close(done)
- }
- }
} else {
- c.fsm = &handshakeFSM12{
- currentFlight: start.flight,
- flights: start.flights,
- retransmit: start.flights != nil,
- state: &c.state,
- cache: c.handshakeCache,
- cfg: c.handshakeConfig,
- retransmitInterval: c.handshakeConfig.initialRetransmitInterval,
- closed: make(chan struct{}),
- }
- c.handshakeConfig.onFlightState = func(_ flightVal, s handshakeState) {
- if s == handshakeFinished && c.setHandshakeCompletedSuccessfully() {
- close(done)
- }
+ if err := c.setupHandshakeFSM12(start); err != nil {
+ return err
}
}
ctxRead, cancelRead := context.WithCancel(context.Background())
ctxHs, cancel := context.WithCancel(context.Background())
+ if c.detached != nil && start.postSetup != nil {
+ c.detached.quiescentSkip.Store(true)
+ }
c.closeLock.Lock()
c.cancelHandshaker = cancel
@@ -1591,13 +3022,17 @@ func (c *Conn) handshake(ctx context.Context, start handshakeStart) error {
var handshakeLoopsFinished sync.WaitGroup
handshakeLoopsFinished.Add(2)
+ c.closeLock.Lock()
+ c.handshakeLoops = &handshakeLoopsFinished
+ c.closeLock.Unlock()
// Handshake routine should be live until close.
// The other party may request retransmission of the last flight to cope with packet drop.
go func() {
defer handshakeLoopsFinished.Done()
- err := c.fsm.Run(ctxHs, c, start.fsmState)
+ err := c.fsm.Run(ctxHs, handshakeConn{c}, start.fsmState)
if !errors.Is(err, context.Canceled) {
+ c.signalHandshakeTerminated(err)
select {
case firstErr <- err:
default:
@@ -1605,15 +3040,11 @@ func (c *Conn) handshake(ctx context.Context, start handshakeStart) error {
}
}()
- if start.postSetup != nil {
- start.postSetup(ctxHs)
- }
-
go func() {
defer func() {
if c.isHandshakeCompletedSuccessfully() {
// Escaping read loop.
- // It's safe to close decrypted channnel now.
+ // It's safe to close the decrypted channel now.
close(c.decrypted)
}
@@ -1621,63 +3052,44 @@ func (c *Conn) handshake(ctx context.Context, start handshakeStart) error {
cancel()
}()
defer handshakeLoopsFinished.Done()
+ if start.postSetup != nil {
+ start.postSetup(ctxHs)
+ }
for {
- if err := c.readAndBuffer(ctxRead); err != nil { //nolint:nestif
- var alertErr *alertError
- if errors.As(err, &alertErr) {
- if !alertErr.IsFatalOrCloseNotify() {
- if c.isHandshakeCompletedSuccessfully() {
- // Pass the error to Read()
- select {
- case c.decrypted <- err:
- case <-c.closed.Done():
- case <-ctxRead.Done():
- }
- }
-
- continue // non-fatal alert must not stop read loop
- }
- } else {
- switch {
- case errors.Is(err, context.DeadlineExceeded),
- errors.Is(err, context.Canceled),
- errors.Is(err, io.EOF),
- errors.Is(err, net.ErrClosed):
- case errors.Is(err, recordlayer.ErrInvalidPacketLength):
- // Decode error must be silently discarded
- // [RFC6347 Section-4.1.2.7]
- continue
- default:
- if c.isHandshakeCompletedSuccessfully() {
- // Keep read loop and pass the read error to Read()
- select {
- case c.decrypted <- err:
- case <-c.closed.Done():
- case <-ctxRead.Done():
- }
-
- continue // non-fatal alert must not stop read loop
- }
- }
- }
+ err := c.readAndBuffer(ctxRead)
+ if err == nil {
+ continue
+ }
- select {
- case firstErr <- err:
- default:
- }
+ action := c.classifyReadLoopError(err)
+ if action == readLoopContinue {
+ c.signalHandshakeQuiescent()
- if alertErr != nil {
- if alertErr.IsFatalOrCloseNotify() {
- _ = c.close(false) //nolint:contextcheck
- }
- }
- if !c.isConnectionClosed() && errors.Is(err, context.Canceled) {
- c.log.Trace("handshake timeouts - closing underline connection")
- _ = c.close(false) //nolint:contextcheck
- }
+ continue
+ }
+ if action == readLoopDeliverAndContinue {
+ c.deliverReadError(ctxRead, err)
+ c.signalHandshakeQuiescent()
+
+ continue
+ }
+
+ select {
+ case firstErr <- err:
+ default:
+ }
- return
+ if !errors.Is(err, context.Canceled) {
+ c.signalHandshakeTerminated(err)
+ }
+ if action == readLoopCloseAndStop {
+ if errors.Is(err, context.Canceled) {
+ c.log.Trace("handshake timeouts - closing underlying connection")
+ }
+ _ = c.close(false) //nolint:contextcheck
}
+
+ return
}
}()
@@ -1694,11 +3106,35 @@ func (c *Conn) handshake(ctx context.Context, start handshakeStart) error {
handshakeLoopsFinished.Wait()
return c.translateHandshakeCtxError(ctx.Err())
- case <-done:
+ case <-c.handshakeEstablished.Done():
return nil
}
}
+func (c *Conn) setupHandshakeFSM13(start handshakeStart) error {
+ state13, err := dtlsstate.As13(c.state)
+ if err != nil {
+ return err
+ }
+ fsm, err := dtlshandshake.NewFSM13(state13, c.handshakeCache, c.handshakeConfig, start.flight13, start.flights, c.handshakeEstablished)
+ if err != nil {
+ return err
+ }
+ c.fsm = fsm
+
+ return nil
+}
+
+func (c *Conn) setupHandshakeFSM12(start handshakeStart) error {
+ state12, err := dtlsstate.As12(c.state)
+ if err != nil {
+ return err
+ }
+ c.fsm = dtlshandshake.NewFSM12(state12, c.handshakeCache, c.handshakeConfig, start.flight12, start.flights, c.handshakeEstablished)
+
+ return nil
+}
+
func (c *Conn) translateHandshakeCtxError(err error) error {
if err == nil {
return nil
@@ -1714,32 +3150,35 @@ func (c *Conn) close(byUser bool) error {
c.closeLock.Lock()
cancelHandshaker := c.cancelHandshaker
cancelHandshakeReader := c.cancelHandshakeReader
- c.closeLock.Unlock()
-
- cancelHandshaker()
- cancelHandshakeReader()
-
- if c.isHandshakeCompletedSuccessfully() && byUser {
- // Discard error from notify() to return non-error on the first user call of Close()
- // even if the underlying connection is already closed.
- _ = c.notify(context.Background(), alert.Warning, alert.CloseNotify)
- }
-
- c.closeLock.Lock()
- // Don't return ErrConnClosed at the first time of the call from user.
closedByUser := c.connectionClosedByUser
if byUser {
c.connectionClosedByUser = true
}
isClosed := c.isConnectionClosed()
- c.closed.Close()
+ if !isClosed {
+ c.closed.Close()
+ }
c.closeLock.Unlock()
- if closedByUser {
- return ErrConnClosed
+ cancelHandshaker()
+ cancelHandshakeReader()
+
+ if closedByUser || isClosed {
+ return nil
+ }
+
+ if c.isHandshakeCompletedSuccessfully() {
+ if byUser {
+ // Discard error from notify() to return non-error on user Close()
+ // even if the underlying connection is already closed.
+ _ = c.notify(context.Background(), alert.Warning, alert.CloseNotify)
+ }
+ if !c.exporting.Load() {
+ c.clearConnectionIDs()
+ }
}
- if isClosed {
+ if c.detached != nil {
return nil
}
@@ -1747,24 +3186,61 @@ func (c *Conn) close(byUser bool) error {
}
func (c *Conn) isConnectionClosed() bool {
- select {
- case <-c.closed.Done():
- return true
- default:
- return false
- }
+ return c.closed.Err() != nil
+}
+
+func (c *Conn) setLocalEpoch(epoch uint64) {
+ dtlsstate.CommonState(c.state).SetLocalEpoch(epoch)
+}
+
+func (c *Conn) setRemoteEpoch(epoch uint64) {
+ dtlsstate.CommonState(c.state).SetRemoteEpoch(epoch)
}
-func (c *Conn) setLocalEpoch(epoch uint16) {
- c.state.LocalEpoch.Store(epoch)
+func (c *Conn) commitLocalKeyUpdate(generation *dtlsstate.TrafficGeneration) error {
+ c.writeLock.Lock()
+ defer c.writeLock.Unlock()
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ state13, ok := c.state.(*dtlsstate.State13)
+ if !ok || state13.TrafficKeys == nil {
+ return dtlserrors.ErrInvalidProtocolVersionState
+ }
+ current, _ := state13.TrafficKeys.CurrentWrite()
+ if err := validateNextWriteGeneration(current, generation, state13.LocalEpoch()); err != nil {
+ return err
+ }
+
+ state13.TrafficKeys.Install(generation, nil)
+ state13.SetLocalEpoch(generation.Epoch)
+
+ return nil
}
-func (c *Conn) setRemoteEpoch(epoch uint16) {
- c.state.RemoteEpoch.Store(epoch)
+func validateNextWriteGeneration(
+ current, next *dtlsstate.TrafficGeneration,
+ localEpoch uint64,
+) error {
+ if current == nil || next == nil {
+ return dtlserrors.ErrInvalidEpoch
+ }
+ if current.Epoch == ^uint64(0) || current.Generation == ^uint64(0) {
+ return dtlserrors.ErrEpochOverflow
+ }
+ if current.Epoch != localEpoch || next.Epoch != current.Epoch+1 || next.Generation != current.Generation+1 {
+ return dtlserrors.ErrInvalidEpoch
+ }
+
+ return nil
}
// LocalAddr implements net.Conn.LocalAddr.
func (c *Conn) LocalAddr() net.Addr {
+ if c.detached != nil {
+ return nil
+ }
+
return c.nextConn.LocalAddr()
}
@@ -1777,18 +3253,15 @@ func (c *Conn) RemoteAddr() net.Addr {
}
func (c *Conn) sessionKey() []byte {
- if c.state.IsClient {
+ common := dtlsstate.CommonState(c.state)
+ if common.IsClient {
// As ServerName can be like 0.example.com, it's better to add
// delimiter character which is not allowed to be in
// neither address or domain name.
- if c.state.LocalVersion == protocol.Version1_3 {
- return []byte(c.rAddr.String() + "_" + c.fsm.(*handshakeFSM13).cfg.serverName) //nolint:forcetypeassert
- }
-
- return []byte(c.rAddr.String() + "_" + c.fsm.(*handshakeFSM12).cfg.serverName) //nolint:forcetypeassert
+ return []byte(c.rAddr.String() + "_" + c.handshakeConfig.ServerName)
}
- return c.state.SessionID
+ return common.SessionID
}
// SetDeadline implements net.Conn.SetDeadline.
@@ -1812,3 +3285,28 @@ func (c *Conn) SetWriteDeadline(t time.Time) error {
// Write deadline is also fully managed by this layer.
return nil
}
+
+func (c *Conn) acceptEarlyApplicationData(epoch, size uint64) (bool, error) {
+ state, ok := c.state.(*dtlsstate.State13)
+ if !ok || epoch != dtlsflight13.EpochEarlyData {
+ return true, nil
+ }
+ if state.EarlyDataStatus != dtlsstate.EarlyDataAccepted || c.detached == nil {
+ return false, nil
+ }
+ if size > uint64(state.EarlyDataLimit)-state.EarlyDataBytes {
+ return false, dtlserrors.ErrInvalidPacketLength
+ }
+ state.EarlyDataBytes += size
+
+ return true, nil
+}
+
+func (c *Conn) pushHandshake(data []byte, epoch uint64, sequence uint16, typ handshake.Type, isClient bool) error {
+ if dtlsstate.CommonState(c.state).LocalVersion != protocol.Version1_3 {
+ return c.handshakeCache.PushBounded12(data, epoch, sequence, typ, isClient)
+ }
+ c.handshakeCache.Push(data, epoch, sequence, typ, isClient)
+
+ return nil
+}
diff --git a/conn_go_test.go b/conn_go_test.go
deleted file mode 100644
index bb2719ace..000000000
--- a/conn_go_test.go
+++ /dev/null
@@ -1,155 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-//go:build !js
-
-package dtls
-
-import (
- "context"
- "errors"
- "net"
- "testing"
- "time"
-
- "github.com/pion/dtls/v3/pkg/crypto/selfsign"
- dtlsnet "github.com/pion/dtls/v3/pkg/net"
- "github.com/pion/transport/v4/dpipe"
- "github.com/pion/transport/v4/test"
- "github.com/stretchr/testify/assert"
-)
-
-func TestContextConfig(t *testing.T) { //nolint:cyclop
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- addrListen, err := net.ResolveUDPAddr("udp", "localhost:0")
- assert.NoError(t, err)
-
- // Dummy listener
- listen, err := net.ListenUDP("udp", addrListen)
- assert.NoError(t, err)
- defer func() {
- _ = listen.Close()
- }()
- addr, ok := listen.LocalAddr().(*net.UDPAddr)
- assert.True(t, ok)
-
- cert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- clientOpts := []ClientOption{
- WithCertificates(cert),
- }
- serverOpts := []ServerOption{
- WithCertificates(cert),
- }
-
- dials := map[string]struct {
- f func() (func() (net.Conn, error), func())
- order []byte
- }{
- "Dial": {
- f: func() (func() (net.Conn, error), func()) {
- ctx, cancel := context.WithTimeout(context.Background(), 40*time.Millisecond)
-
- return func() (net.Conn, error) {
- conn, err := DialWithOptions("udp", addr, clientOpts...)
- if err != nil {
- return nil, err
- }
-
- return conn, conn.HandshakeContext(ctx)
- }, func() {
- cancel()
- }
- },
- order: []byte{0, 1, 2},
- },
- "Client": {
- f: func() (func() (net.Conn, error), func()) {
- ca, _ := dpipe.Pipe()
- ctx, cancel := context.WithTimeout(context.Background(), 40*time.Millisecond)
-
- return func() (net.Conn, error) {
- conn, err := ClientWithOptions(dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), clientOpts...)
- if err != nil {
- return nil, err
- }
-
- return conn, conn.HandshakeContext(ctx)
- }, func() {
- _ = ca.Close()
- cancel()
- }
- },
- order: []byte{0, 1, 2},
- },
- "Server": {
- f: func() (func() (net.Conn, error), func()) {
- ca, _ := dpipe.Pipe()
- ctx, cancel := context.WithTimeout(context.Background(), 40*time.Millisecond)
-
- return func() (net.Conn, error) {
- conn, err := ServerWithOptions(dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), serverOpts...)
- if err != nil {
- return nil, err
- }
-
- return conn, conn.HandshakeContext(ctx)
- }, func() {
- _ = ca.Close()
- cancel()
- }
- },
- order: []byte{0, 1, 2},
- },
- }
-
- for name, dial := range dials {
- t.Run(name, func(t *testing.T) {
- done := make(chan struct{})
-
- go func() {
- d, cancel := dial.f()
- conn, err := d()
- defer cancel()
- var netError net.Error
- if !errors.As(err, &netError) || !netError.Temporary() { //nolint:staticcheck
- assert.Fail(t, "Dial failed with unexpected error", "err: %v", err)
- close(done)
-
- return
- }
- done <- struct{}{}
- if err == nil {
- _ = conn.Close()
- }
- }()
-
- var order []byte
- early := time.After(20 * time.Millisecond)
- late := time.After(60 * time.Millisecond)
- func() {
- for len(order) < 3 {
- select {
- case <-early:
- order = append(order, 0)
- case _, ok := <-done:
- if !ok {
- return
- }
- order = append(order, 1)
- case <-late:
- order = append(order, 2)
- }
- }
- }()
- assert.Equal(t, dial.order, order, "Invalid cancel timing")
- })
- }
-}
diff --git a/conn_test.go b/conn_test.go
index 53eba1599..b05f54a87 100644
--- a/conn_test.go
+++ b/conn_test.go
@@ -6,138 +6,91 @@ package dtls
import (
"bytes"
"context"
- "crypto"
- "crypto/ecdsa"
- cryptoElliptic "crypto/elliptic"
- "crypto/rand"
- "crypto/rsa"
- "crypto/tls"
- "crypto/x509"
"encoding/hex"
"errors"
"fmt"
- "io"
"net"
- "strings"
"sync"
"sync/atomic"
"testing"
"time"
- "github.com/pion/dtls/v3/internal/ciphersuite"
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
- "github.com/pion/dtls/v3/pkg/crypto/hash"
- "github.com/pion/dtls/v3/pkg/crypto/selfsign"
- "github.com/pion/dtls/v3/pkg/crypto/signature"
- "github.com/pion/dtls/v3/pkg/crypto/signaturehash"
- dtlsnet "github.com/pion/dtls/v3/pkg/net"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
+ "github.com/pion/dtls/v4/internal/ciphersuite"
+ "github.com/pion/dtls/v4/internal/closer"
+ dtlsconfig "github.com/pion/dtls/v4/internal/config"
+ dtlserrors "github.com/pion/dtls/v4/internal/errors"
+ dtlsflight "github.com/pion/dtls/v4/internal/flight"
+ dtlsflight13 "github.com/pion/dtls/v4/internal/flight/flight13"
+ dtlsfragmentbuffer "github.com/pion/dtls/v4/internal/fragmentbuffer"
+ dtlshandshake "github.com/pion/dtls/v4/internal/handshake"
+ "github.com/pion/dtls/v4/internal/negotiation"
+ "github.com/pion/dtls/v4/internal/recordwire"
+ dtlsstate "github.com/pion/dtls/v4/internal/state"
+ cryptosuite "github.com/pion/dtls/v4/pkg/crypto/ciphersuite"
+ "github.com/pion/dtls/v4/pkg/crypto/elliptic"
+ "github.com/pion/dtls/v4/pkg/crypto/selfsign"
+ "github.com/pion/dtls/v4/pkg/crypto/signaturehash"
+ "github.com/pion/dtls/v4/pkg/protocol"
+ "github.com/pion/dtls/v4/pkg/protocol/alert"
+ "github.com/pion/dtls/v4/pkg/protocol/extension"
+ extension13 "github.com/pion/dtls/v4/pkg/protocol/extension/dtls13"
+ "github.com/pion/dtls/v4/pkg/protocol/handshake"
+ "github.com/pion/dtls/v4/pkg/protocol/recordlayer"
"github.com/pion/logging"
- "github.com/pion/transport/v4/dpipe"
- "github.com/pion/transport/v4/test"
+ "github.com/pion/transport/v5/deadline"
+ "github.com/pion/transport/v5/dpipe"
+ "github.com/pion/transport/v5/netctx"
+ "github.com/pion/transport/v5/test"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
-var (
- errTestPSKInvalidIdentity = errors.New("TestPSK: Server got invalid identity")
- errTestPSKClientInvalidIdentity = errors.New("TestPSK: Client got invalid identity")
- errPSKRejected = errors.New("psk Rejected")
- errNotExpectedChain = errors.New("not expected chain")
- errExpecedChain = errors.New("expected chain")
- errWrongCert = errors.New("wrong cert")
- errConnectionAttemptFailed = errors.New("connection attempt failed")
-)
-
-func TestStressDuplex(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- // Run the test
- stressDuplex(t)
+func defaultCipherSuites() []cryptosuite.Suite {
+ return defaultCipherSuitesForVersion(protocol.Version1_2)
}
-func stressDuplex(t *testing.T) {
- t.Helper()
-
- ca, cb, err := pipeMemory()
- assert.NoError(t, err)
-
- defer func() {
- assert.NoError(t, ca.Close())
- assert.NoError(t, cb.Close())
- }()
-
- opt := test.Options{
- MsgSize: 2048,
- MsgCount: 100,
+func cipherSuiteIDs(suites []cryptosuite.Suite) []uint16 {
+ ids := make([]uint16, len(suites))
+ for i, suite := range suites {
+ ids[i] = uint16(suite.ID())
}
- assert.NoError(t, test.StressDuplex(ca, cb, opt))
+ return ids
}
-func TestRoutineLeakOnClose(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(5 * time.Second)
- defer lim.Stop()
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
+func marshalTestRecord(header recordlayer.RecordConfig, content protocol.Content) ([]byte, error) {
+ payload, err := content.Marshal()
+ if err != nil {
+ return nil, err
+ }
- ca, cb, err := pipeMemory()
- assert.NoError(t, err)
+ header.ContentType = content.ContentType()
- _, err = ca.Write(make([]byte, 100))
- assert.NoError(t, err)
- assert.NoError(t, cb.Close())
- assert.NoError(t, ca.Close())
- // Packet is sent, but not read.
- // inboundLoop routine should not be leaked.
+ return recordlayer.MarshalRecord(header, payload)
}
-func TestReadWriteDeadline(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(5 * time.Second)
- defer lim.Stop()
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- var netErr net.Error
-
- ca, cb, err := pipeMemory()
- assert.NoError(t, err)
- assert.NoError(t, ca.SetDeadline(time.Unix(0, 1)))
+func TestMarshalRecordContentEnforcesPlaintextLimit(t *testing.T) {
+ _, plaintext, err := marshalRecordContent(&protocol.ApplicationData{
+ Data: make([]byte, maxPlaintextRecordLen),
+ })
+ require.NoError(t, err)
+ assert.Len(t, plaintext, maxPlaintextRecordLen)
- _, werr := ca.Write(make([]byte, 100))
- assert.ErrorAs(t, werr, &netErr, "Write must return net.Error")
- assert.True(t, netErr.Timeout(), "Deadline exceeded Write must return Timeout")
- assert.True(t, netErr.Temporary(), "Deadline exceeded Write must return Temporary") //nolint:staticcheck
+ _, _, err = marshalRecordContent(&protocol.ApplicationData{
+ Data: make([]byte, maxPlaintextRecordLen+1),
+ })
+ assert.ErrorIs(t, err, dtlserrors.ErrInvalidPacketLength)
+}
- _, rerr := ca.Read(make([]byte, 100))
- assert.ErrorAs(t, rerr, &netErr, "Read must return net.Error")
- assert.True(t, netErr.Timeout(), "Deadline exceeded Read must return Timeout")
- assert.True(t, netErr.Temporary(), "Deadline exceeded Read must return Temporary") //nolint:staticcheck
- assert.NoError(t, ca.SetDeadline(time.Time{}))
- assert.NoError(t, ca.Close())
- assert.NoError(t, cb.Close())
+func TestApplicationDataPacketOwnsPayload(t *testing.T) {
+ conn := &Conn{state: dtlsstate.NewActive(true)}
+ payload := []byte("application data")
+ packet := conn.newApplicationDataPacket(payload)
+ payload[0] = 'X'
- _, err = ca.Write(make([]byte, 100))
- assert.ErrorIs(t, err, ErrConnClosed)
- _, err = ca.Read(make([]byte, 100))
- assert.ErrorIs(t, err, io.EOF)
+ applicationData, ok := packet.Content.(*protocol.ApplicationData)
+ require.True(t, ok)
+ assert.Equal(t, []byte("application data"), applicationData.Data)
}
func TestSequenceNumberOverflow(t *testing.T) {
@@ -153,11 +106,13 @@ func TestSequenceNumberOverflow(t *testing.T) {
ca, cb, err := pipeMemory()
assert.NoError(t, err)
- atomic.StoreUint64(&ca.state.LocalSequenceNumber[1], recordlayer.MaxSequenceNumber)
- _, werr := ca.Write(make([]byte, 100))
+ dtlsstate.CommonState(ca.state).SetLocalSequenceNumber(1, recordlayer.MaxSequenceNumber)
+ n, werr := ca.Write(make([]byte, 100))
assert.NoError(t, werr, "Write must send message with maximum sequence number")
- _, werr = ca.Write(make([]byte, 100))
- assert.ErrorIs(t, werr, dtlserrors.ErrSequenceNumberOverflow, "Write must abandonsend message with maximum sequence number") //nolint:lll
+ assert.Equal(t, 100, n)
+ n, werr = ca.Write(make([]byte, 100))
+ assert.ErrorIs(t, werr, dtlserrors.ErrSequenceNumberOverflow, "Write must abandonsend message with maximum sequence number")
+ assert.Zero(t, n)
assert.NoError(t, ca.Close())
assert.NoError(t, cb.Close())
@@ -169,41 +124,48 @@ func TestSequenceNumberOverflow(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
- atomic.StoreUint64(&ca.state.LocalSequenceNumber[0], recordlayer.MaxSequenceNumber+1)
+ dtlsstate.CommonState(ca.state).SetLocalSequenceNumber(0, recordlayer.MaxSequenceNumber+1)
// Try to send handshake packet.
- werr := ca.writePackets(ctx, []*packet{
- {
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- Cookie: make([]byte, 64),
- CipherSuiteIDs: cipherSuiteIDs(defaultCipherSuites()),
- CompressionMethods: defaultCompressionMethods(),
- },
- },
- },
- },
- })
- assert.ErrorIs(t, werr, dtlserrors.ErrSequenceNumberOverflow,
- "Connection must fail when handshake packet reaches maximum sequence num")
+ werr := ca.writePackets(ctx, []*dtlsflight.Outbound{{Content: &handshake.Handshake{Message: &handshake.MessageClientHello{Version: protocol.Version1_2, Cookie: make([]byte, 64), CipherSuiteIDs: cipherSuiteIDs(defaultCipherSuites()), CompressionMethods: dtlsflight.DefaultCompressionMethods()}}}})
+ assert.ErrorIs(t, werr, dtlserrors.ErrSequenceNumberOverflow, "Connection must fail when handshake packet reaches maximum sequence num")
assert.NoError(t, ca.Close())
assert.NoError(t, cb.Close())
})
}
+type packetTestConn struct {
+ net.Conn
+ remoteAddr net.Addr
+}
+
+func (c *packetTestConn) ReadFrom(p []byte) (int, net.Addr, error) {
+ n, err := c.Conn.Read(p)
+
+ return n, c.remoteAddr, err
+}
+
+func (c *packetTestConn) WriteTo(p []byte, _ net.Addr) (int, error) {
+ return c.Conn.Write(p)
+}
+
+func (c *packetTestConn) RemoteAddr() net.Addr { return c.remoteAddr }
+
+func packetPipe() (*packetTestConn, *packetTestConn) {
+ a, b := dpipe.Pipe()
+
+ return &packetTestConn{Conn: a, remoteAddr: b.LocalAddr()},
+ &packetTestConn{Conn: b, remoteAddr: a.LocalAddr()}
+}
+
func pipeMemory() (*Conn, *Conn, error) {
// In memory pipe
- ca, cb := dpipe.Pipe()
+ ca, cb := packetPipe()
return pipeConn(ca, cb)
}
-func pipeConn(ca, cb net.Conn) (*Conn, *Conn, error) {
+func pipeConn(ca, cb net.PacketConn) (*Conn, *Conn, error) {
type result struct {
c *Conn
err error
@@ -215,16 +177,12 @@ func pipeConn(ca, cb net.Conn) (*Conn, *Conn, error) {
// Setup client
go func() {
- client, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), []ClientOption{
- WithSRTPProtectionProfiles(SRTP_AES128_CM_HMAC_SHA1_80),
- }, true)
+ client, err := testClient(ctx, ca, cb.LocalAddr(), []ClientOption{WithSRTPProtectionProfiles(SRTP_AES128_CM_HMAC_SHA1_80)}, true)
resultCh <- result{client, err}
}()
// Setup server
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), []ServerOption{
- WithSRTPProtectionProfiles(SRTP_AES128_CM_HMAC_SHA1_80),
- }, true)
+ server, err := testServer(ctx, cb, ca.LocalAddr(), []ServerOption{WithSRTPProtectionProfiles(SRTP_AES128_CM_HMAC_SHA1_80)}, true)
if err != nil {
// Read from resultCh to prevent goroutine leak
if res := <-resultCh; res.c != nil {
@@ -245,13 +203,7 @@ func pipeConn(ca, cb net.Conn) (*Conn, *Conn, error) {
return res.c, server, nil
}
-func testClient(
- ctx context.Context,
- pktConn net.PacketConn,
- rAddr net.Addr,
- opts []ClientOption,
- generateCertificate bool,
-) (*Conn, error) {
+func testClient(ctx context.Context, pktConn net.PacketConn, rAddr net.Addr, opts []ClientOption, generateCertificate bool) (*Conn, error) {
if generateCertificate {
clientCert, err := selfsign.GenerateSelfSigned()
if err != nil {
@@ -260,7 +212,7 @@ func testClient(
opts = append(opts, WithCertificates(clientCert))
}
opts = append(opts, WithInsecureSkipVerify(true))
- conn, err := ClientWithOptions(pktConn, rAddr, opts...)
+ conn, err := Client(pktConn, rAddr, opts...)
if err != nil {
return nil, err
}
@@ -268,13 +220,7 @@ func testClient(
return conn, conn.HandshakeContext(ctx)
}
-func testServer(
- ctx context.Context,
- c net.PacketConn,
- rAddr net.Addr,
- opts []ServerOption,
- generateCertificate bool,
-) (*Conn, error) {
+func testServer(ctx context.Context, c net.PacketConn, rAddr net.Addr, opts []ServerOption, generateCertificate bool) (*Conn, error) {
if generateCertificate {
serverCert, err := selfsign.GenerateSelfSigned()
if err != nil {
@@ -282,7 +228,7 @@ func testServer(
}
opts = append(opts, WithCertificates(serverCert))
}
- conn, err := ServerWithOptions(c, rAddr, opts...)
+ conn, err := Server(c, rAddr, opts...)
if err != nil {
return nil, err
}
@@ -290,36 +236,65 @@ func testServer(
return conn, conn.HandshakeContext(ctx)
}
-func sendClientHello(
- cookie []byte,
- ca net.Conn,
- sequenceNumber uint64,
- extensions []extension.Extension,
- cipherSuiteIDsOverride ...uint16,
-) error {
+type handshakeResult struct {
+ conn *Conn
+ configErr error
+ handshakeError error
+}
+
+func handshakePair(t *testing.T, clientOpts []ClientOption, serverOpts []ServerOption) (handshakeResult, handshakeResult) {
+ t.Helper()
+ ca, cb := packetPipe()
+ t.Cleanup(func() {
+ _ = ca.Close()
+ _ = cb.Close()
+ })
+ clientCh := make(chan handshakeResult)
+ go func() {
+ client, err := Client(ca, ca.RemoteAddr(), clientOpts...)
+ var handshakeErr error
+ if err == nil {
+ handshakeErr = client.Handshake()
+ }
+ clientCh <- handshakeResult{client, err, handshakeErr}
+ }()
+ server, err := Server(cb, cb.RemoteAddr(), serverOpts...)
+ var handshakeErr error
+ if err == nil {
+ handshakeErr = server.Handshake()
+ }
+ clientResult := <-clientCh
+ serverResult := handshakeResult{server, err, handshakeErr}
+ t.Cleanup(func() {
+ if clientResult.conn != nil {
+ _ = clientResult.conn.Close()
+ }
+ if serverResult.conn != nil {
+ _ = serverResult.conn.Close()
+ }
+ })
+
+ return clientResult, serverResult
+}
+
+//nolint:unused // Used by Go 1.25+ tests in sync_test.go.
+func sendClientHello(cookie []byte, ca net.Conn, sequenceNumber uint64, extensions []extension.Value, cipherSuiteIDsOverride ...uint16) error {
cipherSuites := cipherSuiteIDsOverride
if len(cipherSuites) == 0 {
cipherSuites = cipherSuiteIDs(defaultCipherSuites())
}
- packet, err := (&recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- SequenceNumber: sequenceNumber,
- },
- Content: &handshake.Handshake{
- Header: handshake.Header{
- MessageSequence: uint16(sequenceNumber), //nolint:gosec // G115
- },
- Message: &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- Cookie: cookie,
- CipherSuiteIDs: cipherSuites,
- CompressionMethods: defaultCompressionMethods(),
- Extensions: extensions,
- },
+ clientHello := handshake.MessageClientHello{Version: protocol.Version1_2, Cookie: cookie, CipherSuiteIDs: cipherSuites, CompressionMethods: dtlsflight.DefaultCompressionMethods(), Extensions: extensions}
+
+ packet, err := marshalTestRecord(recordlayer.RecordConfig{
+ Version: protocol.Version1_2,
+ SequenceNumber: sequenceNumber,
+ }, &handshake.Handshake{
+ Header: handshake.Header{
+ MessageSequence: uint16(sequenceNumber), //nolint:gosec // G115
},
- }).Marshal()
+ Message: &clientHello,
+ })
if err != nil {
return err
}
@@ -331,3332 +306,2375 @@ func sendClientHello(
return nil
}
-func TestHandshakeWithAlert(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
+func TestECDHEPSKHandshake(t *testing.T) {
+ for _, id := range []cryptosuite.ID{
+ cryptosuite.TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256,
+ cryptosuite.TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256,
+ } {
+ t.Run(id.String(), func(t *testing.T) {
+ client, server := handshakePair(t, []ClientOption{
+ WithCipherSuites(id),
+ WithPSK(func() ([]PSK, error) {
+ return []PSK{{Identity: []byte("client"), Key: []byte("key")}}, nil
+ }, nil),
+ }, []ServerOption{
+ WithCipherSuites(id),
+ WithPSK(nil, func(identities [][]byte) (*PSK, error) {
+ return &PSK{Identity: identities[0], Key: []byte("key")}, nil
+ }),
+ })
+ for _, result := range []handshakeResult{client, server} {
+ require.NoError(t, result.configErr)
+ require.NoError(t, result.handshakeError)
+ state, ok := result.conn.ConnectionState()
+ require.True(t, ok)
+ require.Equal(t, id, state.CipherSuiteID)
+ }
+ serverState, ok := server.conn.ConnectionState()
+ require.True(t, ok)
+ require.Equal(t, []byte("client"), serverState.IdentityHint)
+ exchangeResumeData(t, client.conn, server.conn)
+ })
+ }
+}
- cases := map[string]struct {
- serverOpts []ServerOption
- clientOpts []ClientOption
- errServer error
- errClient error
+func TestExportKeyingMaterial(t *testing.T) {
+ const exportLabel = "EXTRACTOR-dtls_srtp"
+
+ for _, tt := range []struct {
+ name string
+ version protocol.Version
+ cipherSuite cryptosuite.ID
+ expectedServerKey string
+ expectedClientKey string
+ contextErr error
+ labelErr error
}{
- "CipherSuiteNoIntersection": {
- serverOpts: []ServerOption{
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- },
- clientOpts: []ClientOption{
- WithCipherSuites(TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
- },
- errServer: dtlserrors.ErrCipherSuiteNoIntersection,
- errClient: &alertError{&alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}},
+ {
+ name: "DTLS12", version: protocol.Version1_2,
+ cipherSuite: cryptosuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ expectedServerKey: "61099d7dcb08522ce77b",
+ expectedClientKey: "87f04002f61cf1fe8c77",
+ contextErr: dtlserrors.ErrContextUnsupported,
+ labelErr: dtlserrors.ErrReservedExportKeyingMaterial,
},
- "SignatureSchemesNoIntersection": {
- serverOpts: []ServerOption{
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithSignatureSchemes(tls.ECDSAWithP256AndSHA256),
- },
- clientOpts: []ClientOption{
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithSignatureSchemes(tls.ECDSAWithP521AndSHA512),
- },
- errServer: &alertError{&alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}},
- errClient: dtlserrors.ErrNoAvailableSignatureSchemes,
+ {
+ name: "DTLS13", version: protocol.Version1_3,
+ cipherSuite: cryptosuite.TLS_AES_128_GCM_SHA256,
+ expectedServerKey: "31361451e3d8dc3ad264",
+ expectedClientKey: "31361451e3d8dc3ad264",
},
- }
-
- for name, testCase := range cases {
- t.Run(name, func(t *testing.T) {
- clientErr := make(chan error, 1)
+ } {
+ t.Run(tt.name, func(t *testing.T) {
+ common := &dtlsstate.Common{
+ LocalRandom: handshake.Random{GMTUnixTime: time.Unix(500, 0)},
+ RemoteRandom: handshake.Random{GMTUnixTime: time.Unix(1000, 0)},
+ LocalSequenceNumber: map[uint64]uint64{},
+ CipherSuite: ciphersuite.ForID(tt.cipherSuite),
+ }
+ conn := &Conn{state: &dtlsstate.State12{Common: common}}
+ if tt.version == protocol.Version1_3 {
+ conn.state = &dtlsstate.State13{
+ Common: common,
+ KeySchedule: dtlsstate.KeySchedule{ExporterMasterSecret: make([]byte, 32)},
+ }
+ }
- ca, cb := dpipe.Pipe()
- go func() {
- _, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), testCase.clientOpts, true)
- clientErr <- err
- }()
+ state, ok := conn.ConnectionState()
+ require.True(t, ok)
+ _, err := state.ExportKeyingMaterial(exportLabel, nil, 10)
+ assert.ErrorIs(t, err, dtlserrors.ErrHandshakeInProgress)
+
+ conn.setLocalEpoch(1)
+ for _, role := range []struct {
+ name string
+ isClient bool
+ expected string
+ }{
+ {name: "server", expected: tt.expectedServerKey},
+ {name: "client", isClient: true, expected: tt.expectedClientKey},
+ } {
+ t.Run(role.name, func(t *testing.T) {
+ common.IsClient = role.isClient
+ state, ok := conn.ConnectionState()
+ require.True(t, ok)
+
+ keyingMaterial, err := state.ExportKeyingMaterial(exportLabel, nil, 10)
+ require.NoError(t, err)
+ assert.Equal(t, role.expected, hex.EncodeToString(keyingMaterial))
+
+ otherMaterial, err := state.ExportKeyingMaterial("EXPORTER-other", nil, 10)
+ require.NoError(t, err)
+ assert.NotEqual(t, keyingMaterial, otherMaterial)
+
+ contextMaterial, err := state.ExportKeyingMaterial(exportLabel, []byte{0x00}, 10)
+ assert.ErrorIs(t, err, tt.contextErr)
+ if tt.contextErr == nil {
+ assert.Len(t, contextMaterial, 10)
+ assert.NotEqual(t, keyingMaterial, contextMaterial)
+ }
- _, errServer := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), testCase.serverOpts, true)
- assert.ErrorIs(t, errServer, testCase.errServer)
- assert.ErrorIs(t, <-clientErr, testCase.errClient)
+ for label := range invalidKeyingLabels() {
+ _, err := state.ExportKeyingMaterial(label, nil, 10)
+ assert.ErrorIs(t, err, tt.labelErr, label)
+ }
+ })
+ }
})
}
}
-func TestHandshakeWithInvalidRecord(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
+func TestShouldWrapConnectionIDUsesOnlyDTLS12(t *testing.T) {
+ state12 := dtlsstate.NewActive(false)
+ assert.False(t, state12.ShouldWrapConnectionID())
+ dtlsstate.CommonState(state12).RemoteConnectionID = []byte{0x01}
+ assert.True(t, state12.ShouldWrapConnectionID())
- type result struct {
- c *Conn
- err error
- }
- clientErr := make(chan result, 1)
- ca, cb := dpipe.Pipe()
- caWithInvalidRecord := &connWithCallback{Conn: ca}
+ state13 := dtlsstate.NewState13(false)
+ state13.RemoteConnectionID = []byte{0x01}
+ assert.False(t, state13.ShouldWrapConnectionID())
+}
- var msgSeq atomic.Int32
- // Send invalid record after first message
- caWithInvalidRecord.onWrite = func([]byte) {
- if msgSeq.Add(1) == 2 {
- _, err := ca.Write([]byte{0x01, 0x02})
- assert.NoError(t, err)
- }
- }
- go func() {
- client, err := testClient(
- ctx,
- dtlsnet.PacketConnFromConn(caWithInvalidRecord),
- caWithInvalidRecord.RemoteAddr(),
- []ClientOption{WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)},
- true,
- )
- clientErr <- result{client, err}
- }()
+func marshalVersionNegotiationHelloRetryRequestServerHello13(t *testing.T, cfg *dtlsconfig.HandshakeConfig, extensions []extension.Value) []byte {
+ t.Helper()
- server, errServer := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), []ServerOption{
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- }, true)
+ var hrrRandomFixed [handshake.RandomLength]byte
+ copy(hrrRandomFixed[:], handshake.HelloRetryRequestRandom())
+ var hrrRandom handshake.Random
+ hrrRandom.UnmarshalFixed(hrrRandomFixed)
- errClient := <-clientErr
+ return marshalVersionNegotiationServerHello13(t, cfg, hrrRandom, extensions)
+}
- defer func() {
- if server != nil {
- assert.NoError(t, server.Close())
- }
+func marshalVersionNegotiationServerHello13(t *testing.T, cfg *dtlsconfig.HandshakeConfig, random handshake.Random, extensions []extension.Value) []byte {
+ t.Helper()
- if errClient.c != nil {
- assert.NoError(t, errClient.c.Close())
- }
- }()
+ cipherSuiteID := uint16(cfg.LocalCipherSuites[0].ID())
+ serverHello := &handshake.MessageServerHello{Version: protocol.Version1_2, Random: random, CipherSuiteID: &cipherSuiteID, CompressionMethod: dtlsflight.DefaultCompressionMethods()[0], Extensions: extensions}
+ rawServerHello, err := (&handshake.Handshake{Message: serverHello}).Marshal()
+ assert.NoError(t, err)
- assert.NoError(t, errServer)
- assert.NoError(t, errClient.err)
+ return rawServerHello
}
-func TestExportKeyingMaterial(t *testing.T) {
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
+func testVersionNegotiationHandshakeConfig13(t *testing.T) *dtlsconfig.HandshakeConfig {
+ t.Helper()
- var rand [28]byte
- exportLabel := "EXTRACTOR-dtls_srtp"
+ cipherSuites, err := selectCipherSuites(
+ nil,
+ nil,
+ true,
+ false,
+ protocol.Version1_3,
+ protocol.Version1_3,
+ )
+ assert.NoError(t, err)
- expectedServerKey := []byte{0x61, 0x09, 0x9d, 0x7d, 0xcb, 0x08, 0x52, 0x2c, 0xe7, 0x7b}
- expectedClientKey := []byte{0x87, 0xf0, 0x40, 0x02, 0xf6, 0x1c, 0xf1, 0xfe, 0x8c, 0x77}
+ loggerFactory := logging.NewDefaultLoggerFactory()
+
+ return &dtlsconfig.HandshakeConfig{
+ LocalCipherSuites: cipherSuites,
+ EllipticCurves: defaultCurves,
+ InitialRetransmitInterval: time.Second,
+ ExtendedMasterSecret: dtlsconfig.ExtendedMasterSecretType(RequestExtendedMasterSecret),
+ Log: loggerFactory.NewLogger("dtls"),
+ MinVersion: protocol.Version1_3,
+ MaxVersion: protocol.Version1_3,
+ LocalSignatureSchemes: signaturehash.Algorithms(),
+ LocalCertSignatureSchemes: nil,
+ LocalSRTPProtectionProfiles: nil,
+ }
+}
+func TestPickVersionFromServerHelloRejectsUnsolicitedExtension(t *testing.T) {
+ const offeredType extension.Type = 0xfefe
+ extensions := []extension.Value{
+ extension.Raw{Type: offeredType, Data: []byte{0x01}},
+ }
+ clientHello := handshake.MessageClientHello{Version: protocol.Version1_2, CipherSuiteIDs: []uint16{0x1301}, CompressionMethods: []*protocol.CompressionMethod{{}}, Extensions: extensions}
+ _, offer, err := negotiation.FinalizeClientHello(&clientHello, nil)
+ require.NoError(t, err)
+
+ cfg := testVersionNegotiationHandshakeConfig13(t)
+ cfg.MinVersion = protocol.Version1_2
+ common := &dtlsstate.Common{}
+ require.NoError(t, common.LocalClientHelloSnapshots.Record(offer))
conn := &Conn{
- state: dtlsstate.State{
- LocalRandom: handshake.Random{GMTUnixTime: time.Unix(500, 0), RandomBytes: rand},
- RemoteRandom: handshake.Random{GMTUnixTime: time.Unix(1000, 0), RandomBytes: rand},
- LocalSequenceNumber: []uint64{0, 0},
- CipherSuite: &ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256{},
- },
+ handshakeConfig: cfg,
+ state: &dtlsstate.State13{Common: common},
}
- conn.setLocalEpoch(0)
- conn.setRemoteEpoch(0)
+ extensions = []extension.Value{
+ extension.Raw{Type: 0xfefd, Data: []byte{0x02}},
+ }
+ serverHello := handshake.MessageServerHello{
+ Version: protocol.Version1_2,
+ Extensions: extensions,
+ }
+ err = conn.pickVersionFromServerHello(&serverHello)
- state, ok := conn.ConnectionState()
- assert.True(t, ok)
+ require.ErrorIs(t, err, dtlserrors.ErrUnsolicitedExtension)
+ var classified *alert.Alert
+ require.ErrorAs(t, err, &classified)
+ assert.Equal(t, alert.UnsupportedExtension, classified.Description)
+ assert.Equal(t, protocol.Version(0), common.LocalVersion)
+}
- _, err := state.ExportKeyingMaterial(exportLabel, nil, 0)
- assert.ErrorIs(t, err, dtlserrors.ErrHandshakeInProgress, "ExportKeyingMaterial when epoch == 0 error mismatch")
+func TestPickVersionFromServerResponseRejectsHelloRetryRequestWithoutSupportedVersions(t *testing.T) {
+ cfg := testVersionNegotiationHandshakeConfig13(t)
+ cfg.MinVersion = protocol.Version1_2
+ cfg.MaxVersion = protocol.Version1_3
+ selectedGroup := elliptic.P384
- conn.setLocalEpoch(1)
- state, ok = conn.ConnectionState()
- assert.True(t, ok)
+ rawServerHello := marshalVersionNegotiationHelloRetryRequestServerHello13(t, cfg, []extension.Value{&extension13.RetryKeyShare{SelectedGroup: selectedGroup}})
- _, err = state.ExportKeyingMaterial(exportLabel, []byte{0x00}, 0)
- assert.ErrorIs(t, err, dtlserrors.ErrContextUnsupported, "ExportKeyingMaterial with context mismatch")
+ conn := &Conn{
+ handshakeCache: dtlsflight.NewCache(),
+ handshakeConfig: cfg,
+ }
+ conn.handshakeCache.Push(rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
- for k := range invalidKeyingLabels() {
- state, ok = conn.ConnectionState()
- assert.True(t, ok)
+ ok, err := conn.pickVersionFromServerResponse()
- _, err = state.ExportKeyingMaterial(k, nil, 0)
- assert.ErrorIs(t, err, dtlserrors.ErrReservedExportKeyingMaterial, "ExportKeyingMaterial reserved label mismatch")
- }
+ assert.ErrorIs(t, err, dtlserrors.ErrMissingSupportedVersionsExtension)
+ var classified *alert.Alert
+ require.ErrorAs(t, err, &classified)
+ assert.Equal(t, alert.MissingExtension, classified.Description)
+ assert.False(t, ok)
+ assert.Equal(t, protocol.Version(0), dtlsstate.CommonState(conn.state).LocalVersion)
+}
- state, ok = conn.ConnectionState()
- assert.True(t, ok)
+func TestPickVersionFromServerResponseRejectsUnexpectedMessage(t *testing.T) {
+ cfg := testVersionNegotiationHandshakeConfig13(t)
+ cfg.MinVersion = protocol.Version1_2
+ cfg.MaxVersion = protocol.Version1_3
+ raw, err := (&handshake.Handshake{
+ Message: &handshake.MessageFinished{VerifyData: []byte{0x01}},
+ }).Marshal()
+ require.NoError(t, err)
- keyingMaterial, err := state.ExportKeyingMaterial(exportLabel, nil, 10)
- assert.NoError(t, err, "ExportingKeyingMaterial as server error")
- assert.Equal(t, expectedServerKey, keyingMaterial, "ExportKeyingMaterial client export mismatch")
+ conn := &Conn{
+ handshakeCache: dtlsflight.NewCache(),
+ handshakeConfig: cfg,
+ }
+ conn.handshakeCache.Push(raw, cfg.InitialEpoch, 0, handshake.TypeFinished, false)
- conn.state.IsClient = true
- state, ok = conn.ConnectionState()
- assert.True(t, ok)
+ ok, err := conn.pickVersionFromServerResponse()
- keyingMaterial, err = state.ExportKeyingMaterial(exportLabel, nil, 10)
- assert.NoError(t, err)
- assert.Equal(t, expectedClientKey, keyingMaterial, "ExportKeyingMaterial client report mismatch")
+ assert.ErrorIs(t, err, dtlserrors.ErrUnexpectedHandshakeMessage)
+ var classified *alert.Alert
+ require.ErrorAs(t, err, &classified)
+ assert.Equal(t, alert.UnexpectedMessage, classified.Description)
+ assert.False(t, ok)
}
-func TestPSK(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
+func TestPickVersionFromServerResponsePreservesDecodeAlert(t *testing.T) {
+ cfg := testVersionNegotiationHandshakeConfig13(t)
+ cfg.MinVersion = protocol.Version1_2
+ cfg.MaxVersion = protocol.Version1_3
+ conn := &Conn{
+ handshakeCache: dtlsflight.NewCache(),
+ handshakeConfig: cfg,
+ }
+ conn.handshakeCache.Push([]byte{byte(handshake.TypeServerHello)}, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
+ ok, err := conn.pickVersionFromServerResponse()
- for _, test := range []struct {
- Name string
- ClientIdentity []byte
- ServerIdentity []byte
- cipherSuites []CipherSuiteID
- ClientVerifyConnection func(*State) error
- ServerVerifyConnection func(*State) error
- WantFail bool
- ExpectedServerErr string
- ExpectedClientErr string
- }{
- {
- Name: "Server identity specified",
- ServerIdentity: []byte("Test Identity"),
- ClientIdentity: []byte("Client Identity"),
- cipherSuites: []CipherSuiteID{TLS_PSK_WITH_AES_128_CCM_8},
- },
- {
- Name: "Server identity specified - Server verify connection fails",
- ServerIdentity: []byte("Test Identity"),
- ClientIdentity: []byte("Client Identity"),
- cipherSuites: []CipherSuiteID{TLS_PSK_WITH_AES_128_CCM_8},
- ServerVerifyConnection: func(*State) error {
- return errExample
- },
- WantFail: true,
- ExpectedServerErr: errExample.Error(),
- ExpectedClientErr: alert.BadCertificate.String(),
- },
- {
- Name: "Server identity specified - Client verify connection fails",
- ServerIdentity: []byte("Test Identity"),
- ClientIdentity: []byte("Client Identity"),
- cipherSuites: []CipherSuiteID{TLS_PSK_WITH_AES_128_CCM_8},
- ClientVerifyConnection: func(*State) error {
- return errExample
+ assert.ErrorIs(t, err, dtlserrors.ErrBufferTooSmall)
+ var classified *alert.Alert
+ require.ErrorAs(t, err, &classified)
+ assert.Equal(t, alert.DecodeError, classified.Description)
+ assert.False(t, ok)
+}
+
+func TestPickVersionFromServerResponseRejectsServerHelloWithClientHelloSupportedVersionsEncoding(t *testing.T) {
+ cfg := testVersionNegotiationHandshakeConfig13(t)
+ cfg.MinVersion = protocol.Version1_2
+ cfg.MaxVersion = protocol.Version1_3
+ random := handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}}
+
+ rawServerHello := marshalVersionNegotiationServerHello13(
+ t,
+ cfg,
+ random,
+ []extension.Value{
+ extension.Raw{
+ Type: extension.TypeSupportedVersions,
+ Data: []byte{
+ 0x02, // ClientHello vector length
+ 0xfe, 0xfc, // DTLS v1.3
+ },
},
- WantFail: true,
- ExpectedServerErr: alert.BadCertificate.String(),
- ExpectedClientErr: errExample.Error(),
- },
- {
- Name: "Server identity nil",
- ServerIdentity: nil,
- ClientIdentity: []byte("Client Identity"),
- cipherSuites: []CipherSuiteID{TLS_PSK_WITH_AES_128_CCM_8},
- },
- {
- Name: "TLS_PSK_WITH_AES_128_CBC_SHA256",
- ServerIdentity: nil,
- ClientIdentity: []byte("Client Identity"),
- cipherSuites: []CipherSuiteID{TLS_PSK_WITH_AES_128_CBC_SHA256},
- },
- {
- Name: "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256",
- ServerIdentity: nil,
- ClientIdentity: []byte("Client Identity"),
- cipherSuites: []CipherSuiteID{TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256},
- },
- {
- Name: "Client identity empty",
- ServerIdentity: nil,
- ClientIdentity: []byte{},
- cipherSuites: []CipherSuiteID{TLS_PSK_WITH_AES_128_CCM_8},
},
- } {
- t.Run(test.Name, func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
+ )
- type result struct {
- c *Conn
- err error
- }
- clientRes := make(chan result, 1)
+ parsed := &handshake.Handshake{}
+ err := parsed.Unmarshal(rawServerHello)
+ assert.ErrorIs(t, err, dtlserrors.ErrInvalidSupportedVersionsFormat)
+}
- ca, cb := dpipe.Pipe()
- go func() {
- clientOpts := []ClientOption{
- WithPSK(func(hint []byte) ([]byte, error) {
- if !bytes.Equal(test.ServerIdentity, hint) {
- return nil, fmt.Errorf(
- "%w expected(% 02x) actual(% 02x)",
- errTestPSKClientInvalidIdentity,
- test.ServerIdentity, hint,
- )
- }
-
- return []byte{0xAB, 0xC1, 0x23}, nil
- }),
- WithPSKIdentityHint(test.ClientIdentity),
- WithCipherSuites(test.cipherSuites...),
- }
- if test.ClientVerifyConnection != nil {
- clientOpts = append(clientOpts, WithVerifyConnection(test.ClientVerifyConnection))
- }
+func TestSelectRemoteVersionActivatesChosenState(t *testing.T) {
+ cfg := testVersionNegotiationHandshakeConfig13(t)
+ cfg.MinVersion = protocol.Version1_2
+ cfg.MaxVersion = protocol.Version1_3
- c, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), clientOpts, false)
- clientRes <- result{c, err}
- }()
+ commonState := &dtlsstate.Common{}
+ conn := &Conn{
+ handshakeConfig: cfg,
+ state: &dtlsstate.State12{
+ Common: commonState,
+ HandshakeSendSequence: 3,
+ },
+ }
+ err := conn.selectRemoteVersion([]protocol.Version{protocol.Version1_3})
+ assert.NoError(t, err)
+ state13, ok := conn.state.(*dtlsstate.State13)
+ assert.True(t, ok)
+ assert.Equal(t, 3, state13.HandshakeSendSequence)
+ assert.NotNil(t, state13.LocalKeypairs)
+ assert.Equal(t, protocol.Version1_3, dtlsstate.CommonState(conn.state).LocalVersion)
- serverOpts := []ServerOption{
- WithPSK(func(hint []byte) ([]byte, error) {
- t.Log(hint)
- if !bytes.Equal(test.ClientIdentity, hint) {
- return nil, fmt.Errorf("%w: expected(% 02x) actual(% 02x)", errTestPSKInvalidIdentity, test.ClientIdentity, hint)
- }
+ err = conn.selectRemoteVersion([]protocol.Version{protocol.Version1_2})
+ assert.NoError(t, err)
+ state12, ok := conn.state.(*dtlsstate.State12)
+ assert.True(t, ok)
+ assert.Equal(t, 3, state12.HandshakeSendSequence)
+ assert.Equal(t, protocol.Version1_2, dtlsstate.CommonState(conn.state).LocalVersion)
+ commonState.IsClient = true
+ cfg.ECHConfigList = []byte{1}
+ err = conn.selectRemoteVersion([]protocol.Version{protocol.Version1_2})
+ require.ErrorIs(t, err, dtlserrors.ErrUnsupportedProtocolVersion)
+ var failure *alert.Alert
+ require.ErrorAs(t, err, &failure)
+ require.Equal(t, alert.ProtocolVersion, failure.Description)
+}
- return []byte{0xAB, 0xC1, 0x23}, nil
- }),
- WithCipherSuites(test.cipherSuites...),
- }
- if test.ServerIdentity != nil {
- serverOpts = append(serverOpts, WithPSKIdentityHint(test.ServerIdentity))
- }
- if test.ServerVerifyConnection != nil {
- serverOpts = append(serverOpts, WithVerifyConnection(test.ServerVerifyConnection))
- }
+type connWithCallback struct {
+ *packetTestConn
+ onWrite func([]byte)
+}
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), serverOpts, false)
- if test.WantFail {
- res := <-clientRes
- assert.Error(t, err)
- assert.True(t, strings.Contains(err.Error(), test.ExpectedServerErr), "TestPSK: Server expected error mismatch")
- assert.Error(t, res.err, "TestPSK: Client expected error mismatch")
- assert.True(t, strings.Contains(res.err.Error(), test.ExpectedClientErr),
- "TestPSK: Client expeected error mismatch")
+func (c *connWithCallback) WriteTo(b []byte, addr net.Addr) (int, error) {
+ if c.onWrite != nil {
+ c.onWrite(b)
+ }
- return
- }
- assert.NoError(t, err)
+ return c.packetTestConn.WriteTo(b, addr)
+}
- state, ok := server.ConnectionState()
- assert.True(t, ok, "TestPSK: Server ConnectionState failed")
+func TestPacketQueueWriterCopiesExactRecord(t *testing.T) {
+ readBuffer := []byte{1, 2, 3}
+ conn := &Conn{}
+ lease := readBufferLease{
+ conn: conn,
+ recyclableReadBuffer: &readBuffer,
+ datagramContainsCID: true,
+ }
- actualPSKIdentityHint := state.IdentityHint
- assert.Equal(t, test.ClientIdentity, actualPSKIdentityHint, "TestPSK: Server ClientPSKIdentity Mismatch")
+ require.True(t, lease.enqueue(addrPkt{data: readBuffer}))
+ assert.Same(t, &readBuffer, lease.recyclableReadBuffer)
+ require.Len(t, conn.encryptedPackets, 1)
+ assert.NotSame(t, &readBuffer[0], &conn.encryptedPackets[0].data[0])
+ assert.Equal(t, len(conn.encryptedPackets[0].data), cap(conn.encryptedPackets[0].data))
+ assert.True(t, conn.encryptedPackets[0].datagramContainsCID)
+ readBuffer[0] = 9
+ assert.Equal(t, []byte{1, 2, 3}, conn.encryptedPackets[0].data)
+
+ rejectedReadBuffer := []byte{4, 5, 6}
+ fullConn := &Conn{encryptedPackets: make([]addrPkt, maxAppDataPacketQueueSize)}
+ rejectedLease := readBufferLease{conn: fullConn, recyclableReadBuffer: &rejectedReadBuffer}
+ assert.False(t, rejectedLease.enqueue(addrPkt{data: rejectedReadBuffer}))
+ assert.Same(t, &rejectedReadBuffer, rejectedLease.recyclableReadBuffer)
+}
- defer func() {
- _ = server.Close()
- }()
+func TestReadAndBufferNoFSMQueuesExactRecordCopy(t *testing.T) {
+ ca, cb := packetPipe()
+ defer func() {
+ assert.NoError(t, ca.Close())
+ assert.NoError(t, cb.Close())
+ }()
- res := <-clientRes
- assert.NoError(t, res.err)
- assert.NoError(t, res.c.Close())
- })
+ conn := &Conn{
+ nextConn: netctx.NewPacketConn(cb),
+ readDeadline: deadline.New(),
+ handshakeEstablished: dtlshandshake.NewEstablishment(),
+ fragmentBuffer: dtlsfragmentbuffer.New(),
+ handshakeCache: dtlsflight.NewCache(),
+ readBufferPool: readBufferPoolForSize(defaultReceiveBufferSize),
+ log: logging.NewDefaultLoggerFactory().NewLogger("dtls"),
+ state: &dtlsstate.State13{Common: &dtlsstate.Common{
+ LocalVersion: protocol.Version1_3,
+ }},
}
+ rawPacket, err := recordlayer.MarshalCiphertext(recordlayer.CiphertextConfig{EpochLow: uint8(dtlsflight13.EpochHandshake), SequenceNumber: 1, TwoByteSequence: true, LengthPresent: true}, bytes.Repeat([]byte{0xa5}, 16))
+ require.NoError(t, err)
+
+ writeResult := make(chan error, 1)
+ go func() {
+ _, writeErr := ca.Write(rawPacket)
+ writeResult <- writeErr
+ }()
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+ require.NoError(t, conn.readAndBufferNoFSM(ctx))
+ require.NoError(t, <-writeResult)
+ require.Len(t, conn.encryptedPackets, 1)
+ assert.Equal(t, rawPacket, conn.encryptedPackets[0].data)
+ assert.Equal(t, len(rawPacket), cap(conn.encryptedPackets[0].data))
}
-func TestPSKHintFail(t *testing.T) {
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
+func TestHandleIncomingPacket13RejectsFixedHandshakeEpoch(t *testing.T) {
+ commonState := &dtlsstate.Common{IsClient: true, LocalVersion: protocol.Version1_3}
+ conn := &Conn{
+ fragmentBuffer: dtlsfragmentbuffer.New(),
+ handshakeCache: dtlsflight.NewCache(),
+ log: logging.NewDefaultLoggerFactory().NewLogger("dtls"),
+ replayProtectionWindow: defaultReplayProtectionWindow,
+ handshakeConfig: testVersionNegotiationHandshakeConfig13(t),
+ state: &dtlsstate.State13{Common: commonState},
+ }
+ conn.setRemoteEpoch(0)
- serverAlertError := &alertError{&alert.Alert{Level: alert.Fatal, Description: alert.InternalError}}
- pskRejected := errPSKRejected
+ rawPacket, err := marshalTestRecord(recordlayer.RecordConfig{Version: protocol.Version1_2, Epoch: uint16(dtlsflight13.EpochHandshake), SequenceNumber: 0}, &handshake.Handshake{Header: handshake.Header{MessageSequence: 1}, Message: &handshake.MessageEncryptedExtensions{}})
+ assert.NoError(t, err)
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- clientErr := make(chan error, 1)
-
- ca, cb := dpipe.Pipe()
- go func() {
- opts := []ClientOption{
- WithPSK(func([]byte) ([]byte, error) {
- return nil, pskRejected
- }),
- WithPSKIdentityHint([]byte{}),
- WithCipherSuites(TLS_PSK_WITH_AES_128_CCM_8),
- }
-
- _, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), opts, false)
- clientErr <- err
- }()
-
- opts := []ServerOption{
- WithPSK(func([]byte) ([]byte, error) {
- return nil, pskRejected
- }),
- WithPSKIdentityHint([]byte{}),
- WithCipherSuites(TLS_PSK_WITH_AES_128_CCM_8),
- }
-
- _, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), opts, false)
- assert.ErrorIs(t, err, serverAlertError, "TestPSK: Server should fail with alert error")
- assert.ErrorIs(t, <-clientErr, pskRejected, "TestPSK: Client should fail with pskRejected error")
-}
-
-func TestPSKMismatchNoRetransmitLoop(t *testing.T) {
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- var serverWrites atomic.Int32
- var clientWrites atomic.Int32
-
- ca, cb := dpipe.Pipe()
- defer func() {
- _ = ca.Close()
- }()
- defer func() {
- _ = cb.Close()
- }()
-
- caCount := &connWithCallback{Conn: ca}
- caCount.onWrite = func([]byte) {
- clientWrites.Add(1)
- }
- cbCount := &connWithCallback{Conn: cb}
- cbCount.onWrite = func([]byte) {
- serverWrites.Add(1)
- }
-
- clientErr := make(chan error, 1)
- serverErr := make(chan error, 1)
-
- go func() {
- opts := []ClientOption{
- WithPSK(func([]byte) ([]byte, error) {
- return []byte("client-psk"), nil
- }),
- WithPSKIdentityHint([]byte("Client Identity")),
- WithCipherSuites(TLS_PSK_WITH_AES_128_CCM_8),
- }
-
- c, err := testClient(ctx, dtlsnet.PacketConnFromConn(caCount), caCount.RemoteAddr(), opts, false)
- if c != nil {
- _ = c.Close() //nolint:contextcheck
- }
- clientErr <- err
- }()
-
- go func() {
- opts := []ServerOption{
- WithPSK(func([]byte) ([]byte, error) {
- return []byte("server-psk"), nil
- }),
- WithCipherSuites(TLS_PSK_WITH_AES_128_CCM_8),
- }
-
- s, err := testServer(ctx, dtlsnet.PacketConnFromConn(cbCount), cbCount.RemoteAddr(), opts, false)
- if s != nil {
- _ = s.Close() //nolint:contextcheck
- }
- serverErr <- err
- }()
-
- serverErrRes := <-serverErr
- clientErrRes := <-clientErr
-
- assert.ErrorContains(t, serverErrRes, "handshake failed")
- assert.ErrorContains(t, clientErrRes, "handshake failed")
-
- serverCount := serverWrites.Load()
- clientCount := clientWrites.Load()
-
- time.Sleep(2 * time.Second)
-
- assert.Equal(t, serverCount, serverWrites.Load(), "Server should not retransmit after handshake failure")
- assert.Equal(t, clientCount, clientWrites.Load(), "Client should not retransmit after handshake failure")
- assert.LessOrEqual(t, serverCount, int32(20), "Server retransmit count too high for backoff")
- assert.LessOrEqual(t, clientCount, int32(20), "Client retransmit count too high for backoff")
-}
-
-// Assert that ServerKeyExchange is only sent if Identity is set on server side.
-func TestPSKServerKeyExchange(t *testing.T) { //nolint:cyclop
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- for _, test := range []struct {
- Name string
- SetIdentity bool
- }{
- {
- Name: "Server Identity Set",
- SetIdentity: true,
- },
- {
- Name: "Server Not Identity Set",
- SetIdentity: false,
- },
- } {
- testCase := test
- t.Run(testCase.Name, func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- var gotServerKeyExchange atomic.Bool
- expectedServerKeyExchange := testCase.SetIdentity
-
- clientErr := make(chan error, 1)
- ca, cb := dpipe.Pipe()
- cbAnalyzer := &connWithCallback{Conn: cb}
- cbAnalyzer.onWrite = func(in []byte) {
- messages, err := recordlayer.UnpackDatagram(in)
- assert.NoError(t, err)
-
- for i := range messages {
- var header recordlayer.Header
- if err := header.Unmarshal(messages[i]); err != nil {
- continue
- }
- if header.ContentType != protocol.ContentTypeHandshake || header.Epoch != 0 {
- continue
- }
- payload := messages[i][recordlayer.FixedHeaderSize:]
- for len(payload) >= handshake.HeaderLength {
- var h handshake.Header
- if err := h.Unmarshal(payload); err != nil {
- break
- }
- if h.Type == handshake.TypeServerKeyExchange {
- gotServerKeyExchange.Store(true)
-
- break
- }
- fragLen := int(h.FragmentLength)
- if fragLen <= 0 || handshake.HeaderLength+fragLen > len(payload) {
- break
- }
- payload = payload[handshake.HeaderLength+fragLen:]
- }
- }
- }
-
- go func() {
- opts := []ClientOption{
- WithPSK(func([]byte) ([]byte, error) {
- return []byte{0xAB, 0xC1, 0x23}, nil
- }),
- WithPSKIdentityHint([]byte{0xAB, 0xC1, 0x23}),
- WithCipherSuites(TLS_PSK_WITH_AES_128_CCM_8),
- }
-
- if client, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), opts, false); err != nil {
- clientErr <- err
- } else {
- clientErr <- client.Close() //nolint
- }
- }()
-
- opts := []ServerOption{
- WithPSK(func([]byte) ([]byte, error) {
- return []byte{0xAB, 0xC1, 0x23}, nil
- }),
- WithCipherSuites(TLS_PSK_WITH_AES_128_CCM_8),
- }
- if testCase.SetIdentity {
- opts = append(opts, WithPSKIdentityHint([]byte{0xAB, 0xC1, 0x23}))
- }
-
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cbAnalyzer), cbAnalyzer.RemoteAddr(), opts, false)
- assert.NoError(t, err)
-
- // Read the value immediately after handshake completes, before closing
- receivedServerKeyExchange := gotServerKeyExchange.Load()
-
- assert.NoError(t, server.Close())
- assert.NoError(t, <-clientErr, "TestPSK: Client erro")
-
- assert.Equal(t, expectedServerKeyExchange, receivedServerKeyExchange)
- })
- }
-}
-
-func TestClientTimeout(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), time.Second)
- defer cancel()
-
- clientErr := make(chan error, 1)
-
- ca, _ := dpipe.Pipe()
- go func() {
- c, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), nil, true)
- if err == nil {
- _ = c.Close() //nolint:contextcheck
- }
- clientErr <- err
- }()
-
- // no server!
- err := <-clientErr
- var netErr net.Error
- assert.ErrorAs(t, err, &netErr, "Client error exp(Temporary network error) failed")
- assert.True(t, netErr.Timeout(), "Client error exp(Timeout) failed")
-}
-
-func TestSRTPConfiguration(t *testing.T) {
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- for _, test := range []struct {
- Name string
- ClientSRTP []SRTPProtectionProfile
- ServerSRTP []SRTPProtectionProfile
- ClientSRTPMasterKeyIdentifier []byte
- ServerSRTPMasterKeyIdentifier []byte
- ExpectedProfile SRTPProtectionProfile
- WantClientError error
- WantServerError error
- }{
- {
- Name: "No SRTP in use",
- ClientSRTP: nil,
- ServerSRTP: nil,
- ExpectedProfile: 0,
- WantClientError: nil,
- WantServerError: nil,
- },
- {
- Name: "SRTP both ends",
- ClientSRTP: []SRTPProtectionProfile{SRTP_AES128_CM_HMAC_SHA1_80},
- ServerSRTP: []SRTPProtectionProfile{SRTP_AES128_CM_HMAC_SHA1_80},
- ExpectedProfile: SRTP_AES128_CM_HMAC_SHA1_80,
- ClientSRTPMasterKeyIdentifier: []byte("ClientSRTPMKI"),
- ServerSRTPMasterKeyIdentifier: []byte("ServerSRTPMKI"),
- WantClientError: nil,
- WantServerError: nil,
- },
- {
- Name: "SRTP client only",
- ClientSRTP: []SRTPProtectionProfile{SRTP_AES128_CM_HMAC_SHA1_80},
- ServerSRTP: nil,
- ExpectedProfile: 0,
- WantClientError: &alertError{&alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}},
- WantServerError: dtlserrors.ErrServerNoMatchingSRTPProfile,
- },
- {
- Name: "SRTP server only",
- ClientSRTP: nil,
- ServerSRTP: []SRTPProtectionProfile{SRTP_AES128_CM_HMAC_SHA1_80},
- ExpectedProfile: 0,
- WantClientError: nil,
- WantServerError: nil,
- },
- {
- Name: "Multiple Suites",
- ClientSRTP: []SRTPProtectionProfile{SRTP_AES128_CM_HMAC_SHA1_80, SRTP_AES128_CM_HMAC_SHA1_32},
- ServerSRTP: []SRTPProtectionProfile{SRTP_AES128_CM_HMAC_SHA1_80, SRTP_AES128_CM_HMAC_SHA1_32},
- ExpectedProfile: SRTP_AES128_CM_HMAC_SHA1_80,
- WantClientError: nil,
- WantServerError: nil,
- },
- {
- Name: "Multiple Suites, Server Chooses",
- ClientSRTP: []SRTPProtectionProfile{SRTP_AES128_CM_HMAC_SHA1_80, SRTP_AES128_CM_HMAC_SHA1_32},
- ServerSRTP: []SRTPProtectionProfile{SRTP_AES128_CM_HMAC_SHA1_32, SRTP_AES128_CM_HMAC_SHA1_80},
- ExpectedProfile: SRTP_AES128_CM_HMAC_SHA1_32,
- WantClientError: nil,
- WantServerError: nil,
- },
- } {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- ca, cb := dpipe.Pipe()
- type result struct {
- c *Conn
- err error
- }
- resultCh := make(chan result)
-
- go func() {
- opts := []ClientOption{WithSRTPMasterKeyIdentifier(test.ServerSRTPMasterKeyIdentifier)}
- if len(test.ClientSRTP) > 0 {
- opts = append(opts, WithSRTPProtectionProfiles(test.ClientSRTP...))
- }
- client, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), opts, true)
- resultCh <- result{client, err}
- }()
-
- opts := []ServerOption{WithSRTPMasterKeyIdentifier(test.ClientSRTPMasterKeyIdentifier)}
- if len(test.ServerSRTP) > 0 {
- opts = append(opts, WithSRTPProtectionProfiles(test.ServerSRTP...))
- }
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), opts, true)
- assert.ErrorIs(t, err, test.WantServerError, "TestSRTPConfiguration: Server Error Mismatch")
-
- if err == nil {
- defer func() {
- _ = server.Close()
- }()
- }
-
- res := <-resultCh
- if res.err == nil {
- defer func() {
- _ = res.c.Close()
- }()
- }
- assert.ErrorIsf(t, res.err, test.WantClientError, "TestSRTPConfiguration: Client Error Mismatch '%s'", test.Name)
- if res.c == nil {
- return
- }
-
- actualClientSRTP, _ := res.c.SelectedSRTPProtectionProfile()
- assert.Equalf(t, test.ExpectedProfile, actualClientSRTP,
- "TestSRTPConfiguration: Client SRTPProtectionProfile Mismatch '%s'", test.Name)
-
- actualServerSRTP, _ := server.SelectedSRTPProtectionProfile()
- assert.Equalf(t, test.ExpectedProfile, actualServerSRTP,
- "TestSRTPConfiguration: Server SRTPProtectionProfile Mismatch '%s'", test.Name)
-
- actualServerMKI, _ := server.RemoteSRTPMasterKeyIdentifier()
- assert.Truef(t, bytes.Equal(test.ServerSRTPMasterKeyIdentifier, actualServerMKI),
- "TestSRTPConfiguration: Server SRTPMKI Mismatch '%s'", test.Name)
-
- actualClientMKI, _ := res.c.RemoteSRTPMasterKeyIdentifier()
- assert.Truef(t, bytes.Equal(test.ClientSRTPMasterKeyIdentifier, actualClientMKI),
- "TestSRTPConfiguration: Client SRTPMKI Mismatch '%s'", test.Name)
- }
-}
-
-func TestClientCertificate(t *testing.T) { //nolint:gocyclo,cyclop,maintidx
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- srvCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- srvCAPool := x509.NewCertPool()
- srvCertificate, err := x509.ParseCertificate(srvCert.Certificate[0])
- assert.NoError(t, err)
-
- srvCAPool.AddCert(srvCertificate)
-
- cert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- certificate, err := x509.ParseCertificate(cert.Certificate[0])
- assert.NoError(t, err)
-
- caPool := x509.NewCertPool()
- caPool.AddCert(certificate)
-
- t.Run("parallel", func(t *testing.T) { // sync routines to check routine leak
- tests := map[string]struct {
- clientOpts []ClientOption
- serverOpts []ServerOption
- clientAuth ClientAuthType
- expectedClientCert [][]byte
- expectedServerCert [][]byte
- wantErr bool
- }{
- "NoClientCert": {
- clientOpts: []ClientOption{WithRootCAs(srvCAPool)},
- serverOpts: []ServerOption{WithCertificates(srvCert), WithClientAuth(NoClientCert), WithClientCAs(caPool)},
- clientAuth: NoClientCert,
- expectedServerCert: srvCert.Certificate,
- },
- "NoClientCert_ServerVerifyConnectionFails": {
- clientOpts: []ClientOption{WithRootCAs(srvCAPool)},
- serverOpts: []ServerOption{
- WithCertificates(srvCert),
- WithClientAuth(NoClientCert),
- WithClientCAs(caPool),
- WithVerifyConnection(func(*State) error {
- return errExample
- }),
- },
- clientAuth: NoClientCert,
- expectedServerCert: srvCert.Certificate,
- wantErr: true,
- },
- "NoClientCert_ClientVerifyConnectionFails": {
- clientOpts: []ClientOption{
- WithRootCAs(srvCAPool),
- WithVerifyConnection(func(*State) error {
- return errExample
- }),
- },
- serverOpts: []ServerOption{WithCertificates(srvCert), WithClientAuth(NoClientCert), WithClientCAs(caPool)},
- clientAuth: NoClientCert,
- expectedServerCert: srvCert.Certificate,
- wantErr: true,
- },
- "NoClientCert_cert": {
- clientOpts: []ClientOption{WithRootCAs(srvCAPool), WithCertificates(cert)},
- serverOpts: []ServerOption{WithCertificates(srvCert), WithClientAuth(RequireAnyClientCert)},
- clientAuth: RequireAnyClientCert,
- expectedClientCert: cert.Certificate,
- expectedServerCert: srvCert.Certificate,
- },
- "RequestClientCert_cert_sigscheme": { // specify signature algorithm
- clientOpts: []ClientOption{WithRootCAs(srvCAPool), WithCertificates(cert)},
- serverOpts: []ServerOption{
- WithSignatureSchemes(tls.ECDSAWithP521AndSHA512),
- WithCertificates(srvCert),
- WithClientAuth(RequestClientCert),
- },
- clientAuth: RequestClientCert,
- expectedClientCert: cert.Certificate,
- expectedServerCert: srvCert.Certificate,
- },
- "RequestClientCert_cert": {
- clientOpts: []ClientOption{WithRootCAs(srvCAPool), WithCertificates(cert)},
- serverOpts: []ServerOption{WithCertificates(srvCert), WithClientAuth(RequestClientCert)},
- clientAuth: RequestClientCert,
- expectedClientCert: cert.Certificate,
- expectedServerCert: srvCert.Certificate,
- },
- "RequestClientCert_no_cert": {
- clientOpts: []ClientOption{WithRootCAs(srvCAPool)},
- serverOpts: []ServerOption{
- WithCertificates(srvCert),
- WithClientAuth(RequestClientCert),
- WithClientCAs(caPool),
- },
- clientAuth: RequestClientCert,
- expectedServerCert: srvCert.Certificate,
- },
- "RequireAnyClientCert": {
- clientOpts: []ClientOption{WithRootCAs(srvCAPool), WithCertificates(cert)},
- serverOpts: []ServerOption{WithCertificates(srvCert), WithClientAuth(RequireAnyClientCert)},
- clientAuth: RequireAnyClientCert,
- expectedClientCert: cert.Certificate,
- expectedServerCert: srvCert.Certificate,
- },
- "RequireAnyClientCert_error": {
- clientOpts: []ClientOption{WithRootCAs(srvCAPool)},
- serverOpts: []ServerOption{WithCertificates(srvCert), WithClientAuth(RequireAnyClientCert)},
- clientAuth: RequireAnyClientCert,
- expectedServerCert: srvCert.Certificate,
- wantErr: true,
- },
- "VerifyClientCertIfGiven_no_cert": {
- clientOpts: []ClientOption{WithRootCAs(srvCAPool)},
- serverOpts: []ServerOption{
- WithCertificates(srvCert),
- WithClientAuth(VerifyClientCertIfGiven),
- WithClientCAs(caPool),
- },
- clientAuth: VerifyClientCertIfGiven,
- expectedServerCert: srvCert.Certificate,
- },
- "VerifyClientCertIfGiven_cert": {
- clientOpts: []ClientOption{WithRootCAs(srvCAPool), WithCertificates(cert)},
- serverOpts: []ServerOption{
- WithCertificates(srvCert),
- WithClientAuth(VerifyClientCertIfGiven),
- WithClientCAs(caPool),
- },
- clientAuth: VerifyClientCertIfGiven,
- expectedClientCert: cert.Certificate,
- expectedServerCert: srvCert.Certificate,
- },
- "VerifyClientCertIfGiven_error": {
- clientOpts: []ClientOption{WithRootCAs(srvCAPool), WithCertificates(cert)},
- serverOpts: []ServerOption{WithCertificates(srvCert), WithClientAuth(VerifyClientCertIfGiven)},
- clientAuth: VerifyClientCertIfGiven,
- expectedClientCert: cert.Certificate,
- expectedServerCert: srvCert.Certificate,
- wantErr: true,
- },
- "RequireAndVerifyClientCert": {
- clientOpts: []ClientOption{
- WithRootCAs(srvCAPool),
- WithCertificates(cert),
- WithVerifyConnection(func(s *State) error {
- if ok := bytes.Equal(s.PeerCertificates[0], srvCertificate.Raw); !ok {
- return errExample
- }
-
- return nil
- }),
- },
- serverOpts: []ServerOption{
- WithCertificates(srvCert),
- WithClientAuth(RequireAndVerifyClientCert),
- WithClientCAs(caPool),
- WithVerifyConnection(func(s *State) error {
- if ok := bytes.Equal(s.PeerCertificates[0], certificate.Raw); !ok {
- return errExample
- }
-
- return nil
- }),
- },
- clientAuth: RequireAndVerifyClientCert,
- expectedClientCert: cert.Certificate,
- expectedServerCert: srvCert.Certificate,
- },
- "RequireAndVerifyClientCert_callbacks": {
- clientOpts: []ClientOption{
- WithRootCAs(srvCAPool),
- WithGetClientCertificate(func(*CertificateRequestInfo) (*tls.Certificate, error) { return &cert, nil }),
- },
- serverOpts: []ServerOption{
- WithGetCertificate(func(*ClientHelloInfo) (*tls.Certificate, error) { return &srvCert, nil }),
- WithClientAuth(RequireAndVerifyClientCert),
- WithClientCAs(caPool),
- },
- clientAuth: RequireAndVerifyClientCert,
- expectedClientCert: cert.Certificate,
- expectedServerCert: srvCert.Certificate,
- },
- }
- for name, tt := range tests {
- t.Run(name, func(t *testing.T) {
- ca, cb := dpipe.Pipe()
- type result struct {
- c *Conn
- err, hserr error
- }
- clientCh := make(chan result)
-
- go func() {
- client, err := ClientWithOptions(dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), tt.clientOpts...)
- var hsErr error
- if err == nil {
- hsErr = client.Handshake()
- }
- clientCh <- result{client, err, hsErr}
- }()
-
- server, err := ServerWithOptions(dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), tt.serverOpts...)
- var hserr error
- if err == nil {
- hserr = server.Handshake()
- }
- res := <-clientCh
- defer func() {
- if err == nil {
- _ = server.Close()
- }
- if res.err == nil {
- _ = res.c.Close()
- }
- }()
-
- if tt.wantErr {
- assert.True(t, err != nil || hserr != nil || res.err != nil || res.hserr != nil, "Error expected")
-
- return // Error expected, test succeeded
- }
- assert.NoError(t, err)
- assert.NoError(t, res.err)
- assert.NoError(t, hserr)
- assert.NoError(t, res.hserr)
-
- state, ok := server.ConnectionState()
- assert.True(t, ok, "Server connection state not available")
-
- actualClientCert := state.PeerCertificates
- if tt.expectedClientCert != nil {
- assert.NotNil(t, actualClientCert, "Client did not provide a certificate")
- assert.Equal(t, actualClientCert[0], tt.expectedClientCert[0], "Client certificate was not communicated correctly")
- }
- if tt.clientAuth == NoClientCert {
- assert.Nil(t, actualClientCert, "Client certificate wasn't expected")
- }
-
- clientState, ok := res.c.ConnectionState()
- assert.True(t, ok, "Client connection state not available")
-
- actualServerCert := clientState.PeerCertificates
- assert.NotNil(t, actualServerCert, "server did not provide a certificate")
-
- assert.NotEmpty(t, tt.expectedServerCert, "Server certificate was not communicated correctly")
- assert.Equal(t, actualServerCert[0], tt.expectedServerCert[0], "Server certificate was not communicated correctly")
- })
- }
- })
-}
-
-func TestConnectionID(t *testing.T) {
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- clientCID := []byte{5, 77, 33, 24, 93, 27, 45, 81}
- serverCID := []byte{64, 24, 73, 2, 17, 96, 38, 59}
- cidEcho := func(echo []byte) func() []byte {
- return func() []byte {
- return echo
- }
- }
- tests := map[string]struct {
- clientOpts []ClientOption
- serverOpts []ServerOption
- clientConnectionID []byte
- serverConnectionID []byte
- }{
- "BidirectionalConnectionIDs": {
- clientOpts: []ClientOption{WithConnectionIDGenerator(cidEcho(clientCID))},
- serverOpts: []ServerOption{WithConnectionIDGenerator(cidEcho(serverCID))},
- clientConnectionID: clientCID,
- serverConnectionID: serverCID,
- },
- "BothSupportOnlyClientSends": {
- clientOpts: []ClientOption{WithConnectionIDGenerator(cidEcho(nil))},
- serverOpts: []ServerOption{WithConnectionIDGenerator(cidEcho(serverCID))},
- serverConnectionID: serverCID,
- },
- "BothSupportOnlyServerSends": {
- clientOpts: []ClientOption{WithConnectionIDGenerator(cidEcho(clientCID))},
- serverOpts: []ServerOption{WithConnectionIDGenerator(cidEcho(nil))},
- clientConnectionID: clientCID,
- },
- "ClientDoesNotSupport": {
- serverOpts: []ServerOption{WithConnectionIDGenerator(cidEcho(serverCID))},
- },
- "ServerDoesNotSupport": {
- clientOpts: []ClientOption{WithConnectionIDGenerator(cidEcho(clientCID))},
- },
- "NeitherSupport": {},
- }
- for name, tt := range tests {
- t.Run(name, func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- ca, cb := dpipe.Pipe()
- type result struct {
- c *Conn
- err error
- }
- c := make(chan result)
-
- go func() {
- client, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), tt.clientOpts, true)
- c <- result{client, err}
- }()
-
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), tt.serverOpts, true)
- assert.NoError(t, err)
-
- res := <-c
- assert.NoError(t, res.err)
- defer func() {
- if err == nil {
- _ = server.Close()
- }
- if res.err == nil {
- _ = res.c.Close()
- }
- }()
-
- assert.True(t, bytes.Equal(tt.clientConnectionID, res.c.state.GetLocalConnectionID()),
- "Unexpected client local connection ID")
- assert.True(t, bytes.Equal(tt.serverConnectionID, res.c.state.RemoteConnectionID),
- "Unexpected client remote connection ID")
- assert.True(t, bytes.Equal(tt.serverConnectionID, server.state.GetLocalConnectionID()),
- "Unexpected server local connection ID")
- assert.True(t, bytes.Equal(tt.clientConnectionID, server.state.RemoteConnectionID),
- "Unexpected server remote connection ID")
- })
- }
-}
-
-func TestExtendedMasterSecret(t *testing.T) {
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- tests := map[string]struct {
- clientOpts []ClientOption
- serverOpts []ServerOption
- expectedClientErr error
- expectedServerErr error
- }{
- "Request_Request_ExtendedMasterSecret": {
- clientOpts: []ClientOption{WithExtendedMasterSecret(RequestExtendedMasterSecret)},
- serverOpts: []ServerOption{WithExtendedMasterSecret(RequestExtendedMasterSecret)},
- expectedClientErr: nil,
- expectedServerErr: nil,
- },
- "Request_Require_ExtendedMasterSecret": {
- clientOpts: []ClientOption{WithExtendedMasterSecret(RequestExtendedMasterSecret)},
- serverOpts: []ServerOption{WithExtendedMasterSecret(RequireExtendedMasterSecret)},
- expectedClientErr: nil,
- expectedServerErr: nil,
- },
- "Request_Disable_ExtendedMasterSecret": {
- clientOpts: []ClientOption{WithExtendedMasterSecret(RequestExtendedMasterSecret)},
- serverOpts: []ServerOption{WithExtendedMasterSecret(DisableExtendedMasterSecret)},
- expectedClientErr: nil,
- expectedServerErr: nil,
- },
- "Require_Request_ExtendedMasterSecret": {
- clientOpts: []ClientOption{WithExtendedMasterSecret(RequireExtendedMasterSecret)},
- serverOpts: []ServerOption{WithExtendedMasterSecret(RequestExtendedMasterSecret)},
- expectedClientErr: nil,
- expectedServerErr: nil,
- },
- "Require_Require_ExtendedMasterSecret": {
- clientOpts: []ClientOption{WithExtendedMasterSecret(RequireExtendedMasterSecret)},
- serverOpts: []ServerOption{WithExtendedMasterSecret(RequireExtendedMasterSecret)},
- expectedClientErr: nil,
- expectedServerErr: nil,
- },
- "Require_Disable_ExtendedMasterSecret": {
- clientOpts: []ClientOption{WithExtendedMasterSecret(RequireExtendedMasterSecret)},
- serverOpts: []ServerOption{WithExtendedMasterSecret(DisableExtendedMasterSecret)},
- expectedClientErr: dtlserrors.ErrClientRequiredButNoServerEMS,
- expectedServerErr: &alertError{&alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}},
- },
- "Disable_Request_ExtendedMasterSecret": {
- clientOpts: []ClientOption{WithExtendedMasterSecret(DisableExtendedMasterSecret)},
- serverOpts: []ServerOption{WithExtendedMasterSecret(RequestExtendedMasterSecret)},
- expectedClientErr: nil,
- expectedServerErr: nil,
- },
- "Disable_Require_ExtendedMasterSecret": {
- clientOpts: []ClientOption{WithExtendedMasterSecret(DisableExtendedMasterSecret)},
- serverOpts: []ServerOption{WithExtendedMasterSecret(RequireExtendedMasterSecret)},
- expectedClientErr: &alertError{&alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}},
- expectedServerErr: dtlserrors.ErrServerRequiredButNoClientEMS,
- },
- "Disable_Disable_ExtendedMasterSecret": {
- clientOpts: []ClientOption{WithExtendedMasterSecret(DisableExtendedMasterSecret)},
- serverOpts: []ServerOption{WithExtendedMasterSecret(DisableExtendedMasterSecret)},
- expectedClientErr: nil,
- expectedServerErr: nil,
- },
- }
- for name, tt := range tests {
- t.Run(name, func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- ca, cb := dpipe.Pipe()
- type result struct {
- c *Conn
- err error
- }
- c := make(chan result)
-
- go func() {
- client, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), tt.clientOpts, true)
- c <- result{client, err}
- }()
-
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), tt.serverOpts, true)
- res := <-c
- defer func() {
- if err == nil {
- _ = server.Close()
- }
- if res.err == nil {
- _ = res.c.Close()
- }
- }()
- assert.ErrorIs(t, res.err, tt.expectedClientErr)
- assert.ErrorIs(t, err, tt.expectedServerErr)
- })
- }
-}
-
-func TestServerCertificate(t *testing.T) { //nolint:cyclop
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- cert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- certificate, err := x509.ParseCertificate(cert.Certificate[0])
- assert.NoError(t, err)
-
- caPool := x509.NewCertPool()
- caPool.AddCert(certificate)
-
- t.Run("parallel", func(t *testing.T) { // sync routines to check routine leak
- tests := map[string]struct {
- clientOpts []ClientOption
- serverOpts []ServerOption
- wantErr bool
- }{
- "no_ca": {
- serverOpts: []ServerOption{WithCertificates(cert), WithClientAuth(NoClientCert)},
- wantErr: true,
- },
- "good_ca": {
- clientOpts: []ClientOption{WithRootCAs(caPool)},
- serverOpts: []ServerOption{WithCertificates(cert), WithClientAuth(NoClientCert)},
- },
- "no_ca_skip_verify": {
- clientOpts: []ClientOption{WithInsecureSkipVerify(true)},
- serverOpts: []ServerOption{WithCertificates(cert), WithClientAuth(NoClientCert)},
- },
- "good_ca_skip_verify_custom_verify_peer": {
- clientOpts: []ClientOption{WithRootCAs(caPool), WithCertificates(cert)},
- serverOpts: []ServerOption{
- WithCertificates(cert),
- WithClientAuth(RequireAnyClientCert),
- WithVerifyPeerCertificate(func(_ [][]byte, chain [][]*x509.Certificate) error {
- if len(chain) != 0 {
- return errNotExpectedChain
- }
-
- return nil
- }),
- },
- },
- "good_ca_verify_custom_verify_peer": {
- clientOpts: []ClientOption{WithRootCAs(caPool), WithCertificates(cert)},
- serverOpts: []ServerOption{
- WithClientCAs(caPool),
- WithCertificates(cert),
- WithClientAuth(RequireAndVerifyClientCert),
- WithVerifyPeerCertificate(func(_ [][]byte, chain [][]*x509.Certificate) error {
- if len(chain) == 0 {
- return errExpecedChain
- }
-
- return nil
- }),
- },
- },
- "good_ca_custom_verify_peer": {
- clientOpts: []ClientOption{
- WithRootCAs(caPool),
- WithVerifyPeerCertificate(func([][]byte, [][]*x509.Certificate) error {
- return errWrongCert
- }),
- },
- serverOpts: []ServerOption{WithCertificates(cert), WithClientAuth(NoClientCert)},
- wantErr: true,
- },
- "server_name": {
- clientOpts: []ClientOption{WithRootCAs(caPool), WithServerName(certificate.Subject.CommonName)},
- serverOpts: []ServerOption{WithCertificates(cert), WithClientAuth(NoClientCert)},
- },
- "server_name_error": {
- clientOpts: []ClientOption{WithRootCAs(caPool), WithServerName("barfoo")},
- serverOpts: []ServerOption{WithCertificates(cert), WithClientAuth(NoClientCert)},
- wantErr: true,
- },
- }
- for name, tt := range tests {
- t.Run(name, func(t *testing.T) {
- ca, cb := dpipe.Pipe()
-
- type result struct {
- c *Conn
- err, hserr error
- }
- srvCh := make(chan result)
- go func() {
- s, err := ServerWithOptions(dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), tt.serverOpts...)
- var hsErr error
- if err == nil {
- hsErr = s.Handshake()
- }
- srvCh <- result{s, err, hsErr}
- }()
-
- cli, err := ClientWithOptions(dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), tt.clientOpts...)
- var hserr error
- if err == nil {
- hserr = cli.Handshake()
- }
- if err == nil {
- _ = cli.Close()
- }
- if tt.wantErr {
- assert.True(t, err != nil || hserr != nil, "Expected error")
- } else {
- assert.NoError(t, err, "Client connection failed")
- assert.NoError(t, hserr, "Client handshake failed")
- }
-
- srv := <-srvCh
- if srv.err == nil {
- _ = srv.c.Close()
- }
- })
- }
- })
-}
-
-func TestCipherSuiteConfiguration(t *testing.T) {
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- for _, test := range []struct {
- Name string
- ClientCipherSuites []CipherSuiteID
- ServerCipherSuites []CipherSuiteID
- WantClientError error
- WantServerError error
- WantSelectedCipherSuite CipherSuiteID
- }{
- {
- Name: "No cipherSuites specified",
- ClientCipherSuites: nil,
- ServerCipherSuites: nil,
- WantClientError: nil,
- WantServerError: nil,
- },
- {
- Name: "Invalid CipherSuite",
- ClientCipherSuites: []CipherSuiteID{0x00},
- ServerCipherSuites: []CipherSuiteID{0x00},
- WantClientError: &invalidCipherSuiteError{0x00},
- WantServerError: &invalidCipherSuiteError{0x00},
- },
- {
- Name: "Valid cipherSuites specified",
- ClientCipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
- ServerCipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
- WantClientError: nil,
- WantServerError: nil,
- WantSelectedCipherSuite: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- },
- {
- Name: "cipherSuites mismatch",
- ClientCipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
- ServerCipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA},
- WantClientError: &alertError{&alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}},
- WantServerError: dtlserrors.ErrCipherSuiteNoIntersection,
- },
- {
- Name: "Valid cipherSuites CCM specified",
- ClientCipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_CCM},
- ServerCipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_CCM},
- WantClientError: nil,
- WantServerError: nil,
- WantSelectedCipherSuite: TLS_ECDHE_ECDSA_WITH_AES_128_CCM,
- },
- {
- Name: "Valid cipherSuites CCM-8 specified",
- ClientCipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8},
- ServerCipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8},
- WantClientError: nil,
- WantServerError: nil,
- WantSelectedCipherSuite: TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8,
- },
- {
- Name: "Server supports subset of client suites",
- ClientCipherSuites: []CipherSuiteID{
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
- },
- ServerCipherSuites: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA},
- WantClientError: nil,
- WantServerError: nil,
- WantSelectedCipherSuite: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
- },
- } {
- t.Run(test.Name, func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- ca, cb := dpipe.Pipe()
- type result struct {
- c *Conn
- err error
- }
- resultCh := make(chan result)
-
- go func() {
- var opts []ClientOption
- if len(test.ClientCipherSuites) > 0 {
- opts = append(opts, WithCipherSuites(test.ClientCipherSuites...))
- }
- client, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), opts, true)
- resultCh <- result{client, err}
- }()
-
- var opts []ServerOption
- if len(test.ServerCipherSuites) > 0 {
- opts = append(opts, WithCipherSuites(test.ServerCipherSuites...))
- }
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), opts, true)
- if err == nil {
- defer func() {
- _ = server.Close()
- }()
- }
- assert.ErrorIsf(t, err, test.WantServerError, "TestCipherSuiteConfiguration: Server Error Mismatch '%s'", test.Name)
-
- res := <-resultCh
- if err == nil {
- assert.NoError(t, server.Close())
- assert.NoError(t, res.c.Close())
- }
- assert.ErrorIsf(t, res.err, test.WantClientError, "TestCipherSuiteConfiguration: Client Error Mismatch '%s'")
- if test.WantSelectedCipherSuite != 0x00 {
- assert.Equal(t, test.WantSelectedCipherSuite, res.c.state.CipherSuite.ID(),
- "TestCipherSuiteConfiguration: Server Selected Bad Cipher Suite '%s'", test.Name)
- }
- })
- }
-}
-
-func TestCertificateAndPSKServer(t *testing.T) {
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- for _, test := range []struct {
- Name string
- ClientPSK bool
- }{
- {
- Name: "Client uses PKI",
- ClientPSK: false,
- },
- {
- Name: "Client uses psk",
- ClientPSK: true,
- },
- } {
- t.Run(test.Name, func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- ca, cb := dpipe.Pipe()
- type result struct {
- c *Conn
- err error
- }
- resultCh := make(chan result)
-
- go func() {
- opts := []ClientOption{WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)}
- if test.ClientPSK {
- opts = []ClientOption{
- WithPSK(func([]byte) ([]byte, error) {
- return []byte{0x00, 0x01, 0x02}, nil
- }),
- WithPSKIdentityHint([]byte{0x00}),
- WithCipherSuites(TLS_PSK_WITH_AES_128_GCM_SHA256),
- }
- }
-
- client, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), opts, false)
- resultCh <- result{client, err}
- }()
-
- opts := []ServerOption{
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_PSK_WITH_AES_128_GCM_SHA256),
- WithPSK(func([]byte) ([]byte, error) {
- return []byte{0x00, 0x01, 0x02}, nil
- }),
- }
-
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), opts, true)
- assert.NoErrorf(t, err, "TestCertificateAndPSKServer: Server Error Mismatch '%s'", test.Name)
- if err != nil {
- defer func() {
- assert.NoError(t, server.Close())
- }()
- }
-
- res := <-resultCh
- assert.NoErrorf(t, res.err, "TestCertificateAndPSKServer: Server Error Mismatch '%s'", test.Name)
- assert.NoError(t, server.Close())
- assert.NoError(t, res.c.Close())
- })
- }
-}
-
-func TestPSKConfiguration(t *testing.T) { //nolint:cyclop
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- for _, test := range []struct {
- Name string
- ClientHasCertificate bool
- ServerHasCertificate bool
- ClientPSK PSKCallback
- ServerPSK PSKCallback
- ClientPSKIdentity []byte
- ServerPSKIdentity []byte
- WantClientError error
- WantServerError error
- }{
- {
- Name: "psk and no certificate specified",
- ClientHasCertificate: false,
- ServerHasCertificate: false,
- ClientPSK: func([]byte) ([]byte, error) { return []byte{0x00, 0x01, 0x02}, nil },
- ServerPSK: func([]byte) ([]byte, error) { return []byte{0x00, 0x01, 0x02}, nil },
- ClientPSKIdentity: []byte{0x00},
- ServerPSKIdentity: []byte{0x00},
- WantClientError: dtlserrors.ErrNoAvailablePSKCipherSuite,
- WantServerError: dtlserrors.ErrNoAvailablePSKCipherSuite,
- },
- {
- Name: "psk and certificate specified",
- ClientHasCertificate: true,
- ServerHasCertificate: true,
- ClientPSK: func([]byte) ([]byte, error) { return []byte{0x00, 0x01, 0x02}, nil },
- ServerPSK: func([]byte) ([]byte, error) { return []byte{0x00, 0x01, 0x02}, nil },
- ClientPSKIdentity: []byte{0x00},
- ServerPSKIdentity: []byte{0x00},
- WantClientError: dtlserrors.ErrNoAvailablePSKCipherSuite,
- WantServerError: dtlserrors.ErrNoAvailablePSKCipherSuite,
- },
- {
- Name: "psk and no identity specified",
- ClientHasCertificate: false,
- ServerHasCertificate: false,
- ClientPSK: func([]byte) ([]byte, error) { return []byte{0x00, 0x01, 0x02}, nil },
- ServerPSK: func([]byte) ([]byte, error) { return []byte{0x00, 0x01, 0x02}, nil },
- ClientPSKIdentity: nil,
- ServerPSKIdentity: nil,
- WantClientError: dtlserrors.ErrPSKAndIdentityMustBeSetForClient,
- WantServerError: dtlserrors.ErrNoAvailablePSKCipherSuite,
- },
- {
- Name: "No psk and identity specified",
- ClientHasCertificate: false,
- ServerHasCertificate: false,
- ClientPSK: nil,
- ServerPSK: nil,
- ClientPSKIdentity: []byte{0x00},
- ServerPSKIdentity: []byte{0x00},
- WantClientError: dtlserrors.ErrIdentityNoPSK,
- WantServerError: dtlserrors.ErrIdentityNoPSK,
- },
- } {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- ca, cb := dpipe.Pipe()
- type result struct {
- c *Conn
- err error
- }
- resultCh := make(chan result)
-
- go func() {
- var opts []ClientOption
- if test.ClientPSK != nil {
- opts = append(opts, WithPSK(test.ClientPSK))
- }
- if test.ClientPSKIdentity != nil {
- opts = append(opts, WithPSKIdentityHint(test.ClientPSKIdentity))
- }
- client, err := testClient(
- ctx,
- dtlsnet.PacketConnFromConn(ca),
- ca.RemoteAddr(),
- opts,
- test.ClientHasCertificate,
- )
- resultCh <- result{client, err}
- }()
-
- var opts []ServerOption
- if test.ServerPSK != nil {
- opts = append(opts, WithPSK(test.ServerPSK))
- }
- if test.ServerPSKIdentity != nil {
- opts = append(opts, WithPSKIdentityHint(test.ServerPSKIdentity))
- }
- _, err := testServer(
- ctx,
- dtlsnet.PacketConnFromConn(cb),
- cb.RemoteAddr(),
- opts,
- test.ServerHasCertificate,
- )
- if err != nil || test.WantServerError != nil {
- if err == nil || test.WantServerError == nil || err.Error() != test.WantServerError.Error() {
- assert.Failf(t, "TestPSKConfiguration", "Server Error Mismatch '%s'", test.Name)
- }
- }
-
- res := <-resultCh
- if res.err != nil || test.WantClientError != nil {
- if res.err == nil || test.WantClientError == nil && res.err.Error() != test.WantClientError.Error() {
- assert.Failf(t, "TestPSKConfiguration", "Client Error Mismatch '%s'", test.Name)
- }
- }
- }
-}
-
-func TestServerTimeout(t *testing.T) { //nolint:cyclop
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- cookie := make([]byte, 20)
- _, err := rand.Read(cookie)
- assert.NoError(t, err)
-
- var rand [28]byte
- random := handshake.Random{GMTUnixTime: time.Unix(500, 0), RandomBytes: rand}
-
- cipherSuites := []CipherSuite{
- &ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256{},
- &ciphersuite.TLSEcdheRsaWithAes128GcmSha256{},
- }
-
- extensions := []extension.Extension{
- &extension.SupportedSignatureAlgorithms{
- SignatureHashAlgorithms: []signaturehash.Algorithm{
- {Hash: hash.SHA256, Signature: signature.ECDSA},
- {Hash: hash.SHA384, Signature: signature.ECDSA},
- {Hash: hash.SHA512, Signature: signature.ECDSA},
- {Hash: hash.SHA256, Signature: signature.RSA},
- {Hash: hash.SHA384, Signature: signature.RSA},
- {Hash: hash.SHA512, Signature: signature.RSA},
- },
- },
- &extension.SupportedEllipticCurves{
- EllipticCurves: []elliptic.Curve{elliptic.X25519, elliptic.P256, elliptic.P384},
- },
- &extension.SupportedPointFormats{
- PointFormats: []elliptic.CurvePointFormat{elliptic.CurvePointFormatUncompressed},
- },
- }
-
- record := &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- SequenceNumber: 0,
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- // sequenceNumber and messageSequence line up, may need to be re-evaluated
- Header: handshake.Header{
- MessageSequence: 0,
- },
- Message: &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- Cookie: cookie,
- Random: random,
- CipherSuiteIDs: cipherSuiteIDs(cipherSuites),
- CompressionMethods: defaultCompressionMethods(),
- Extensions: extensions,
- },
- },
- }
-
- packet, err := record.Marshal()
+ bufferLease := &readBufferLease{conn: conn, recyclableReadBuffer: &rawPacket}
+ outcome, err := conn.handleIncomingPacket(
+ context.Background(),
+ rawPacket,
+ nil,
+ bufferLease,
+ false,
+ )
assert.NoError(t, err)
+ assert.Nil(t, outcome.responseAlert)
+ assert.False(t, outcome.containsHandshake)
+ assert.False(t, outcome.retransmit)
+ assert.Same(t, &rawPacket, bufferLease.recyclableReadBuffer)
+ assert.Empty(t, conn.encryptedPackets)
+}
- ca, cb := dpipe.Pipe()
- defer func() {
- assert.NoError(t, ca.Close())
- }()
-
- // Client reader
- caReadChan := make(chan []byte, 1000)
- go func() {
- for {
- data := make([]byte, 8192)
- n, err := ca.Read(data)
- if err != nil {
- return
- }
+func TestOnConnectionAttemptConnectionOwnership(t *testing.T) {
+ expectedErr := errors.New("connection rejected") //nolint:err113
+ config, err := buildServerConfig(WithOnConnectionAttempt(func(net.Addr) error { return expectedErr }))
+ assert.NoError(t, err)
- caReadChan <- data[:n]
- }
- }()
+ t.Run("Listener closes rejected accepted PacketConn", func(t *testing.T) {
+ ca, cb := packetPipe()
+ defer func() {
+ assert.NoError(t, ca.Close())
+ }()
- // Start sending ClientHello packets until server responds with first packet
- go func() {
- for {
- select {
- case <-time.After(10 * time.Millisecond):
- _, err := ca.Write(packet)
- if err != nil {
- return
- }
- case <-caReadChan:
- // Once we receive the first reply from the server, stop
- return
- }
+ conn := &closeTrackingPacketConn{PacketConn: cb}
+ l := &listener{
+ config: config,
+ parent: &singlePacketListener{conn: conn, raddr: cb.RemoteAddr()},
}
- }()
- ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
- defer cancel()
+ _, err := l.Accept()
+ assert.ErrorIs(t, err, expectedErr)
+ assert.True(t, conn.closed.Load())
+ })
+}
- serverOpts := []ServerOption{
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithFlightInterval(100 * time.Millisecond),
- }
+type closeTrackingPacketConn struct {
+ net.PacketConn
+ closed atomic.Bool
+}
- _, serverErr := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), serverOpts, true)
- var netErr net.Error
- assert.ErrorAsf(t, serverErr, &netErr, "Client error exp(Temporary network error) failed(%v)", serverErr)
- assert.Truef(t, netErr.Timeout(), "Client error exp(Temporary network error) failed(%v)", serverErr)
+func (c *closeTrackingPacketConn) Close() error {
+ c.closed.Store(true)
- // Wait a little longer to ensure no additional messages have been sent by the server
- time.Sleep(300 * time.Millisecond)
- select {
- case msg := <-caReadChan:
- assert.Fail(t, "Expected no additional messages from server", "got: %+v", msg)
- default:
- }
+ return c.PacketConn.Close()
}
-func TestProtocolVersionValidation(t *testing.T) { //nolint:maintidx
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
+type singlePacketListener struct {
+ conn net.PacketConn
+ raddr net.Addr
+}
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
+func (l *singlePacketListener) Accept() (net.PacketConn, net.Addr, error) {
+ return l.conn, l.raddr, nil
+}
- cookie := make([]byte, 20)
- _, err := rand.Read(cookie)
- assert.NoError(t, err)
+func (*singlePacketListener) Close() error { return nil }
- var rand [28]byte
- random := handshake.Random{GMTUnixTime: time.Unix(500, 0), RandomBytes: rand}
-
- clientOpts := []ClientOption{
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithFlightInterval(100 * time.Millisecond),
- }
- serverOpts := []ServerOption{
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithFlightInterval(100 * time.Millisecond),
- }
-
- t.Run("Server", func(t *testing.T) {
- serverCases := map[string]struct {
- records []*recordlayer.RecordLayer
- }{
- "ClientHelloVersion": {
- records: []*recordlayer.RecordLayer{
- {
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageClientHello{
- Version: protocol.Version{Major: 0xfe, Minor: 0xff}, // try to downgrade
- Cookie: cookie,
- Random: random,
- CipherSuiteIDs: []uint16{uint16((&ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256{}).ID())},
- CompressionMethods: defaultCompressionMethods(),
- },
- },
- },
- },
- },
- "SecondsClientHelloVersion": {
- records: []*recordlayer.RecordLayer{
- {
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- Cookie: cookie,
- Random: random,
- CipherSuiteIDs: []uint16{uint16((&ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256{}).ID())},
- CompressionMethods: defaultCompressionMethods(),
- },
- },
- },
- {
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- SequenceNumber: 1,
- },
- Content: &handshake.Handshake{
- Header: handshake.Header{
- MessageSequence: 1,
- },
- Message: &handshake.MessageClientHello{
- Version: protocol.Version{Major: 0xfe, Minor: 0xff}, // try to downgrade
- Cookie: cookie,
- Random: random,
- CipherSuiteIDs: []uint16{uint16((&ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256{}).ID())},
- CompressionMethods: defaultCompressionMethods(),
- },
- },
- },
- },
- },
- }
- for name, serverCase := range serverCases {
- t.Run(name, func(t *testing.T) {
- ca, cb := dpipe.Pipe()
- defer func() {
- assert.NoError(t, ca.Close())
- }()
-
- ctx, cancel := context.WithTimeout(context.Background(), time.Second)
- defer cancel()
-
- var wg sync.WaitGroup
- wg.Add(1)
- defer wg.Wait()
- go func() {
- defer wg.Done()
- _, err := testServer(
- ctx,
- dtlsnet.PacketConnFromConn(cb),
- cb.RemoteAddr(),
- serverOpts,
- true,
- )
- assert.ErrorIs(t, err, dtlserrors.ErrUnsupportedProtocolVersion)
- }()
-
- time.Sleep(50 * time.Millisecond)
-
- resp := make([]byte, 1024)
- for _, record := range serverCase.records {
- packet, err := record.Marshal()
- assert.NoError(t, err)
-
- _, werr := ca.Write(packet)
- assert.NoError(t, werr)
-
- n, rerr := ca.Read(resp[:cap(resp)])
- assert.NoError(t, rerr)
-
- resp = resp[:n]
- }
+func (*singlePacketListener) Addr() net.Addr { return nil }
- h := &recordlayer.Header{}
- assert.NoError(t, h.Unmarshal(resp))
- assert.Equal(t, protocol.ContentTypeAlert, h.ContentType, "Peer must return alert to unsupported protocol version")
- })
- }
- })
+func TestFragmentBuffer_Retransmission(t *testing.T) {
+ fragmentBuffer := dtlsfragmentbuffer.New()
+ frag := []byte{0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x30, 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, 0x01, 0x01}
- t.Run("Client", func(t *testing.T) {
- clientCases := map[string]struct {
- records []*recordlayer.RecordLayer
- }{
- "ServerHelloVersion": {
- records: []*recordlayer.RecordLayer{
- {
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageHelloVerifyRequest{
- Version: protocol.Version1_2,
- Cookie: cookie,
- },
- },
- },
- {
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- SequenceNumber: 1,
- },
- Content: &handshake.Handshake{
- Header: handshake.Header{
- MessageSequence: 1,
- },
- Message: &handshake.MessageServerHello{
- Version: protocol.Version{Major: 0xfe, Minor: 0xff}, // try to downgrade
- Random: random,
- CipherSuiteID: func() *uint16 {
- id := uint16(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)
-
- return &id
- }(),
- CompressionMethod: defaultCompressionMethods()[0],
- },
- },
- },
- },
- },
- }
- for name, clientCase := range clientCases {
- t.Run(name, func(t *testing.T) {
- ca, cb := dpipe.Pipe()
- defer func() {
- assert.NoError(t, ca.Close())
- }()
-
- ctx, cancel := context.WithTimeout(context.Background(), time.Second)
- defer cancel()
-
- var wg sync.WaitGroup
- wg.Add(1)
- defer wg.Wait()
- go func() {
- defer wg.Done()
- _, err := testClient(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), clientOpts, true)
- assert.ErrorIs(t, err, dtlserrors.ErrUnsupportedProtocolVersion)
- }()
-
- time.Sleep(50 * time.Millisecond)
-
- for _, record := range clientCase.records {
- _, err := ca.Read(make([]byte, 1024))
- assert.NoError(t, err)
-
- packet, err := record.Marshal()
- assert.NoError(t, err)
-
- _, err = ca.Write(packet)
- assert.NoError(t, err)
- }
- resp := make([]byte, 1024)
- n, err := ca.Read(resp)
- assert.NoError(t, err)
+ result, err := fragmentBuffer.Push(0, frag[recordlayer.FixedHeaderSize:])
+ assert.NoError(t, err)
+ assert.False(t, result.IsRetransmit)
- resp = resp[:n]
+ v, _ := fragmentBuffer.Pop()
+ assert.NotNil(t, v)
- h := &recordlayer.Header{}
- assert.NoError(t, h.Unmarshal(resp))
- assert.Equal(t, protocol.ContentTypeAlert, h.ContentType, "Peer must return alert to unsupported protocol version")
- })
- }
- })
+ result, err = fragmentBuffer.Push(0, frag[recordlayer.FixedHeaderSize:])
+ assert.NoError(t, err)
+ assert.True(t, result.IsRetransmit)
}
-func TestMultipleHelloVerifyRequest(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- cookies := [][]byte{
- // first clientHello contains an empty cookie
- {},
+func TestDTLS13ServerSendsFinalACK(t *testing.T) {
+ defer test.CheckRoutines(t)()
+ defer test.TimeOut(10 * time.Second).Stop()
+
+ ca, cb := packetPipe()
+ var applicationEpochWrites atomic.Int32
+ ackRecord := make(chan []byte, 1)
+ serverTransport := &connWithCallback{
+ packetTestConn: cb,
+ onWrite: func(raw []byte) {
+ if len(raw) > 0 && protocol.IsDTLS13Ciphertext(protocol.ContentType(raw[0])) && raw[0]&recordwire.EpochMask == byte(dtlsflight13.EpochApplication) {
+ applicationEpochWrites.Add(1)
+ select {
+ case ackRecord <- append([]byte(nil), raw...):
+ default:
+ }
+ }
+ },
}
- var packets [][]byte
- for i := range 2 {
- cookie := make([]byte, 20)
- _, err := rand.Read(cookie)
- assert.NoError(t, err)
- cookies = append(cookies, cookie)
+ clientCert, err := selfsign.GenerateSelfSigned()
+ require.NoError(t, err)
+ client, err := Client(ca, ca.RemoteAddr(), WithCertificates(clientCert), WithInsecureSkipVerify(true), WithMinVersion(protocol.Version1_3), WithMaxVersion(protocol.Version1_3))
+ require.NoError(t, err)
+ defer func() { _ = client.Close() }()
- record := &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- SequenceNumber: uint64(i), //nolint:gosec // G101
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Header: handshake.Header{
- MessageSequence: uint16(i), //nolint:gosec // G115
- },
- Message: &handshake.MessageHelloVerifyRequest{
- Version: protocol.Version1_2,
- Cookie: cookie,
- },
- },
- }
- packet, err := record.Marshal()
- assert.NoError(t, err)
+ serverCert, err := selfsign.GenerateSelfSigned()
+ require.NoError(t, err)
+ server, err := Server(serverTransport, serverTransport.RemoteAddr(), WithCertificates(serverCert), WithInsecureSkipVerify(true), WithMinVersion(protocol.Version1_3), WithMaxVersion(protocol.Version1_3))
+ require.NoError(t, err)
+ defer func() { _ = server.Close() }()
- packets = append(packets, packet)
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ errs := make(chan error, 2)
+ go func() { errs <- client.HandshakeContext(ctx) }()
+ go func() { errs <- server.HandshakeContext(ctx) }()
+ require.NoError(t, <-errs)
+ require.NoError(t, <-errs)
+ assert.GreaterOrEqual(t, applicationEpochWrites.Load(), int32(1))
+
+ var rawACK []byte
+ select {
+ case rawACK = <-ackRecord:
+ case <-time.After(time.Second):
+ require.FailNow(t, "server did not write an application-epoch ACK")
}
+ ciphertext := unmarshalCiphertextRecordForTest(t, rawACK, 0)
+ clientState, ok := client.state.(*dtlsstate.State13)
+ require.True(t, ok)
+ readGeneration, ok := clientState.TrafficKeys.Read(dtlsflight13.EpochApplication)
+ require.True(t, ok)
+ protection := &testRecordProtection13{TrafficProtection: readGeneration.Protection, capabilities: clientState.CipherSuite.Capabilities()}
+ innerPlaintext, err := protection.OpenRecord(ciphertext.Header, 0, ciphertext.EncryptedRecord)
+ require.NoError(t, err)
+ require.Equal(t, protocol.ContentTypeACK, innerPlaintext.RealType)
+ var ack protocol.ACK
+ require.NoError(t, ack.Unmarshal(innerPlaintext.Content))
+ require.NotEmpty(t, ack.Records)
+ assert.Equal(t, dtlsflight13.EpochHandshake, ack.Records[0].Epoch)
+}
+
+func TestHandshakeCancellationWhilePostSetupBlocks(t *testing.T) {
+ defer test.CheckRoutines(t)()
- ca, cb := dpipe.Pipe()
+ ca, cb := packetPipe()
defer func() {
- assert.NoError(t, ca.Close())
+ _ = ca.Close()
+ _ = cb.Close()
}()
- ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
- defer cancel()
-
- var wg sync.WaitGroup
- wg.Add(1)
- defer wg.Wait()
- go func() {
- defer wg.Done()
- _, _ = testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), nil, false)
+ clientCert, err := selfsign.GenerateSelfSigned()
+ require.NoError(t, err)
+ client, err := Client(ca, ca.RemoteAddr(), WithCertificates(clientCert), WithInsecureSkipVerify(true), WithMinVersion(protocol.Version1_3), WithMaxVersion(protocol.Version1_3))
+ require.NoError(t, err)
+ defer func() {
+ _ = client.Close()
}()
- for i, cookie := range cookies {
- // read client hello
- resp := make([]byte, 1024)
- n, err := cb.Read(resp)
- assert.NoError(t, err)
+ postSetupStarted := make(chan struct{})
+ start := client.prepareHandshakeStart13()
+ start.fsmState = dtlshandshake.StateWaiting
+ start.postSetup = func(ctx context.Context) {
+ close(postSetupStarted)
+ <-ctx.Done()
+ }
- record := &recordlayer.RecordLayer{}
- assert.NoError(t, record.Unmarshal(resp[:n]))
+ ctx, cancel := context.WithCancel(context.Background())
+ result := make(chan error, 1)
+ go func() {
+ result <- client.handshake(ctx, start)
+ }()
- clientHello, ok := record.Content.(*handshake.Handshake).Message.(*handshake.MessageClientHello)
- assert.True(t, ok)
- assert.Equal(t, cookie, clientHello.Cookie)
- if len(packets) <= i {
- break
- }
- // write hello verify request
- _, err = cb.Write(packets[i])
- assert.NoError(t, err)
+ select {
+ case <-postSetupStarted:
+ case <-time.After(time.Second):
+ require.FailNow(t, "post-setup hook did not start")
}
cancel()
+
+ select {
+ case err = <-result:
+ assert.ErrorIs(t, err, context.Canceled)
+ case <-time.After(time.Second):
+ require.FailNow(t, "handshake did not observe context cancellation")
+ }
}
-// Assert that a DTLS Server only responds with RenegotiationInfo if a ClientHello contained that
-// extension according to RFC5746 section 3.6, RFC5246 section 7.4.1.4 and RFC5746 section 4.2.
-func TestRenegotationInfo(t *testing.T) { //nolint:cyclop
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(10 * time.Second)
- defer lim.Stop()
+func TestProcessProtectedPacketWritesDTLS13HandshakeRecord(t *testing.T) {
+ conn, peerCipherSuite := newTestConnWithWriteProtection(t)
+ dtlsHandshake := &handshake.Handshake{
+ Message: &handshake.MessageEncryptedExtensions{},
+ }
+ expectedPlaintext, err := dtlsHandshake.Marshal()
+ assert.NoError(t, err)
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
+ rawPacket, err := conn.prepareRecord(&dtlsflight.Outbound{Epoch: dtlsflight13.EpochHandshake, Content: dtlsHandshake, Protection: dtlsflight.ProtectionCiphertext})
+ assert.NoError(t, err)
+ assert.NotEmpty(t, rawPacket)
+ assert.True(t, protocol.IsDTLS13Ciphertext(protocol.ContentType(rawPacket[0])))
- resp := make([]byte, 1024)
+ innerPlaintext := openTestProtectedRecord(t, peerCipherSuite, rawPacket)
+ assert.Equal(t, protocol.ContentTypeHandshake, innerPlaintext.RealType)
+ assert.Equal(t, expectedPlaintext, innerPlaintext.Content)
+}
- for _, testCase := range []struct {
- Name string
- ExpectRenegotiationInfo bool
- SendRenegotiationInfoExt bool
- IncludeRenegotiationSCSV bool
- }{
- {
- Name: "Include RenegotiationInfo",
- ExpectRenegotiationInfo: true,
- SendRenegotiationInfoExt: true,
- },
- {
- Name: "RenegotiationInfo SCSV",
- ExpectRenegotiationInfo: true,
- IncludeRenegotiationSCSV: true,
- },
- {
- Name: "No RenegotiationInfo",
- ExpectRenegotiationInfo: false,
- },
- } {
- test := testCase
- t.Run(test.Name, func(t *testing.T) {
- ca, cb := dpipe.Pipe()
- defer func() {
- assert.NoError(t, ca.Close())
- }()
+type sequenceRecordingProtection struct {
+ capabilities cryptosuite.Capabilities
+ sequences []uint64
+}
- ctx := t.Context()
+func (p *sequenceRecordingProtection) Seal(record cryptosuite.Record, plaintext []byte) ([]byte, error) {
+ p.sequences = append(p.sequences, record.RecordNumber()&recordlayer.MaxSequenceNumber)
+ protectedLen, err := p.capabilities.ProtectedLen(len(plaintext))
+ if err != nil {
+ return nil, err
+ }
- go func() {
- _, err := testServer(
- ctx,
- dtlsnet.PacketConnFromConn(cb),
- cb.RemoteAddr(),
- nil,
- true,
- )
- assert.ErrorIs(t, err, context.Canceled)
- }()
+ return make([]byte, protectedLen), nil
+}
- time.Sleep(50 * time.Millisecond)
+func (*sequenceRecordingProtection) Open(cryptosuite.Record, []byte) ([]byte, error) {
+ return nil, cryptosuite.ErrAuthenticationFailed
+}
- extensions := []extension.Extension{}
- if test.SendRenegotiationInfoExt {
- extensions = append(extensions, &extension.RenegotiationInfo{
- RenegotiatedConnection: 0,
- })
- }
- cipherSuites := cipherSuiteIDs(defaultCipherSuites())
- if test.IncludeRenegotiationSCSV {
- cipherSuites = append(cipherSuites, renegotiationInfoSCSV)
- }
- err := sendClientHello([]byte{}, ca, 0, extensions, cipherSuites...)
- assert.NoError(t, err)
+func TestProcessHandshakePacketCIDFragmentsUseAllocatedSequenceNumbers(t *testing.T) {
+ const (
+ epoch = 1
+ firstSequence uint64 = 41
+ staleSequence uint64 = 7
+ )
- n, err := ca.Read(resp)
- assert.NoError(t, err)
+ remoteCID := []byte("remote-cid")
+ cipherSuite := ciphersuite.ForID(cryptosuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)
+ protection := &sequenceRecordingProtection{capabilities: cipherSuite.Capabilities()}
+ commonState := &dtlsstate.Common{LocalVersion: protocol.Version1_2, LocalSequenceNumber: map[uint64]uint64{1: firstSequence}, RemoteConnectionID: remoteCID, CipherSuite: cipherSuite}
+ conn := &Conn{maximumTransmissionUnit: 10, paddingLengthGenerator: func(uint) uint { return 0 }, state: &dtlsstate.State12{Common: commonState, Protection: protection}}
+ dtlsHandshake := &handshake.Handshake{Header: handshake.Header{MessageSequence: 9}, Message: &handshake.MessageCertificate{Certificate: [][]byte{bytes.Repeat([]byte{0xaa}, 24)}}}
+ _, err := dtlsHandshake.Marshal()
+ require.NoError(t, err)
+
+ pkt := &dtlsflight.Outbound{
+ Epoch: epoch,
+ Content: dtlsHandshake,
+ Protection: dtlsflight.ProtectionCiphertext,
+ }
+ prepared, err := conn.prepareHandshakeRecords(pkt, dtlsHandshake)
+ require.NoError(t, err)
+ require.Greater(t, len(prepared), 1)
+
+ expectedSequences := make([]uint64, len(prepared))
+ for i, record := range prepared {
+ expectedSequence := firstSequence + uint64(i)
+ expectedSequences[i] = expectedSequence
+
+ header, parseErr := recordlayer.ParseRecord(record.raw, len(remoteCID))
+ require.NoError(t, parseErr)
+ assert.Equal(t, protocol.ContentTypeConnectionID, header.ContentType())
+ assert.Equal(t, remoteCID, header.ConnectionID())
+ assert.Equal(t, uint16(epoch), header.Epoch())
+ assert.Equal(t, expectedSequence, header.SequenceNumber())
+ }
- record := &recordlayer.RecordLayer{}
- assert.NoError(t, record.Unmarshal(resp[:n]))
+ // Encrypt receives this record metadata for the nonce and MAC calculation.
+ assert.Equal(t, expectedSequences, protection.sequences)
+ assert.Equal(t, firstSequence+uint64(len(expectedSequences)), commonState.LocalSequenceNumber[epoch])
+}
- helloVerifyRequest, ok := record.Content.(*handshake.Handshake).Message.(*handshake.MessageHelloVerifyRequest)
- assert.True(t, ok)
+func TestProcessProtectedHandshakePacketWritesDTLS13Fragments(t *testing.T) {
+ conn, peerCipherSuite := newTestConnWithWriteProtection(t)
+ dtlsHandshake := &handshake.Handshake{
+ Message: &handshake.MessageEncryptedExtensions{},
+ }
+ expectedPlaintext, err := dtlsHandshake.Marshal()
+ assert.NoError(t, err)
- err = sendClientHello(helloVerifyRequest.Cookie, ca, 1, extensions, cipherSuites...)
- assert.NoError(t, err)
+ prepared, err := conn.prepareHandshakeRecords(&dtlsflight.Outbound{Epoch: dtlsflight13.EpochHandshake, Content: dtlsHandshake, Protection: dtlsflight.ProtectionCiphertext}, dtlsHandshake)
+ assert.NoError(t, err)
+ require.Len(t, prepared, 1)
+ assert.True(t, protocol.IsDTLS13Ciphertext(protocol.ContentType(prepared[0].raw[0])))
- n, err = ca.Read(resp)
- assert.NoError(t, err)
+ innerPlaintext := openTestProtectedRecord(t, peerCipherSuite, prepared[0].raw)
+ assert.Equal(t, protocol.ContentTypeHandshake, innerPlaintext.RealType)
+ assert.Equal(t, expectedPlaintext, innerPlaintext.Content)
+}
- messages, err := recordlayer.UnpackDatagram(resp[:n])
- assert.NoError(t, err)
- assert.NoError(t, record.Unmarshal(messages[0]))
+func TestProcessProtectedHandshakePacketFiltersACKedFragments(t *testing.T) {
+ conn, _ := newTestConnWithWriteProtection(t)
+ conn.maximumTransmissionUnit = 10
+ dtlsHandshake := &handshake.Handshake{Header: handshake.Header{MessageSequence: 9}, Message: &handshake.MessageCertificate{Certificate: [][]byte{bytes.Repeat([]byte{0xaa}, 24)}}}
+ _, err := dtlsHandshake.Marshal()
+ require.NoError(t, err)
+
+ prepared, err := conn.prepareHandshakeRecords(&dtlsflight.Outbound{Epoch: dtlsflight13.EpochHandshake, Content: dtlsHandshake, Protection: dtlsflight.ProtectionCiphertext, TrackACK: true, HandshakeFragmentOffsets: map[uint32]uint32{10: 10}}, dtlsHandshake)
+ require.NoError(t, err)
+ require.Len(t, prepared, 1)
+ assert.Equal(t, uint32(10), prepared[0].tracked.Fragments[0].Offset)
+}
- serverHello, ok := record.Content.(*handshake.Handshake).Message.(*handshake.MessageServerHello)
- assert.True(t, ok)
+func TestProcessProtectedPacketWritesApplicationData(t *testing.T) {
+ conn, peerCipherSuite := newTestConnWithWriteProtection(t)
+ payload := []byte("application data")
- actualNegotationInfo := false
- for _, v := range serverHello.Extensions {
- if _, ok := v.(*extension.RenegotiationInfo); ok {
- actualNegotationInfo = true
- }
- }
+ rawPacket, err := conn.prepareRecord(&dtlsflight.Outbound{Epoch: dtlsflight13.EpochApplication, Content: &protocol.ApplicationData{Data: payload}, Protection: dtlsflight.ProtectionCiphertext})
+ require.NoError(t, err)
- assert.True(t, test.ExpectRenegotiationInfo == actualNegotationInfo,
- "NegotationInfo state in ServerHello is incorrect: expected(%t) actual(%t)",
- test.ExpectRenegotiationInfo, actualNegotationInfo)
- })
- }
+ innerPlaintext := openTestProtectedRecord(t, peerCipherSuite, rawPacket)
+ assert.Equal(t, protocol.ContentTypeApplicationData, innerPlaintext.RealType)
+ assert.Equal(t, payload, innerPlaintext.Content)
}
-func TestServerNameIndicationExtension(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
+func TestProcessProtectedPacketWritesACK(t *testing.T) {
+ conn, peerCipherSuite := newTestConnWithWriteProtection(t)
+ ack := &protocol.ACK{Records: []protocol.RecordNumber{{Epoch: 2, SequenceNumber: 9}}}
+ expected, err := ack.Marshal()
+ require.NoError(t, err)
+
+ rawPacket, err := conn.prepareRecord(&dtlsflight.Outbound{Epoch: dtlsflight13.EpochApplication, Content: ack, Protection: dtlsflight.ProtectionCiphertext})
+ require.NoError(t, err)
+ innerPlaintext := openTestProtectedRecord(t, peerCipherSuite, rawPacket)
+ assert.Equal(t, protocol.ContentTypeACK, innerPlaintext.RealType)
+ assert.Equal(t, expected, innerPlaintext.Content)
+}
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
+func TestOpenCiphertextRecordHandshake(t *testing.T) {
+ conn, peerCipherSuite := newTestConnWithReadProtection(t)
+ expectedPlaintext := encryptedExtensionsHandshake(t)
+ record := sealTestProtectedHandshakeRecord(t, peerCipherSuite, expectedPlaintext)
- for _, test := range []struct {
- Name string
- serverName string
- Expected []byte
- IncludeSNI bool
+ innerPlaintext, sequenceNumber, epoch, err := conn.openCiphertextRecord(record.parsed(t))
+ assert.NoError(t, err)
+ assert.Equal(t, uint64(0), sequenceNumber)
+ assert.Equal(t, dtlsflight13.EpochHandshake, epoch)
+ assert.Equal(t, protocol.ContentTypeHandshake, innerPlaintext.RealType)
+ assert.Equal(t, expectedPlaintext, innerPlaintext.Content)
+}
+
+func TestOpenCiphertextRecordRejectsInvalidRecords(t *testing.T) {
+ tests := []struct {
+ name string
+ mutate func(*Conn, *sealedTestRecord)
+ wantErr error
}{
{
- Name: "Server name is a valid hostname",
- serverName: "example.com", //nolint:goconst
- Expected: []byte("example.com"),
- IncludeSNI: true,
- },
- {
- Name: "Server name is an IP literal",
- serverName: "1.2.3.4",
- Expected: []byte(""),
- IncludeSNI: false,
- },
- {
- Name: "Server name is empty",
- serverName: "",
- Expected: []byte(""),
- IncludeSNI: false,
+ name: "wrong sequence number",
+ mutate: func(conn *Conn, _ *sealedTestRecord) {
+ dtlsstate.CommonState(conn.state).UpdateRemoteSequenceNumber(dtlsflight13.EpochHandshake, 0xffff)
+ },
+ wantErr: errRecordAuthentication,
},
- } {
- t.Run(test.Name, func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- ca, cb := dpipe.Pipe()
- go func() {
- _, _ = testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), []ClientOption{
- WithServerName(test.serverName),
- }, false)
- }()
-
- // Receive ClientHello
- resp := make([]byte, 1024)
- n, err := cb.Read(resp)
- assert.NoError(t, err)
-
- r := &recordlayer.RecordLayer{}
- assert.NoError(t, r.Unmarshal(resp[:n]))
-
- clientHello, ok := r.Content.(*handshake.Handshake).Message.(*handshake.MessageClientHello)
- assert.True(t, ok)
-
- gotSNI := false
- var actualServerName string
- for _, v := range clientHello.Extensions {
- if _, ok := v.(*extension.ServerName); ok {
- gotSNI = true
- extensionServerName, ok := v.(*extension.ServerName)
- assert.True(t, ok)
-
- actualServerName = extensionServerName.ServerName
- }
- }
+ {name: "wrong epoch", mutate: func(_ *Conn, record *sealedTestRecord) { record.Header.EpochLow ^= 0x01 }, wantErr: dtlserrors.ErrInvalidEpoch},
+ {name: "tampered ciphertext", mutate: func(_ *Conn, record *sealedTestRecord) { record.EncryptedRecord[0] ^= 0x80 }, wantErr: errRecordAuthentication},
+ }
- assert.Equalf(t, test.IncludeSNI, gotSNI, "TestSNI: expected SNI inclusion '%s'", test.Name)
- assert.Equalf(t, test.Expected, []byte(actualServerName), "TestSNI: server name mismatch '%s'", test.Name)
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ conn, peerCipherSuite := newTestConnWithReadProtection(t)
+ record := sealTestProtectedHandshakeRecord(t, peerCipherSuite, encryptedExtensionsHandshake(t))
+ test.mutate(conn, &record)
+
+ innerPlaintext, sequenceNumber, epoch, err := conn.openCiphertextRecord(record.parsed(t))
+ assert.ErrorIs(t, err, test.wantErr)
+ assert.Zero(t, innerPlaintext)
+ assert.Zero(t, sequenceNumber)
+ assert.Zero(t, epoch)
})
}
}
-func TestALPNExtension(t *testing.T) { //nolint:maintidx
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- for _, test := range []struct {
- Name string
- ClientProtocolNameList []string
- ServerProtocolNameList []string
- ExpectedProtocol string
- ExpectAlertFromClient bool
- ExpectAlertFromServer bool
- Alert alert.Description
- }{
- {
- Name: "Negotiate a protocol",
- ClientProtocolNameList: []string{"http/1.1", "spd/1"}, //nolint:goconst
- ServerProtocolNameList: []string{"spd/1"},
- ExpectedProtocol: "spd/1", //nolint:goconst
- ExpectAlertFromClient: false,
- ExpectAlertFromServer: false,
- Alert: 0,
- },
- {
- Name: "Server doesn't support any",
- ClientProtocolNameList: []string{"http/1.1", "spd/1"},
- ServerProtocolNameList: []string{},
- ExpectedProtocol: "",
- ExpectAlertFromClient: false,
- ExpectAlertFromServer: false,
- Alert: 0,
- },
- {
- Name: "Negotiate with higher server precedence",
- ClientProtocolNameList: []string{"http/1.1", "spd/1", "http/3"},
- ServerProtocolNameList: []string{"ssh/2", "http/3", "spd/1"},
- ExpectedProtocol: "http/3", //nolint:goconst
- ExpectAlertFromClient: false,
- ExpectAlertFromServer: false,
- Alert: 0,
- },
- {
- Name: "Empty intersection",
- ClientProtocolNameList: []string{"http/1.1", "http/3"},
- ServerProtocolNameList: []string{"ssh/2", "spd/1"},
- ExpectedProtocol: "",
- ExpectAlertFromClient: false,
- ExpectAlertFromServer: true,
- Alert: alert.NoApplicationProtocol,
- },
- {
- Name: "Multiple protocols in ServerHello",
- ClientProtocolNameList: []string{"http/1.1"},
- ServerProtocolNameList: []string{"http/1.1"},
- ExpectedProtocol: "http/1.1",
- ExpectAlertFromClient: true,
- ExpectAlertFromServer: false,
- Alert: alert.InternalError,
- },
- } {
- t.Run(test.Name, func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- ca, cb := dpipe.Pipe()
- go func() {
- var opts []ClientOption
- if len(test.ClientProtocolNameList) > 0 {
- opts = append(opts, WithSupportedProtocols(test.ClientProtocolNameList...))
- }
- _, _ = testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), opts, false)
- }()
-
- // Receive ClientHello
- resp := make([]byte, 1024)
- n, err := cb.Read(resp)
- assert.NoError(t, err)
-
- ctx2, cancel2 := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel2()
-
- ca2, cb2 := dpipe.Pipe()
- go func() {
- var opts []ServerOption
- if len(test.ServerProtocolNameList) > 0 {
- opts = append(opts, WithSupportedProtocols(test.ServerProtocolNameList...))
- }
- _, err2 := testServer(ctx2, dtlsnet.PacketConnFromConn(cb2), cb2.RemoteAddr(), opts, true)
- if test.ExpectAlertFromServer {
- assert.NotErrorIs(t, err2, context.Canceled)
- }
- }()
-
- time.Sleep(50 * time.Millisecond)
-
- // Forward ClientHello
- _, err = ca2.Write(resp[:n])
- assert.NoError(t, err)
-
- // Receive HelloVerify
- resp2 := make([]byte, 1024)
- n, err = ca2.Read(resp2)
- assert.NoError(t, err)
-
- // Forward HelloVerify
- _, err = cb.Write(resp2[:n])
- assert.NoError(t, err)
-
- // Receive ClientHello
- resp3 := make([]byte, 1024)
- n, err = cb.Read(resp3)
- assert.NoError(t, err)
-
- // Forward ClientHello
- _, err = ca2.Write(resp3[:n])
- assert.NoError(t, err)
-
- // Receive ServerHello
- resp4 := make([]byte, 1024)
- n, err = ca2.Read(resp4)
- assert.NoError(t, err)
-
- messages, err := recordlayer.UnpackDatagram(resp4[:n])
- assert.NoError(t, err)
-
- record := &recordlayer.RecordLayer{}
- assert.NoError(t, record.Unmarshal(messages[0]))
+func TestDTLS13DecryptedEncryptedExtensionsIsCached(t *testing.T) {
+ conn, peerCipherSuite := newTestConnWithReadProtection(t)
+ state13, err := dtlsstate.As13(conn.state)
+ require.NoError(t, err)
+ state13.HandshakeRecvSequence = 1
+ expectedPlaintext := encryptedExtensionsHandshakeWithSequence(t, 1)
+ record := sealTestProtectedHandshakeRecord(t, peerCipherSuite, expectedPlaintext)
+ rawPacket, err := record.Marshal()
+ assert.NoError(t, err)
- if test.ExpectAlertFromServer { //nolint:nestif
- a, ok := record.Content.(*alert.Alert)
- assert.True(t, ok)
- assert.Equalf(t, test.Alert, a.Description, "ALPN %v", test.Name)
- } else {
- serverHello, ok := record.Content.(*handshake.Handshake).Message.(*handshake.MessageServerHello)
- assert.True(t, ok)
+ bufferLease := &readBufferLease{conn: conn}
+ outcome, err := conn.handleIncomingPacket(
+ context.Background(), rawPacket, nil, bufferLease, false,
+ )
+ assert.NoError(t, err)
+ assert.Nil(t, outcome.responseAlert)
+ assert.True(t, outcome.containsHandshake)
+ assert.False(t, outcome.retransmit)
+
+ items := conn.handshakeCache.Pull(dtlsflight.HandshakeCachePullRule{Typ: handshake.TypeEncryptedExtensions, Epoch: dtlsflight13.EpochHandshake, IsClient: false})
+ if assert.Len(t, items, 1) && assert.NotNil(t, items[0]) {
+ assert.Equal(t, dtlsflight13.EpochHandshake, items[0].Epoch)
+ assert.Equal(t, uint16(1), items[0].MessageSequence)
+ assert.Equal(t, expectedPlaintext, items[0].Data)
+ }
+}
- var negotiatedProtocol string
- for _, v := range serverHello.Extensions {
- if _, ok := v.(*extension.ALPN); ok {
- e, ok := v.(*extension.ALPN)
- assert.True(t, ok)
+func TestDTLS13ProtectedHandshakeRecordKeepsEpochAndSequence(t *testing.T) {
+ conn, peerCipherSuite := newTestConnWithReadProtection(t)
+ const sequenceNumber = uint64(0x1002a)
+ dtlsstate.CommonState(conn.state).UpdateRemoteSequenceNumber(dtlsflight13.EpochHandshake, 0x10005)
+ expectedPlaintext := encryptedExtensionsHandshakeWithSequence(t, 0)
+ record := sealTestProtectedHandshakeRecordWithSequence(t, peerCipherSuite, expectedPlaintext, sequenceNumber)
+ rawPacket, err := record.Marshal()
+ assert.NoError(t, err)
- negotiatedProtocol = e.ProtocolNameList[0]
+ prepared, ok, err := conn.prepareIncomingPacket(rawPacket, nil, &readBufferLease{conn: conn}, false)
+ require.NoError(t, err)
+ assert.True(t, ok)
+ assert.Equal(t, dtlsflight13.EpochHandshake, prepared.number.Epoch)
+ assert.Equal(t, sequenceNumber, prepared.number.SequenceNumber)
+ assert.Equal(t, protocol.ContentTypeHandshake, prepared.contentType)
+ assert.Equal(t, expectedPlaintext, prepared.content)
+ assert.Equal(t, rawPacket, prepared.raw)
+}
- // Manipulate ServerHello
- if test.ExpectAlertFromClient {
- e.ProtocolNameList = append(e.ProtocolNameList, "oops")
- }
- }
- }
+type testRecordProtection13 struct {
+ cryptosuite.TrafficProtection
+ capabilities cryptosuite.Capabilities
+}
- assert.Equalf(t, test.ExpectedProtocol, negotiatedProtocol, "ALPN %v", test.Name)
+func newTestRecordProtection13(
+ suite cryptosuite.TrafficSuite,
+ secret []byte,
+) (*testRecordProtection13, error) {
+ trafficSecret, err := ciphersuite.NewTrafficSecret(secret)
+ if err != nil {
+ return nil, err
+ }
+ protection, err := suite.NewTrafficProtection(trafficSecret)
+ if err != nil {
+ return nil, err
+ }
- s, err := record.Marshal()
- assert.NoError(t, err)
+ return &testRecordProtection13{TrafficProtection: protection, capabilities: suite.Capabilities()}, nil
+}
- // Forward ServerHello
- _, err = cb.Write(s)
- assert.NoError(t, err)
+func (p *testRecordProtection13) SealRecord(header recordlayer.CiphertextConfig, sequenceNumber uint64, contentType protocol.ContentType, plaintext []byte) (sealedTestRecord, error) {
+ innerPlaintext, err := recordlayer.MarshalInnerPlaintext(plaintext, contentType, 0)
+ if err != nil {
+ return sealedTestRecord{}, err
+ }
+ protectedLen, err := p.capabilities.ProtectedLen(len(innerPlaintext))
+ if err != nil {
+ return sealedTestRecord{}, err
+ }
+ header, metadata, err := newTestRecord13(header, sequenceNumber, protectedLen)
+ if err != nil {
+ return sealedTestRecord{}, err
+ }
+ protected, err := p.Seal(metadata, innerPlaintext)
+ if err != nil {
+ return sealedTestRecord{}, err
+ }
+ mask, err := p.sequenceNumberMask(protected)
+ if err != nil {
+ return sealedTestRecord{}, err
+ }
+ header.SequenceNumber, err = applySequenceNumberMask(header.SequenceNumber, true, mask)
+ if err != nil {
+ return sealedTestRecord{}, err
+ }
- if test.ExpectAlertFromClient {
- resp5 := make([]byte, 1024)
- n, err = cb.Read(resp5)
- assert.NoError(t, err)
+ return sealedTestRecord{Header: header, EncryptedRecord: protected}, nil
+}
- r2 := &recordlayer.RecordLayer{}
- assert.NoError(t, r2.Unmarshal(resp5[:n]))
+func (p *testRecordProtection13) OpenRecord(header recordlayer.CiphertextConfig, sequenceNumber uint64, protected []byte) (openedRecord, error) {
+ mask, err := p.sequenceNumberMask(protected)
+ if err != nil {
+ return openedRecord{}, err
+ }
+ header.SequenceNumber, err = applySequenceNumberMask(header.SequenceNumber, header.TwoByteSequence, mask)
+ if err != nil {
+ return openedRecord{}, err
+ }
+ clearHeader, err := recordwire.AppendUnifiedHeader(nil, header.EpochLow, header.SequenceNumber, header.TwoByteSequence, header.ConnectionID, header.LengthPresent, len(protected))
+ if err != nil {
+ return openedRecord{}, err
+ }
+ metadata, err := ciphersuite.NewUnifiedRecord(uint64(header.EpochLow), sequenceNumber, clearHeader, len(protected))
+ if err != nil {
+ return openedRecord{}, err
+ }
+ plaintext, err := p.Open(metadata, protected)
+ if errors.Is(err, cryptosuite.ErrAuthenticationFailed) {
+ return openedRecord{}, dtlserrors.ErrDecryptPacket
+ }
+ if err != nil {
+ return openedRecord{}, err
+ }
+ content, realType, _, err := recordlayer.ParseInnerPlaintext(plaintext)
+ innerPlaintext := openedRecord{Content: content, RealType: realType}
+ if err != nil {
+ return openedRecord{}, err
+ }
- a, ok := r2.Content.(*alert.Alert)
- assert.True(t, ok)
- assert.Equalf(t, test.Alert, a.Description, "ALPN %v", test.Name)
- }
- }
+ return innerPlaintext, nil
+}
- time.Sleep(50 * time.Millisecond) // Give some time for returned errors
- })
+func newTestRecord13(header recordlayer.CiphertextConfig, sequenceNumber uint64, protectedLen int) (recordlayer.CiphertextConfig, cryptosuite.Record, error) {
+ header.SequenceNumber = uint16(sequenceNumber) //nolint:gosec
+ header.TwoByteSequence = true
+ header.LengthPresent = true
+ clearHeader, err := recordwire.AppendUnifiedHeader(nil, header.EpochLow, header.SequenceNumber, header.TwoByteSequence, header.ConnectionID, header.LengthPresent, protectedLen)
+ if err != nil {
+ return header, nil, err
}
+ metadata, err := ciphersuite.NewUnifiedRecord(uint64(header.EpochLow), sequenceNumber, clearHeader, protectedLen)
+
+ return header, metadata, err
}
-// Make sure the supported_groups extension is not included in the ServerHello.
-func TestSupportedGroupsExtension(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
+func (p *testRecordProtection13) sequenceNumberMask(protected []byte) ([]byte, error) {
+ maskLen := p.capabilities.MaskLen()
+ if len(protected) < maskLen {
+ return nil, dtlserrors.ErrBufferTooSmall
+ }
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
+ return p.Mask(protected[:maskLen])
+}
- t.Run("ServerHello Supported Groups", func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
+func newTestConnWithWriteProtection(t *testing.T) (*Conn, *testRecordProtection13) {
+ t.Helper()
- ca, cb := dpipe.Pipe()
- go func() {
- _, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), nil, true)
- assert.ErrorIs(t, err, context.Canceled)
- }()
- extensions := []extension.Extension{
- &extension.SupportedEllipticCurves{
- EllipticCurves: []elliptic.Curve{elliptic.X25519, elliptic.P256, elliptic.P384},
- },
- &extension.SupportedPointFormats{
- PointFormats: []elliptic.CurvePointFormat{elliptic.CurvePointFormatUncompressed},
- },
- }
+ localCipherSuite := trafficSuiteForTest(t)
+ clientSecret := bytes.Repeat([]byte{0x11}, localCipherSuite.HashFunc()().Size())
+ serverSecret := bytes.Repeat([]byte{0x22}, localCipherSuite.HashFunc()().Size())
+ localWriteProtection, err := newTestRecordProtection13(localCipherSuite, clientSecret)
+ assert.NoError(t, err)
+ localReadProtection, err := newTestRecordProtection13(localCipherSuite, serverSecret)
+ assert.NoError(t, err)
- time.Sleep(50 * time.Millisecond)
+ peerCipherSuite := trafficSuiteForTest(t)
+ peerReadProtection, err := newTestRecordProtection13(peerCipherSuite, clientSecret)
+ assert.NoError(t, err)
- resp := make([]byte, 1024)
- err := sendClientHello([]byte{}, ca, 0, extensions)
- assert.NoError(t, err)
+ commonState := &dtlsstate.Common{
+ IsClient: true,
+ LocalVersion: protocol.Version1_3,
+ CipherSuite: localCipherSuite,
+ }
+ state13 := &dtlsstate.State13{Common: commonState, TrafficKeys: &dtlsstate.TrafficKeyState{}}
+ state13.TrafficKeys.Install(&dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochHandshake, Secret: clientSecret, Protection: localWriteProtection}, &dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochHandshake, Secret: serverSecret, Protection: localReadProtection})
+ state13.TrafficKeys.Install(&dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochApplication, Secret: clientSecret, Protection: localWriteProtection}, &dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochApplication, Secret: serverSecret, Protection: localReadProtection})
- // Receive ServerHello
- n, err := ca.Read(resp)
- assert.NoError(t, err)
+ return &Conn{handshakeCache: dtlsflight.NewCache(), maximumTransmissionUnit: defaultMTU, state: state13}, peerReadProtection
+}
- record := &recordlayer.RecordLayer{}
- assert.NoError(t, record.Unmarshal(resp[:n]))
+func newTestConnWithReadProtection(t *testing.T) (*Conn, *testRecordProtection13) {
+ t.Helper()
- helloVerifyRequest, ok := record.Content.(*handshake.Handshake).Message.(*handshake.MessageHelloVerifyRequest)
- assert.True(t, ok, "Failed to cast MessageHelloVerifyRequest")
+ localCipherSuite := trafficSuiteForTest(t)
+ clientSecret := bytes.Repeat([]byte{0x11}, localCipherSuite.HashFunc()().Size())
+ serverSecret := bytes.Repeat([]byte{0x22}, localCipherSuite.HashFunc()().Size())
+ localWriteProtection, err := newTestRecordProtection13(localCipherSuite, clientSecret)
+ assert.NoError(t, err)
+ localReadProtection, err := newTestRecordProtection13(localCipherSuite, serverSecret)
+ assert.NoError(t, err)
- err = sendClientHello(helloVerifyRequest.Cookie, ca, 1, extensions)
- assert.NoError(t, err)
+ peerCipherSuite := trafficSuiteForTest(t)
+ peerWriteProtection, err := newTestRecordProtection13(peerCipherSuite, serverSecret)
+ assert.NoError(t, err)
- n, err = ca.Read(resp)
- assert.NoError(t, err)
+ commonState := &dtlsstate.Common{
+ IsClient: true,
+ LocalVersion: protocol.Version1_3,
+ CipherSuite: localCipherSuite,
+ }
+ state13 := &dtlsstate.State13{Common: commonState, TrafficKeys: &dtlsstate.TrafficKeyState{}}
+ state13.TrafficKeys.Install(&dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochHandshake, Secret: clientSecret, Protection: localWriteProtection}, &dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochHandshake, Secret: serverSecret, Protection: localReadProtection})
+ state13.TrafficKeys.Install(&dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochApplication, Secret: clientSecret, Protection: localWriteProtection}, &dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochApplication, Secret: serverSecret, Protection: localReadProtection})
- messages, err := recordlayer.UnpackDatagram(resp[:n])
- assert.NoError(t, err)
- assert.NoError(t, record.Unmarshal(messages[0]))
+ conn := &Conn{fragmentBuffer: dtlsfragmentbuffer.New(), handshakeCache: dtlsflight.NewCache(), maximumTransmissionUnit: defaultMTU, replayProtectionWindow: defaultReplayProtectionWindow, log: logging.NewDefaultLoggerFactory().NewLogger("dtls"), state: state13}
+ conn.setRemoteEpoch(dtlsflight13.EpochHandshake)
- serverHello, ok := record.Content.(*handshake.Handshake).Message.(*handshake.MessageServerHello)
- assert.True(t, ok, "TestSupportedGroups: Failed to cast MessageServerHello")
+ return conn, peerWriteProtection
+}
- gotGroups := false
- for _, v := range serverHello.Extensions {
- if _, ok := v.(*extension.SupportedEllipticCurves); ok {
- gotGroups = true
- }
- }
+func encryptedExtensionsHandshake(t *testing.T) []byte {
+ t.Helper()
- assert.False(t, gotGroups, "TestSupportedGroups: supported_groups extension was sent in ServerHello")
- })
+ return encryptedExtensionsHandshakeWithSequence(t, 0)
}
-func TestSessionResume(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
+func encryptedExtensionsHandshakeWithSequence(t *testing.T, messageSequence uint16) []byte {
+ t.Helper()
- t.Run("resumed", func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
+ raw, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: messageSequence}, Message: &handshake.MessageEncryptedExtensions{}}).Marshal()
+ assert.NoError(t, err)
- type result struct {
- c *Conn
- err error
- }
- clientRes := make(chan result, 1)
+ return raw
+}
- ss := &memSessStore{}
+func sealTestProtectedHandshakeRecord(t *testing.T, protection *testRecordProtection13, plaintext []byte) sealedTestRecord {
+ t.Helper()
- id, _ := hex.DecodeString("9b9fc92255634d9fb109febed42166717bb8ded8c738ba71bc7f2a0d9dae0306")
- secret, _ := hex.DecodeString(
- "2e942a37aca5241deb2295b5fcedac221c7078d2503d2b62aeb48c880d7da73c001238b708559686b9da6e829c05ead7",
- )
+ return sealTestProtectedHandshakeRecordWithSequence(t, protection, plaintext, 0)
+}
- s := Session{ID: id, Secret: secret}
+func sealTestProtectedHandshakeRecordWithSequence(t *testing.T, protection *testRecordProtection13, plaintext []byte, sequenceNumber uint64) sealedTestRecord {
+ t.Helper()
- ca, cb := dpipe.Pipe()
+ record, err := protection.SealRecord(recordlayer.CiphertextConfig{EpochLow: uint8(dtlsflight13.EpochHandshake & recordwire.EpochMask)}, sequenceNumber, protocol.ContentTypeHandshake, plaintext)
+ assert.NoError(t, err)
- _ = ss.Set(id, s)
- _ = ss.Set([]byte(ca.RemoteAddr().String()+"_example.com"), s)
+ return record
+}
- go func() {
- opts := []ClientOption{
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithServerName("example.com"),
- WithSessionStore(ss),
- WithMTU(100),
- }
- c, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), opts, false)
- clientRes <- result{c, err}
- }()
+func openTestProtectedRecord(t *testing.T, protection *testRecordProtection13, rawPacket []byte) openedRecord {
+ t.Helper()
- opts := []ServerOption{
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithServerName("example.com"),
- WithSessionStore(ss),
- WithMTU(100),
- }
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), opts, true)
- assert.NoError(t, err)
+ ciphertext := unmarshalCiphertextRecordForTest(t, rawPacket, 0)
- state, ok := server.ConnectionState()
- assert.True(t, ok)
+ innerPlaintext, err := protection.OpenRecord(
+ ciphertext.Header,
+ 0,
+ ciphertext.EncryptedRecord,
+ )
+ assert.NoError(t, err)
- actualSessionID := state.SessionID
- actualMasterSecret := state.masterSecret
- assert.Equal(t, actualSessionID, id, "TestSessionResumetion SessionID mismatch")
- assert.Equal(t, actualMasterSecret, secret, "TestSessionResumetion masterSecret mismatch")
+ return innerPlaintext
+}
- defer func() {
- assert.NoError(t, server.Close())
- }()
+func unmarshalCiphertextRecordForTest(
+ t *testing.T,
+ raw []byte,
+ cidLength int,
+) sealedTestRecord {
+ t.Helper()
- res := <-clientRes
- assert.NoError(t, res.err)
- assert.NoError(t, res.c.Close())
- })
+ records, err := recordlayer.UnpackDatagram(raw, recordlayer.UnpackDatagramConfig{TargetVersion: protocol.Version1_3, CIDLength: cidLength})
+ require.NoError(t, err)
+ require.Len(t, records, 1)
- t.Run("new session", func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
+ parsed, err := recordlayer.ParseRecord(records[0], cidLength)
+ require.NoError(t, err)
+ record := sealedTestRecord{Header: recordlayer.CiphertextConfig{EpochLow: parsed.EpochLow(), SequenceNumber: uint16(parsed.SequenceNumber() & 0xffff), TwoByteSequence: parsed.SequenceBytes() == 2, ConnectionID: parsed.ConnectionID(), LengthPresent: parsed.LengthPresent()}, EncryptedRecord: parsed.Payload()}
- type result struct {
- c *Conn
- err error
- }
- clientRes := make(chan result, 1)
+ return record
+}
- s1 := &memSessStore{}
- s2 := &memSessStore{}
+func TestSealRecordContentUsesWriteTrafficGeneration(t *testing.T) {
+ conn, state, suite := newTrafficKeyTestConn(t)
+ handshakeSecret := trafficKeyTestSecret(suite, 0x11)
+ applicationSecret := trafficKeyTestSecret(suite, 0x22)
+ handshakeProtection := trafficKeyTestProtection(t, suite, handshakeSecret)
+ applicationProtection := trafficKeyTestProtection(t, suite, applicationSecret)
- ca, cb := dpipe.Pipe()
- go func() {
- opts := []ClientOption{
- WithServerName("example.com"),
- WithSessionStore(s1),
- }
- c, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), opts, false)
- clientRes <- result{c, err}
- }()
+ state.TrafficKeys.Install(&dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochHandshake, Secret: handshakeSecret, Protection: handshakeProtection}, nil)
+ state.TrafficKeys.Install(&dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochApplication, Secret: applicationSecret, Protection: applicationProtection}, nil)
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), []ServerOption{
- WithSessionStore(s2),
- }, true)
- assert.NoError(t, err)
+ applicationRecord, err := conn.sealRecordContent(dtlsflight13.EpochApplication, 0, protocol.ContentTypeApplicationData, []byte("application"))
+ require.NoError(t, err)
+ assertTrafficKeyTestRecord(t, applicationProtection, applicationRecord, []byte("application"))
- state, ok := server.ConnectionState()
- assert.True(t, ok)
- actualSessionID := state.SessionID
- actualMasterSecret := state.masterSecret
- ss, _ := s2.Get(actualSessionID)
- assert.Equal(t, actualMasterSecret, ss.Secret, "TestSessionResumetion masterSecret mismatch")
+ handshakeRecord, err := conn.sealRecordContent(dtlsflight13.EpochHandshake, 0, protocol.ContentTypeHandshake, []byte("handshake"))
+ require.NoError(t, err)
+ assertTrafficKeyTestRecord(t, handshakeProtection, handshakeRecord, []byte("handshake"))
- defer func() {
- assert.NoError(t, server.Close())
- }()
+ _, err = conn.sealRecordContent(4, 0, protocol.ContentTypeApplicationData, nil)
+ assert.ErrorIs(t, err, dtlserrors.ErrCipherSuiteRecordProtectionNotImplemented)
+}
- res := <-clientRes
- assert.NoError(t, res.err)
+func TestSealRecordContentAcceptsMaximumContent(t *testing.T) {
+ conn, peerProtection := newTestConnWithWriteProtection(t)
+ plaintext := bytes.Repeat([]byte{0x5a}, maxPlaintextRecordLen)
- cs, _ := s1.Get([]byte(ca.RemoteAddr().String() + "_example.com"))
- assert.Equal(t, actualMasterSecret, cs.Secret, "TestSessionResumetion mismatch")
- assert.NoError(t, res.c.Close())
- })
+ rawRecord, err := conn.sealRecordContent(dtlsflight13.EpochApplication, 0, protocol.ContentTypeApplicationData, plaintext)
+ require.NoError(t, err)
+ innerPlaintext := openTestProtectedRecord(t, peerProtection, rawRecord)
+ assert.Equal(t, plaintext, innerPlaintext.Content)
}
-type memSessStore struct {
- sync.Map
+func TestSealRecordContentUsesNegotiatedConnectionID(t *testing.T) {
+ conn, state, suite := newTrafficKeyTestConn(t)
+ secret := trafficKeyTestSecret(suite, 0x23)
+ protection := trafficKeyTestProtection(t, suite, secret)
+ state.TrafficKeys.Install(&dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochApplication, Secret: secret, Protection: protection}, nil)
+ state.CommitNegotiatedExtensions(&negotiation.ConnectionID{ClientCID: []byte("local-cid"), ServerCID: []byte("remote-cid")})
+
+ rawRecord, err := conn.sealRecordContent(dtlsflight13.EpochApplication, 0, protocol.ContentTypeApplicationData, []byte("application"))
+ require.NoError(t, err)
+
+ record := unmarshalCiphertextRecordForTest(t, rawRecord, len("remote-cid"))
+ assert.Equal(t, []byte("remote-cid"), record.Header.ConnectionID)
+ innerPlaintext, err := protection.OpenRecord(record.Header, 0, record.EncryptedRecord)
+ require.NoError(t, err)
+ assert.Equal(t, []byte("application"), innerPlaintext.Content)
}
-func (ms *memSessStore) Set(key []byte, s Session) error {
- k := hex.EncodeToString(key)
- ms.Store(k, s)
+func TestOpenCiphertextRecordUsesNegotiatedConnectionID(t *testing.T) {
+ conn, state, suite := newTrafficKeyTestConn(t)
+ secret := trafficKeyTestSecret(suite, 0x34)
+ protection := trafficKeyTestProtection(t, suite, secret)
+ state.TrafficKeys.Install(nil, &dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochApplication, Secret: secret, Protection: protection})
+ state.SetRemoteEpoch(dtlsflight13.EpochApplication)
+ state.CommitNegotiatedExtensions(&negotiation.ConnectionID{ClientCID: []byte("local-cid"), ServerCID: []byte("remote-cid")})
+
+ sealed, err := protection.SealRecord(recordlayer.CiphertextConfig{ConnectionID: []byte("local-cid"), EpochLow: uint8(dtlsflight13.EpochApplication & recordwire.EpochMask)}, 0, protocol.ContentTypeApplicationData, []byte("application"))
+ require.NoError(t, err)
+ rawRecord, err := sealed.Marshal()
+ require.NoError(t, err)
+
+ record, err := conn.unmarshalCiphertextRecord(rawRecord, true)
+ require.NoError(t, err)
+ innerPlaintext, sequenceNumber, epoch, err := conn.openCiphertextRecord(record)
+ require.NoError(t, err)
+ assert.Equal(t, uint64(0), sequenceNumber)
+ assert.Equal(t, dtlsflight13.EpochApplication, epoch)
+ assert.Equal(t, []byte("application"), innerPlaintext.Content)
+
+ sealed.Header.ConnectionID = nil
+ rawRecord, err = sealed.Marshal()
+ require.NoError(t, err)
+ _, err = conn.unmarshalCiphertextRecord(rawRecord, false)
+ assert.ErrorIs(t, err, dtlserrors.ErrInvalidCiphertextHeader)
+}
- return nil
+func TestCiphertextConnectionIDDoesNotMigrateWithoutRRC(t *testing.T) {
+ conn, peerProtection := newTestConnWithReadProtection(t)
+ state, ok := conn.state.(*dtlsstate.State13)
+ require.True(t, ok)
+ localCID := []byte("local-cid")
+ state.CommitNegotiatedExtensions(&negotiation.ConnectionID{
+ ClientCID: localCID,
+ ServerCID: []byte("remote-cid"),
+ })
+ conn.setRemoteEpoch(dtlsflight13.EpochApplication)
+ conn.decrypted = make(chan any, 1)
+ conn.closed = closer.NewCloser()
+ activeAddr := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 1), Port: 5000}
+ candidateAddr := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 2), Port: 6000}
+ conn.rAddr = activeAddr
+
+ sealed, err := peerProtection.SealRecord(recordlayer.CiphertextConfig{ConnectionID: localCID, EpochLow: uint8(dtlsflight13.EpochApplication & recordwire.EpochMask)}, 0, protocol.ContentTypeApplicationData, []byte("application"))
+ require.NoError(t, err)
+ rawRecord, err := sealed.Marshal()
+ require.NoError(t, err)
+
+ _, err = conn.handleIncomingPacket(t.Context(), rawRecord, candidateAddr, nil, true)
+ require.NoError(t, err)
+ assert.Equal(t, activeAddr.String(), conn.RemoteAddr().String())
}
-func (ms *memSessStore) Get(key []byte) (Session, error) {
- k := hex.EncodeToString(key)
+func TestLatestCIDControlRecordStartsRRC(t *testing.T) {
+ keyUpdate, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: 0}, Message: &handshake.MessageKeyUpdate{RequestUpdate: handshake.KeyUpdateNotRequested}}).Marshal()
+ require.NoError(t, err)
+ ack, err := (&protocol.ACK{Records: []protocol.RecordNumber{{Epoch: dtlsflight13.EpochApplication, SequenceNumber: 0}}}).Marshal()
+ require.NoError(t, err)
- v, ok := ms.Load(k)
- if !ok {
- return Session{}, nil
+ tests := map[string]struct {
+ contentType protocol.ContentType
+ plaintext []byte
+ }{
+ "ACK": {contentType: protocol.ContentTypeACK, plaintext: ack},
+ "KeyUpdate": {contentType: protocol.ContentTypeHandshake, plaintext: keyUpdate},
}
-
- s, ok := v.(Session)
- if !ok {
- return Session{}, nil
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ testLatestCIDControlRecordStartsRRC(t, test.contentType, test.plaintext)
+ })
}
-
- return s, nil
}
-func (ms *memSessStore) Del(key []byte) error {
- k := hex.EncodeToString(key)
- ms.Delete(k)
+func testLatestCIDControlRecordStartsRRC(
+ t *testing.T,
+ contentType protocol.ContentType,
+ plaintext []byte,
+) {
+ t.Helper()
+ conn, peerProtection := newTestConnWithReadProtection(t)
+ conn.cidPathMigrationPolicy = CIDPathMigrationRRC
+ state, ok := conn.state.(*dtlsstate.State13)
+ require.True(t, ok)
+ localCID := []byte("local-cid")
+ state.CommitNegotiatedExtensions(&negotiation.ConnectionID{ClientCID: localCID, ServerCID: []byte("remote-cid"), ReturnRoutabilityCheck: true})
+ conn.setLocalEpoch(dtlsflight13.EpochApplication)
+ conn.setRemoteEpoch(dtlsflight13.EpochApplication)
+ activeAddr := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 1), Port: 5000}
+ candidateAddr := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 2), Port: 6000}
+ conn.rAddr = activeAddr
+
+ local, peer := packetPipe()
+ defer func() {
+ require.NoError(t, local.Close())
+ require.NoError(t, peer.Close())
+ }()
+ conn.nextConn = netctx.NewPacketConn(local)
+ require.NoError(t, peer.SetReadDeadline(time.Now().Add(time.Second)))
- return nil
-}
+ sealed, err := peerProtection.SealRecord(recordlayer.CiphertextConfig{ConnectionID: localCID, EpochLow: uint8(dtlsflight13.EpochApplication & recordwire.EpochMask)}, 0, contentType, plaintext)
+ require.NoError(t, err)
+ rawRecord, err := sealed.Marshal()
+ require.NoError(t, err)
-// Assert that the server only uses CipherSuites with a hash+signature that matches
-// the certificate. As specified in rfc5246#section-7.4.3
-// .
-func TestCipherSuiteMatchesCertificateType(t *testing.T) { //nolint:cyclop
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
+ type readResult struct {
+ n int
+ err error
+ }
+ read := make(chan readResult, 1)
+ go func() {
+ buf := make([]byte, defaultMTU)
+ n, readErr := peer.Read(buf)
+ read <- readResult{n: n, err: readErr}
+ }()
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
+ _, err = conn.handleIncomingPacket(t.Context(), rawRecord, candidateAddr, nil, true)
+ require.NoError(t, err)
+ result := <-read
+ require.NoError(t, result.err)
+ assert.Positive(t, result.n)
+ assert.Equal(t, activeAddr.String(), conn.RemoteAddr().String())
+}
- for _, test := range []struct {
- Name string
- cipherList []CipherSuiteID
- expectedCipher CipherSuiteID
- generateRSA bool
+func TestRRCRequiresProtectionPolicyAndNegotiation(t *testing.T) {
+ for name, test := range map[string]struct {
+ policy cidPathMigrationPolicy
+ negotiated bool
+ epoch uint16
}{
- {
- Name: "ECDSA Certificate with RSA CipherSuite first",
- cipherList: []CipherSuiteID{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
- expectedCipher: TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- },
- {
- Name: "RSA Certificate with ECDSA CipherSuite first",
- cipherList: []CipherSuiteID{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
- expectedCipher: TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
- generateRSA: true,
- },
+ "Unprotected": {policy: CIDPathMigrationRRC, negotiated: true},
+ "PolicyDisabled": {policy: CIDPathMigrationReject, negotiated: true, epoch: 1},
+ "NotNegotiated": {policy: CIDPathMigrationRRC, epoch: 1},
} {
- t.Run(test.Name, func(t *testing.T) {
- clientErr := make(chan error, 1)
- client := make(chan *Conn, 1)
-
- ca, cb := dpipe.Pipe()
- go func() {
- c, err := testClient(context.TODO(), dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), []ClientOption{
- WithCipherSuites(test.cipherList...),
- }, false)
- clientErr <- err
- client <- c
- }()
-
- var (
- signer crypto.Signer
- err error
+ t.Run(name, func(t *testing.T) {
+ state := dtlsstate.NewState12(true)
+ state.CommitNegotiatedExtensions(&negotiation.ConnectionID{ClientCID: []byte("local-cid"), ServerCID: []byte("remote-cid"), ReturnRoutabilityCheck: test.negotiated})
+ marked := false
+ conn := &Conn{state: &state, cidPathMigrationPolicy: test.policy}
+ _, outcome, err := conn.handleRecordContent(
+ t.Context(),
+ &protocol.ReturnRoutabilityCheck{MessageType: protocol.ReturnRoutabilityCheckPathChallenge},
+ incomingPacketState{
+ number: protocol.RecordNumber{Epoch: uint64(test.epoch)},
+ markPacketAsValid: func() bool {
+ marked = true
+
+ return true
+ },
+ },
+ &net.UDPAddr{},
+ nil,
)
- if test.generateRSA {
- signer, err = rsa.GenerateKey(rand.Reader, 2048)
- assert.NoError(t, err)
- } else {
- signer, err = ecdsa.GenerateKey(cryptoElliptic.P256(), rand.Reader)
- assert.NoError(t, err)
- }
-
- serverCert, err := selfsign.SelfSign(signer)
- assert.NoError(t, err)
-
- s, err := testServer(context.TODO(), dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), []ServerOption{
- WithCipherSuites(test.cipherList...),
- WithCertificates(serverCert),
- }, false)
- assert.NoError(t, err)
- assert.NoError(t, s.Close())
-
- c := <-client
- assert.NoError(t, <-clientErr)
- assert.NoError(t, c.Close())
-
- state, ok := c.ConnectionState()
- assert.True(t, ok)
- assert.Equal(t, test.expectedCipher, state.CipherSuiteID)
+ assert.ErrorIs(t, err, dtlserrors.ErrUnexpectedPostHandshakeMessage)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.UnexpectedMessage}, outcome.responseAlert)
+ assert.False(t, marked)
})
}
}
-// Test that we return the proper certificate if we are serving multiple ServerNames on a single Server.
-func TestMultipleServerCertificates(t *testing.T) {
- fooCert, err := selfsign.GenerateSelfSignedWithDNS("foo")
- assert.NoError(t, err)
-
- barCert, err := selfsign.GenerateSelfSignedWithDNS("bar")
- assert.NoError(t, err)
-
- caPool := x509.NewCertPool()
- for _, cert := range []tls.Certificate{fooCert, barCert} {
- certificate, err := x509.ParseCertificate(cert.Certificate[0])
- assert.NoError(t, err)
- caPool.AddCert(certificate)
- }
-
- for _, test := range []struct {
- RequestServerName string
- ExpectedDNSName string
+func TestWriteRRCRequiresPolicyAndNegotiation(t *testing.T) {
+ for name, test := range map[string]struct {
+ policy cidPathMigrationPolicy
+ negotiated bool
}{
- {
- "foo", //nolint:goconst
- "foo",
- },
- {
- "bar",
- "bar",
- },
- {
- "invalid",
- "foo",
- },
+ "PolicyDisabled": {policy: CIDPathMigrationReject, negotiated: true},
+ "NotNegotiated": {policy: CIDPathMigrationRRC},
} {
- t.Run(test.RequestServerName, func(t *testing.T) {
- clientErr := make(chan error, 2)
- client := make(chan *Conn, 1)
-
- ca, cb := dpipe.Pipe()
- go func() {
- clientConn, err := testClient(context.TODO(), dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), []ClientOption{
- WithRootCAs(caPool),
- WithServerName(test.RequestServerName),
- WithVerifyPeerCertificate(func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
- certificate, err := x509.ParseCertificate(rawCerts[0])
- if err != nil {
- return err
- }
-
- if certificate.DNSNames[0] != test.ExpectedDNSName {
- return errWrongCert
- }
-
- return nil
- }),
- }, false)
- clientErr <- err
- client <- clientConn
- }()
-
- s, err := testServer(context.TODO(), dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), []ServerOption{
- WithCertificates(fooCert, barCert),
- }, false)
- assert.NoError(t, err)
- assert.NoError(t, <-clientErr)
- assert.NoError(t, s.Close())
- assert.NoError(t, (<-client).Close())
+ t.Run(name, func(t *testing.T) {
+ state := dtlsstate.NewState12(true)
+ state.CommitNegotiatedExtensions(&negotiation.ConnectionID{ClientCID: []byte("local-cid"), ServerCID: []byte("remote-cid"), ReturnRoutabilityCheck: test.negotiated})
+ conn := &Conn{state: &state, cidPathMigrationPolicy: test.policy}
+ err := (returnRoutabilityConn{conn: conn}).WriteRRC(t.Context(), &net.UDPAddr{}, protocol.ReturnRoutabilityCheckPathChallenge, [protocol.ReturnRoutabilityCheckCookieLength]byte{})
+ assert.ErrorIs(t, err, dtlserrors.ErrUnexpectedPostHandshakeMessage)
})
}
}
-func TestEllipticCurveConfiguration(t *testing.T) {
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- for _, test := range []struct {
- Name string
- ConfigCurves []elliptic.Curve
- HandshakeCurves []elliptic.Curve
+func TestCIDPathMigrationPolicies(t *testing.T) {
+ activeAddr := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 1), Port: 5000}
+ candidateAddr := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 2), Port: 6000}
+ for name, test := range map[string]struct {
+ policy cidPathMigrationPolicy
+ expectedAddr net.Addr
+ expectedLog string
}{
- {
- Name: "Curve defaulting",
- ConfigCurves: nil,
- HandshakeCurves: defaultCurves,
- },
- {
- Name: "Single curve",
- ConfigCurves: []elliptic.Curve{elliptic.X25519},
- HandshakeCurves: []elliptic.Curve{elliptic.X25519},
+ "Reject": {
+ policy: CIDPathMigrationReject, expectedAddr: activeAddr,
+ expectedLog: "rejected CID path migration",
},
- {
- Name: "Multiple curves",
- ConfigCurves: []elliptic.Curve{elliptic.P384, elliptic.X25519},
- HandshakeCurves: []elliptic.Curve{elliptic.P384, elliptic.X25519},
+ "Unsafe": {policy: CIDPathMigrationUnsafe, expectedAddr: candidateAddr},
+ "RRCNotNegotiated": {
+ policy: CIDPathMigrationRRC, expectedAddr: activeAddr,
+ expectedLog: "RRC was not negotiated",
},
} {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- ca, cb := dpipe.Pipe()
- type result struct {
- c *Conn
- err error
- }
- resultCh := make(chan result)
-
- go func() {
- opts := []ClientOption{WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)}
- if len(test.ConfigCurves) > 0 {
- opts = append(opts, WithEllipticCurves(test.ConfigCurves...))
- }
- client, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), opts, true)
- resultCh <- result{client, err}
- }()
-
- opts := []ServerOption{WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)}
- if len(test.ConfigCurves) > 0 {
- opts = append(opts, WithEllipticCurves(test.ConfigCurves...))
- }
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), opts, true)
- assert.NoError(t, err)
-
- ok := len(test.ConfigCurves) == 0 || len(test.ConfigCurves) == len(test.HandshakeCurves)
- assert.True(t, ok, "Failed to default Elliptic curves")
-
- if len(test.ConfigCurves) != 0 {
- assert.Equal(
- t,
- len(test.HandshakeCurves),
- len(server.fsm.(*handshakeFSM12).cfg.ellipticCurves), //nolint:forcetypeassert
- "Failed to configure Elliptic curves",
+ t.Run(name, func(t *testing.T) {
+ var logs bytes.Buffer
+ conn := &Conn{rAddr: activeAddr, cidPathMigrationPolicy: test.policy, log: logging.NewDefaultLeveledLoggerForScope("dtls", logging.LogLevelError, &logs)}
+ (returnRoutabilityConn{conn: conn}).HandleCandidate(
+ t.Context(), false, true, true, candidateAddr,
)
- for i, c := range test.ConfigCurves {
- assert.Equal(
- t,
- c,
- server.fsm.(*handshakeFSM12).cfg.ellipticCurves[i], //nolint:forcetypeassert
- "Failed to maintain Elliptic curve order",
- )
+ assert.Equal(t, test.expectedAddr.String(), conn.RemoteAddr().String())
+ if test.expectedLog == "" {
+ assert.Empty(t, logs.String())
+ } else {
+ assert.Contains(t, logs.String(), test.expectedLog)
}
- }
-
- res := <-resultCh
- assert.NoError(t, res.err, "Client error")
-
- defer func() {
- assert.NoError(t, server.Close())
- assert.NoError(t, res.c.Close())
- }()
+ })
}
}
-func TestSkipHelloVerify(t *testing.T) {
- report := test.CheckRoutines(t)
- defer report()
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- ca, cb := dpipe.Pipe()
- certificate, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
- gotHello := make(chan struct{})
+func TestOpenCiphertextRecordUsesReadTrafficGeneration(t *testing.T) {
+ conn, state, suite := newTrafficKeyTestConn(t)
+ handshakeSecret := trafficKeyTestSecret(suite, 0x31)
+ applicationSecret := trafficKeyTestSecret(suite, 0x32)
+ handshakeProtection := trafficKeyTestProtection(t, suite, handshakeSecret)
+ applicationProtection := trafficKeyTestProtection(t, suite, applicationSecret)
+
+ state.TrafficKeys.Install(nil, &dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochHandshake, Secret: handshakeSecret, Protection: handshakeProtection})
+ state.TrafficKeys.Install(nil, &dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochApplication, Secret: applicationSecret, Protection: applicationProtection})
+
+ state.SetRemoteEpoch(dtlsflight13.EpochHandshake)
+ handshakeRecord := sealTrafficKeyTestRecord(t, handshakeProtection, dtlsflight13.EpochHandshake, protocol.ContentTypeHandshake, []byte("handshake"))
+ innerPlaintext, sequenceNumber, epoch, err := conn.openCiphertextRecord(handshakeRecord.parsed(t))
+ require.NoError(t, err)
+ assert.Equal(t, uint64(0), sequenceNumber)
+ assert.Equal(t, dtlsflight13.EpochHandshake, epoch)
+ assert.Equal(t, []byte("handshake"), innerPlaintext.Content)
+
+ state.SetRemoteEpoch(dtlsflight13.EpochApplication)
+ applicationRecord := sealTrafficKeyTestRecord(t, applicationProtection, dtlsflight13.EpochApplication, protocol.ContentTypeApplicationData, []byte("application"))
+ innerPlaintext, sequenceNumber, epoch, err = conn.openCiphertextRecord(applicationRecord.parsed(t))
+ require.NoError(t, err)
+ assert.Equal(t, uint64(0), sequenceNumber)
+ assert.Equal(t, dtlsflight13.EpochApplication, epoch)
+ assert.Equal(t, []byte("application"), innerPlaintext.Content)
+
+ wrongDirection := trafficKeyTestProtection(t, suite, trafficKeyTestSecret(suite, 0x33))
+ state.TrafficKeys.Install(nil, &dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochApplication, Secret: trafficKeyTestSecret(suite, 0x33), Protection: wrongDirection})
+ innerPlaintext, sequenceNumber, epoch, err = conn.openCiphertextRecord(applicationRecord.parsed(t))
+ assert.ErrorIs(t, err, errRecordAuthentication)
+ assert.Zero(t, innerPlaintext)
+ assert.Zero(t, sequenceNumber)
+ assert.Zero(t, epoch)
+
+ state.SetRemoteEpoch(4)
+ applicationRecord.Header.EpochLow = 0
+ innerPlaintext, sequenceNumber, epoch, err = conn.openCiphertextRecord(applicationRecord.parsed(t))
+ assert.ErrorIs(t, err, dtlserrors.ErrInvalidEpoch)
+ assert.Zero(t, innerPlaintext)
+ assert.Zero(t, sequenceNumber)
+ assert.Zero(t, epoch)
+}
- go func() {
- server, sErr := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), []ServerOption{
- WithCertificates(certificate),
- WithLoggerFactory(logging.NewDefaultLoggerFactory()),
- WithInsecureSkipVerifyHello(true),
- }, false)
- assert.NoError(t, sErr)
-
- buf := make([]byte, 1024)
- _, sErr = server.Read(buf) //nolint:contextcheck
- assert.NoError(t, sErr)
- gotHello <- struct{}{}
- assert.NoError(t, server.Close()) //nolint:contextcheck
- }()
+func TestOpenCiphertextRecordRetainsPreviousReadGeneration(t *testing.T) {
+ conn, state, suite := newTrafficKeyTestConn(t)
+ previousSecret := trafficKeyTestSecret(suite, 0x41)
+ currentSecret := trafficKeyTestSecret(suite, 0x42)
+ previousProtection := trafficKeyTestProtection(t, suite, previousSecret)
+ currentProtection := trafficKeyTestProtection(t, suite, currentSecret)
+ state.TrafficKeys.Install(nil, &dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochApplication, Generation: 0, Secret: previousSecret, Protection: previousProtection})
+ state.TrafficKeys.Install(nil, &dtlsstate.TrafficGeneration{Epoch: dtlsflight13.EpochApplication + 1, Generation: 1, Secret: currentSecret, Protection: currentProtection})
+ state.SetRemoteEpoch(dtlsflight13.EpochApplication + 1)
+ record := sealTrafficKeyTestRecord(t, previousProtection, dtlsflight13.EpochApplication, protocol.ContentTypeHandshake, []byte("retransmitted KeyUpdate"))
+
+ innerPlaintext, _, epoch, err := conn.openCiphertextRecord(record.parsed(t))
+ require.NoError(t, err)
+ assert.Equal(t, dtlsflight13.EpochApplication, epoch)
+ assert.Equal(t, []byte("retransmitted KeyUpdate"), innerPlaintext.Content)
+}
- client, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), []ClientOption{
- WithLoggerFactory(logging.NewDefaultLoggerFactory()),
- WithInsecureSkipVerify(true),
- }, false)
- assert.NoError(t, err)
+func TestDTLS13SequenceTrackingAfterAuthentication(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ tamper bool
+ nextSequence uint64
+ }{
+ {"authenticated invalid content", false, 65636},
+ {"authentication failure", true, 101},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ conn, peer := newTestConnWithReadProtection(t)
+ receive := func(sequence uint64, contentType protocol.ContentType, tamper bool) bool {
+ record, err := peer.SealRecord(recordlayer.CiphertextConfig{EpochLow: 2}, sequence, contentType, []byte("content"))
+ require.NoError(t, err)
+ if tamper {
+ record.EncryptedRecord[len(record.EncryptedRecord)-1] ^= 1
+ }
+ raw, err := record.Marshal()
+ require.NoError(t, err)
+ prepared, ok, err := conn.prepareIncomingPacket(raw, nil, nil, false)
+ require.NoError(t, err)
+ if ok {
+ assert.Equal(t, []byte("content"), prepared.content)
+ prepared.markPacketAsValid()
+ }
- _, err = client.Write([]byte("hello"))
- assert.NoError(t, err)
+ return ok
+ }
- select {
- case <-gotHello:
- // OK
- case <-time.After(time.Second * 5):
- assert.Fail(t, "timeout")
+ require.True(t, receive(100, protocol.ContentTypeApplicationData, false))
+ require.False(t, receive(32868, protocol.ContentTypeChangeCipherSpec, test.tamper))
+ require.True(t, receive(test.nextSequence, protocol.ContentTypeApplicationData, false))
+ })
}
- assert.NoError(t, client.Close())
}
-type connWithCallback struct {
- net.Conn
- onWrite func([]byte)
+func TestQueueIfCipherSuiteUninitializedUsesReadTrafficGeneration(t *testing.T) {
+ conn, state, suite := newTrafficKeyTestConn(t)
+ for _, epoch := range []uint64{dtlsflight13.EpochHandshake, dtlsflight13.EpochApplication} {
+ //nolint:gosec //G115
+ state.TrafficKeys.Install(nil, &dtlsstate.TrafficGeneration{Epoch: epoch, Secret: trafficKeyTestSecret(suite, byte(epoch)), Protection: trafficKeyTestProtection(t, suite, trafficKeyTestSecret(suite, byte(epoch)))})
+ state.SetRemoteEpoch(epoch)
+ assert.False(t, conn.queueIfCipherSuiteUninitialized(nil, nil, nil, "traffic key available"))
+ }
}
-func (c *connWithCallback) Write(b []byte) (int, error) {
- if c.onWrite != nil {
- c.onWrite(b)
- }
+func newTrafficKeyTestConn(t *testing.T) (*Conn, *dtlsstate.State13, cryptosuite.TrafficSuite) {
+ t.Helper()
- return c.Conn.Write(b)
+ suite := trafficSuiteForTest(t)
+ state := &dtlsstate.State13{Common: &dtlsstate.Common{IsClient: true, LocalVersion: protocol.Version1_3, CipherSuite: suite}, TrafficKeys: &dtlsstate.TrafficKeyState{}}
+
+ return &Conn{state: state}, state, suite
}
-func TestApplicationDataQueueLimited(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
+func trafficSuiteForTest(t *testing.T) cryptosuite.TrafficSuite {
+ t.Helper()
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
+ suite, ok := ciphersuite.ForID(cryptosuite.TLS_AES_128_GCM_SHA256).(cryptosuite.TrafficSuite)
+ require.True(t, ok)
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
+ return suite
+}
- ca, cb := dpipe.Pipe()
- defer func() {
- assert.NoError(t, ca.Close())
- }()
- defer func() {
- assert.NoError(t, cb.Close())
- }()
+func trafficKeyTestSecret(suite cryptosuite.TrafficSuite, value byte) []byte {
+ return bytes.Repeat([]byte{value}, suite.HashFunc()().Size())
+}
- done := make(chan struct{})
- go func() {
- serverCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
+func trafficKeyTestProtection(
+ t *testing.T,
+ suite cryptosuite.TrafficSuite,
+ secret []byte,
+) *testRecordProtection13 {
+ t.Helper()
- dconn, err := ServerWithOptions(dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), WithCertificates(serverCert))
- assert.NoError(t, err)
+ protection, err := newTestRecordProtection13(suite, secret)
+ require.NoError(t, err)
- go func() {
- for range 5 {
- dconn.lock.RLock()
- qlen := len(dconn.encryptedPackets)
- dconn.lock.RUnlock()
- assert.GreaterOrEqual(t, maxAppDataPacketQueueSize, qlen, "too many encrypted packets enqueued")
- time.Sleep(1 * time.Second)
- }
- }()
- assert.Error(t, dconn.HandshakeContext(ctx))
- close(done)
- }()
- extensions := []extension.Extension{}
+ return protection
+}
- time.Sleep(50 * time.Millisecond)
+func sealTrafficKeyTestRecord(t *testing.T, protection *testRecordProtection13, epoch uint64, contentType protocol.ContentType, plaintext []byte) sealedTestRecord {
+ t.Helper()
- assert.NoError(t, sendClientHello([]byte{}, ca, 0, extensions))
+ record, err := protection.SealRecord(recordlayer.CiphertextConfig{EpochLow: uint8(epoch & recordwire.EpochMask), TwoByteSequence: true}, 0, contentType, plaintext)
+ require.NoError(t, err)
- time.Sleep(50 * time.Millisecond)
+ return record
+}
- for i := range 1000 {
- // Send an application data packet
- packet, err := (&recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- SequenceNumber: uint64(3),
- Epoch: 1, // use an epoch greater than 0
- },
- Content: &protocol.ApplicationData{
- Data: []byte{1, 2, 3, 4},
- },
- }).Marshal()
- assert.NoError(t, err)
- _, err = ca.Write(packet)
- assert.NoError(t, err)
- if i%100 == 0 {
- time.Sleep(10 * time.Millisecond)
- }
- }
- time.Sleep(1 * time.Second)
- assert.NoError(t, ca.Close())
- <-done
+func assertTrafficKeyTestRecord(
+ t *testing.T,
+ protection *testRecordProtection13,
+ rawRecord []byte,
+ want []byte,
+) {
+ t.Helper()
+
+ record := unmarshalCiphertextRecordForTest(t, rawRecord, 0)
+ innerPlaintext, err := protection.OpenRecord(record.Header, 0, record.EncryptedRecord)
+ require.NoError(t, err)
+ assert.Equal(t, want, innerPlaintext.Content)
}
-func TestHelloRandom(t *testing.T) {
- report := test.CheckRoutines(t)
- defer report()
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
+func TestDetachedConnWritesRRCAsAddressedEvent(t *testing.T) {
+ conn, _ := newTestConnWithReadProtection(t)
+ conn.cidPathMigrationPolicy = CIDPathMigrationRRC
+ state, ok := conn.state.(*dtlsstate.State13)
+ require.True(t, ok)
+ state.CommitNegotiatedExtensions(&negotiation.ConnectionID{
+ ClientCID: []byte("local-cid"),
+ ServerCID: []byte("remote-cid"),
+ ReturnRoutabilityCheck: true,
+ })
+ conn.setLocalEpoch(dtlsflight13.EpochApplication)
+ addr := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 1), Port: 5000}
+ conn.rAddr = addr
+ detached := &DetachedConn{}
+ conn.detached = detached
+
+ require.NoError(t, (returnRoutabilityConn{conn: conn}).WriteRRC(
+ t.Context(), addr, protocol.ReturnRoutabilityCheckPathChallenge,
+ [protocol.ReturnRoutabilityCheckCookieLength]byte{},
+ ))
+ event := detached.NextEvent()
+ require.Equal(t, DetachedWriteDatagrams, event.Kind)
+ require.Equal(t, addr.String(), event.Addr.String())
+ require.Len(t, event.Datagrams, 1)
+}
- ca, cb := dpipe.Pipe()
- certificate, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
- gotHello := make(chan struct{})
+func TestDetachedConnEventReadyCoalesces(t *testing.T) {
+ conn := &DetachedConn{eventReady: make(chan struct{}, 1)}
+ conn.publishEvent(DetachedEvent{Kind: DetachedApplicationData})
+ conn.publishEvent(DetachedEvent{Kind: DetachedHandshakeDone})
- chRandom := [handshake.RandomBytesLength]byte{}
- _, err = rand.Read(chRandom[:])
- assert.NoError(t, err)
+ requireDetachedEventReady(t, conn)
+ select {
+ case <-conn.EventReady():
+ require.Fail(t, "multiple readiness signals for one nonempty queue")
+ default:
+ }
+ require.Equal(t, DetachedApplicationData, conn.NextEvent().Kind)
+ require.Equal(t, DetachedHandshakeDone, conn.NextEvent().Kind)
+ require.Equal(t, DetachedNoEvent, conn.NextEvent().Kind)
- go func() {
- server, sErr := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), []ServerOption{
- WithGetCertificate(func(chi *ClientHelloInfo) (*tls.Certificate, error) {
- if len(chi.CipherSuites) == 0 {
- return &certificate, nil
- }
- assert.Equal(t, chRandom[:], chi.RandomBytes[:])
+ conn.publishEvent(DetachedEvent{Kind: DetachedApplicationData})
+ require.Equal(t, DetachedApplicationData, conn.NextEvent().Kind)
+ require.Equal(t, DetachedNoEvent, conn.NextEvent().Kind)
+ select {
+ case <-conn.EventReady():
+ require.Fail(t, "stale readiness signal after draining events")
+ default:
+ }
- return &certificate, nil
- }),
- WithLoggerFactory(logging.NewDefaultLoggerFactory()),
- }, false)
- assert.NoError(t, sErr)
+ conn.publishEvent(DetachedEvent{Kind: DetachedHandshakeDone})
+ requireDetachedEventReady(t, conn)
+}
- buf := make([]byte, 1024)
- _, sErr = server.Read(buf) //nolint:contextcheck
- assert.NoError(t, sErr)
+func TestDetachedConnValidInputCancelsPendingRetransmit(t *testing.T) { //nolint:cyclop
+ certificate, err := selfsign.GenerateSelfSigned()
+ require.NoError(t, err)
+ clientAddr := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 1), Port: 4444}
+ serverAddr := &net.UDPAddr{IP: net.IPv4(192, 0, 2, 2), Port: 5555}
- gotHello <- struct{}{}
- assert.NoError(t, server.Close()) //nolint:contextcheck
- }()
+ for name, version := range map[string]protocol.Version{"DTLS12": protocol.Version1_2, "DTLS13": protocol.Version1_3} {
+ t.Run(name, func(t *testing.T) {
+ client, err := DetachedClient(serverAddr,
+ WithInsecureSkipVerify(true),
+ WithMinVersion(version),
+ WithMaxVersion(version),
+ WithFlightInterval(time.Hour),
+ )
+ require.NoError(t, err)
+ server, err := DetachedServer(clientAddr,
+ WithCertificates(certificate),
+ WithMinVersion(version),
+ WithMaxVersion(version),
+ WithFlightInterval(time.Hour),
+ )
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ _ = client.Close()
+ _ = server.Close()
+ })
- client, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), []ClientOption{
- WithLoggerFactory(logging.NewDefaultLoggerFactory()),
- WithHelloRandomBytesGenerator(func() [handshake.RandomBytesLength]byte {
- return chRandom
- }),
- WithInsecureSkipVerify(true),
- }, false)
- assert.NoError(t, err)
+ timers := make(chan *detachedTimer, 8)
+ client.conn.handshakeConfig.TimerFactory = func(d time.Duration) dtlsconfig.Timer {
+ timer := client.newTimer(d).(*detachedTimer) //nolint:forcetypeassert // test factory always returns this type
+ timers <- timer
- _, err = client.Write([]byte("hello"))
- assert.NoError(t, err)
+ return timer
+ }
- select {
- case <-gotHello:
- // OK
- case <-time.After(time.Second * 5):
- assert.Fail(t, "timeout")
- }
+ require.NoError(t, server.Start(t.Context()))
+ require.NoError(t, client.Start(t.Context()))
- assert.NoError(t, client.Close())
-}
+ var clientFlight [][]byte
+ for event := client.NextEvent(); event.Kind != DetachedNoEvent; event = client.NextEvent() {
+ if event.Kind == DetachedWriteDatagrams {
+ clientFlight = append(clientFlight, event.Datagrams...)
+ }
+ }
+ require.NotEmpty(t, clientFlight)
+ initialTimer := <-timers
-func TestOnConnectionAttempt(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*20)
- defer cancel()
+ // Keep the valid input operation ahead of the pending timer callback.
+ // Stop must claim the timer while fire is waiting for driveMu.
+ client.driveMu.Lock()
+ locked := true
+ defer func() {
+ if locked {
+ client.driveMu.Unlock()
+ }
+ }()
- var clientOnConnectionAttempt, serverOnConnectionAttempt atomic.Int32
+ for _, datagram := range clientFlight {
+ require.NoError(t, server.HandleDatagram(datagram, clientAddr))
+ }
+ var serverFlight [][]byte
+ for event := server.NextEvent(); event.Kind != DetachedNoEvent; event = server.NextEvent() {
+ if event.Kind == DetachedWriteDatagrams {
+ serverFlight = append(serverFlight, event.Datagrams...)
+ }
+ }
+ require.NotEmpty(t, serverFlight)
- ca, cb := dpipe.Pipe()
- clientErr := make(chan error, 1)
- go func() {
- _, err := testClient(ctx, dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), nil, true)
- clientErr <- err
- }()
+ fireDone := make(chan struct{})
+ go func() {
+ initialTimer.fire()
+ close(fireDone)
+ }()
+ for _, datagram := range serverFlight {
+ select {
+ case client.inbound <- addrPkt{rAddr: serverAddr, data: datagram}:
+ case <-client.terminal:
+ require.NoError(t, client.terminalErr)
+ }
+ require.NoError(t, client.waitUntilBlocked())
+ }
+ require.True(t, initialTimer.claimed.Load())
- expectedErr := errConnectionAttemptFailed
- _, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), []ServerOption{
- WithOnConnectionAttempt(func(in net.Addr) error {
- serverOnConnectionAttempt.Store(1)
- assert.NotNil(t, in)
+ immediateWrites := 0
+ for event := client.NextEvent(); event.Kind != DetachedNoEvent; event = client.NextEvent() {
+ if event.Kind == DetachedWriteDatagrams {
+ immediateWrites++
+ }
+ }
+ require.Positive(t, immediateWrites)
- return expectedErr
- }),
- }, true)
- assert.ErrorIs(t, err, expectedErr)
- assert.Error(t, <-clientErr)
- assert.Equal(t, int32(1), serverOnConnectionAttempt.Load(), "onConnectionAttempt did not fire for server")
- assert.Equal(t, int32(0), clientOnConnectionAttempt.Load(), "onConnectionAttempt fired for client")
+ client.driveMu.Unlock()
+ locked = false
+ select {
+ case <-fireDone:
+ case <-time.After(time.Second):
+ require.FailNow(t, "timer callback remained blocked after valid input")
+ }
+ select {
+ case <-client.EventReady():
+ require.Fail(t, "canceled retransmit produced an event")
+ default:
+ }
+ })
+ }
}
-func TestFragmentBuffer_Retransmission(t *testing.T) {
- fragmentBuffer := newFragmentBuffer()
- frag := []byte{
- 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x30, 0x03, 0x00,
- 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, 0x01, 0x01,
+func requireDetachedEventReady(t *testing.T, conn *DetachedConn) {
+ t.Helper()
+ timer := time.NewTimer(2 * time.Second)
+ defer timer.Stop()
+ select {
+ case <-conn.EventReady():
+ case <-timer.C:
+ require.FailNow(t, "timed out waiting for a detached event")
}
+}
- _, isRetransmission, err := fragmentBuffer.push(frag)
- assert.NoError(t, err)
- assert.False(t, isRetransmission)
-
- v, _ := fragmentBuffer.pop()
- assert.NotNil(t, v)
+// sealedTestRecord holds encoder inputs for deterministic wire mutations in tests.
+type sealedTestRecord struct {
+ Header recordlayer.CiphertextConfig
+ EncryptedRecord []byte
+}
- _, isRetransmission, err = fragmentBuffer.push(frag)
- assert.NoError(t, err)
- assert.True(t, isRetransmission)
+func (r sealedTestRecord) Marshal() ([]byte, error) {
+ return recordlayer.MarshalCiphertext(r.Header, r.EncryptedRecord)
}
-func TestConnectionState(t *testing.T) {
- ca, cb := dpipe.Pipe()
+func (r sealedTestRecord) parsed(t *testing.T) recordlayer.ParsedRecord {
+ t.Helper()
+ raw, err := r.Marshal()
+ require.NoError(t, err)
+ parsed, err := recordlayer.ParseRecord(raw, len(r.Header.ConnectionID))
+ require.NoError(t, err)
- // Setup client
- clientCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
+ return parsed
+}
- client, err := ClientWithOptions(
- dtlsnet.PacketConnFromConn(ca),
- ca.RemoteAddr(),
- WithCertificates(clientCert),
- WithInsecureSkipVerify(true),
+func TestHandleIncomingPacketControlRecordEpoch(t *testing.T) {
+ t.Cleanup(test.CheckRoutines(t))
+ defer test.TimeOut(10 * time.Second).Stop()
+
+ certificate, err := selfsign.GenerateSelfSigned()
+ require.NoError(t, err)
+ client, server := handshakePair(t,
+ []ClientOption{WithInsecureSkipVerify(true), WithMaxVersion(protocol.Version1_2)},
+ []ServerOption{WithCertificates(certificate), WithMaxVersion(protocol.Version1_2)},
)
- assert.NoError(t, err)
- defer func() {
- _ = client.Close()
- }()
+ for _, result := range []handshakeResult{client, server} {
+ require.NoError(t, result.configErr)
+ require.NoError(t, result.handshakeError)
+ }
- _, ok := client.ConnectionState()
- assert.False(t, ok)
+ for _, testCase := range []struct {
+ name string
+ version protocol.Version
+ remoteEpoch uint64
+ recordEpoch uint64
+ content protocol.Content
+ }{
+ {name: "DTLS12/close_after_handshake", version: protocol.Version1_2, remoteEpoch: 1, content: &alert.Alert{Level: alert.Warning, Description: alert.CloseNotify}},
+ {name: "DTLS13/alert_after_keys", version: protocol.Version1_3, remoteEpoch: 2, content: &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}},
+ {name: "ACK/before_keys", version: protocol.Version1_3, content: &protocol.ACK{Records: []protocol.RecordNumber{{Epoch: 2}}}},
+ {name: "ACK/after_keys", version: protocol.Version1_3, remoteEpoch: 3, content: &protocol.ACK{Records: []protocol.RecordNumber{{Epoch: 2}}}},
+ {name: "ACK/mixed_epochs", version: protocol.Version1_3, remoteEpoch: 3, recordEpoch: 2, content: &protocol.ACK{Records: []protocol.RecordNumber{{Epoch: 2}, {Epoch: 3}}}},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ conn, peerProtection := newTestConnWithReadProtection(t)
+ common := dtlsstate.CommonState(conn.state)
+ common.LocalVersion = testCase.version
+ if testCase.version == protocol.Version1_2 {
+ conn.state = &dtlsstate.State12{Common: common}
+ conn.handshakeEstablished = client.conn.handshakeEstablished
+ }
+ conn.setRemoteEpoch(testCase.remoteEpoch)
+ raw, err := marshalTestRecord(recordlayer.RecordConfig{Version: protocol.Version1_2}, testCase.content)
+ require.NoError(t, err)
+ if testCase.recordEpoch != 0 {
+ plaintext, marshalErr := testCase.content.Marshal()
+ require.NoError(t, marshalErr)
+ sealed, sealErr := peerProtection.SealRecord(recordlayer.CiphertextConfig{
+ EpochLow: uint8(testCase.recordEpoch & recordwire.EpochMask),
+ }, 0, testCase.content.ContentType(), plaintext)
+ require.NoError(t, sealErr)
+ raw, err = sealed.Marshal()
+ require.NoError(t, err)
+ }
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- errorChannel := make(chan error)
- go func() {
- errC := client.HandshakeContext(ctx)
- errorChannel <- errC
- }()
+ outcome, err := conn.handleIncomingPacket(t.Context(), raw, nil, nil, false)
+ require.NoError(t, err)
+ assert.Equal(t, packetOutcome{}, outcome)
+ if detector := common.ReplayDetector[testCase.recordEpoch]; detector != nil {
+ _, acceptable := detector.Check(0)
+ assert.True(t, acceptable, "discarded records must not consume replay sequence numbers")
+ }
+ })
+ }
+}
- // Setup server
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), nil, true)
- assert.NoError(t, err)
+type limitedTrafficSuite struct {
+ cryptosuite.TrafficSuite
+ limits cryptosuite.UsageLimits
+}
- defer func() {
- _ = server.Close()
- }()
+func (s *limitedTrafficSuite) ID() cryptosuite.ID { return 0xffa8 }
+func (s *limitedTrafficSuite) UsageLimits() cryptosuite.UsageLimits { return s.limits }
- err = <-errorChannel
- assert.NoError(t, err)
+func trafficSuiteWithLimits(t *testing.T, seals, failures uint64) *limitedTrafficSuite {
+ t.Helper()
- _, ok = client.ConnectionState()
- assert.True(t, ok)
+ return &limitedTrafficSuite{
+ TrafficSuite: trafficSuiteForTest(t),
+ limits: cryptosuite.UsageLimits{MaxSealedRecords: seals, MaxAuthenticationFailures: failures},
+ }
}
-func TestMultiHandshake(t *testing.T) {
- defer test.CheckRoutines(t)()
- defer test.TimeOut(time.Second * 10).Stop()
-
- ca, cb := dpipe.Pipe()
- serverCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
+type limitedConnectionSuite struct {
+ cryptosuite.ConnectionSuite
+}
- server, err := ServerWithOptions(dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), WithCertificates(serverCert))
- assert.NoError(t, err)
+func (s *limitedConnectionSuite) ID() cryptosuite.ID { return 0xffa9 }
- go func() {
- _ = server.Handshake()
- }()
+func (s *limitedConnectionSuite) UsageLimits() cryptosuite.UsageLimits {
+ return cryptosuite.UsageLimits{MaxSealedRecords: 8, MaxAuthenticationFailures: 2}
+}
- clientCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
+func limitedTrafficPair(t *testing.T, version protocol.Version) (*Conn, *Conn) {
+ t.Helper()
+ var suite cryptosuite.Suite = trafficSuiteWithLimits(t, 8, 2)
+ if version == protocol.Version1_2 {
+ connectionSuite, ok := ciphersuite.ForID(cryptosuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256).(cryptosuite.ConnectionSuite)
+ require.True(t, ok)
+ suite = &limitedConnectionSuite{ConnectionSuite: connectionSuite}
+ }
+ certificate, err := selfsign.GenerateSelfSigned()
+ require.NoError(t, err)
+ custom := WithCustomCipherSuites(func() []cryptosuite.Suite { return []cryptosuite.Suite{suite} })
+ client, server := handshakePair(t,
+ []ClientOption{WithMinVersion(version), WithMaxVersion(version), WithInsecureSkipVerify(true), WithCipherSuites(suite.ID()), custom},
+ []ServerOption{WithMinVersion(version), WithMaxVersion(version), WithCertificates(certificate), WithCipherSuites(suite.ID()), custom})
+ require.NoError(t, client.configErr)
+ require.NoError(t, server.configErr)
+ require.NoError(t, client.handshakeError)
+ require.NoError(t, server.handshakeError)
+
+ return client.conn, server.conn
+}
- client, err := ClientWithOptions(dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), WithCertificates(clientCert))
- assert.NoError(t, err)
- assert.Error(t, client.Handshake())
- assert.Error(t, client.Handshake())
- assert.NoError(t, server.Close())
- assert.NoError(t, client.Close())
+func TestConnectionKeyUsage(t *testing.T) {
+ for _, version := range []protocol.Version{protocol.Version1_2, protocol.Version1_3} {
+ t.Run(fmt.Sprintf("%x", version), func(t *testing.T) {
+ client, server := limitedTrafficPair(t, version)
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ require.NoError(t, client.SetWriteDeadline(time.Now().Add(5*time.Second)))
+ require.NoError(t, server.SetReadDeadline(time.Now().Add(5*time.Second)))
+ before, ok := client.ConnectionState()
+ require.True(t, ok)
+ require.NotNil(t, before.KeyUsage)
+ buffer := make([]byte, 32)
+ for range 12 {
+ _, err := client.Write([]byte("payload"))
+ require.NoError(t, err)
+ _, err = server.Read(buffer)
+ require.NoError(t, err)
+ }
+ after, ok := client.ConnectionState()
+ require.True(t, ok)
+ require.GreaterOrEqual(t, after.KeyUsage.SealedRecords, before.KeyUsage.SealedRecords+12)
+ require.Equal(t, uint64(8), after.KeyUsage.RecommendedLimits.MaxSealedRecords)
+ require.Zero(t, after.KeyUsage.RemainingSealedRecords)
+ require.Less(t, before.KeyUsage.SealedRecords, after.KeyUsage.SealedRecords, "snapshots must not change")
+
+ packet := &dtlsflight.Outbound{Epoch: after.KeyUsage.WriteEpoch, Content: &protocol.ApplicationData{Data: []byte("payload")}, Protection: dtlsflight.ProtectionCiphertext}
+ client.writeLock.Lock()
+ datagrams, address, err := client.prepareRawPacketsTracked([]*dtlsflight.Outbound{packet})
+ client.writeLock.Unlock()
+ require.NoError(t, err)
+ require.Len(t, datagrams, 1)
+ raw := datagrams[0].raw
+ raw[len(raw)-1] ^= 1
+ for range 3 {
+ _, err = client.nextConn.WriteToContext(ctx, raw, address)
+ require.NoError(t, err)
+ }
+ _, err = client.Write([]byte("still connected"))
+ require.NoError(t, err)
+ n, err := server.Read(buffer)
+ require.NoError(t, err)
+ require.Equal(t, "still connected", string(buffer[:n]))
+ received, ok := server.ConnectionState()
+ require.True(t, ok)
+ require.Equal(t, uint64(3), received.KeyUsage.AuthenticationFailures)
+ require.Equal(t, uint64(2), received.KeyUsage.RecommendedLimits.MaxAuthenticationFailures)
+ require.Zero(t, received.KeyUsage.RemainingAuthenticationFailures)
+ require.False(t, client.isConnectionClosed())
+ require.False(t, server.isConnectionClosed())
+
+ if version == protocol.Version1_3 {
+ require.NoError(t, client.UpdateKeys(ctx, KeyUpdateOptions{}))
+ updated, ok := client.ConnectionState()
+ require.True(t, ok)
+ require.Equal(t, after.KeyUsage.WriteEpoch+1, updated.KeyUsage.WriteEpoch)
+ require.Positive(t, updated.KeyUsage.RemainingSealedRecords)
+ }
+ })
+ }
}
-func TestCloseDuringHandshake(t *testing.T) {
- defer test.CheckRoutines(t)()
- defer test.TimeOut(time.Second * 10).Stop()
+func TestEarlyApplicationRecords(t *testing.T) {
+ client, clientState, suite := newTrafficKeyTestConn(t)
+ server, serverState, _ := newTrafficKeyTestConn(t)
+ serverState.IsClient = false
+ clientState.CipherSuite, serverState.CipherSuite = nil, nil
+ server.log = logging.NewDefaultLoggerFactory().NewLogger("dtls")
+ server.replayProtectionWindow = defaultReplayProtectionWindow
+ server.detached = &DetachedConn{eventReady: make(chan struct{}, 1)}
+ serverState.EarlyDataStatus, serverState.EarlyDataLimit = dtlsstate.EarlyDataAccepted, 4096
+ hello, err := (&handshake.Handshake{Message: &handshake.MessageClientHello{
+ Version: protocol.Version1_2, CipherSuiteIDs: []uint16{uint16(suite.ID())},
+ }}).Marshal()
+ require.NoError(t, err)
+ psk := bytes.Repeat([]byte{0x42}, 32)
+ require.NoError(t, dtlshandshake.InitEarlyRecordProtection(clientState, suite, psk, hello, nil))
+ require.NoError(t, dtlshandshake.InitEarlyRecordProtection(serverState, suite, psk, hello, nil))
+ _, hasClientRead := clientState.TrafficKeys.Read(dtlsflight13.EpochEarlyData)
+ _, hasServerWrite := serverState.TrafficKeys.Write(dtlsflight13.EpochEarlyData)
+ assert.False(t, hasClientRead)
+ assert.False(t, hasServerWrite)
+ assert.Nil(t, clientState.CipherSuite)
+ assert.Nil(t, serverState.CipherSuite)
+ assert.Zero(t, serverState.RemoteEpoch())
+
+ raw, err := client.sealRecordContent(dtlsflight13.EpochEarlyData, 0, protocol.ContentTypeApplicationData, []byte("early"))
+ require.NoError(t, err)
+ prepared, ok, err := server.prepareCiphertextPacket(raw, nil, nil, false)
+ require.NoError(t, err)
+ require.True(t, ok)
+ assert.Equal(t, dtlsflight13.EpochEarlyData, prepared.number.Epoch)
+ _, _, err = server.handleApplicationDataRecord(t.Context(), &protocol.ApplicationData{Data: prepared.content}, prepared)
+ require.NoError(t, err)
+ assert.Equal(t, []byte("early"), server.detached.NextEvent().Data)
+ _, ok, err = server.prepareCiphertextPacket(raw, nil, nil, false)
+ require.NoError(t, err)
+ assert.False(t, ok, "duplicate early records must be discarded")
+ _, err = server.sealRecordContent(dtlsflight13.EpochEarlyData, 0, protocol.ContentTypeApplicationData, nil)
+ require.ErrorIs(t, err, dtlserrors.ErrInvalidContentType)
+ _, err = client.sealRecordContent(dtlsflight13.EpochEarlyData, 1, protocol.ContentTypeHandshake, nil)
+ require.ErrorIs(t, err, dtlserrors.ErrInvalidContentType)
+
+ serverState.CipherSuite = ciphersuite.ForID(cryptosuite.TLS_AES_256_GCM_SHA384)
+ serverState.SetRemoteEpoch(dtlsflight13.EpochHandshake)
+ parsed, err := recordlayer.ParseRecord(raw, 0)
+ require.NoError(t, err)
+ opened, _, epoch, err := server.openCiphertextRecord(parsed)
+ require.NoError(t, err)
+ assert.Equal(t, dtlsflight13.EpochEarlyData, epoch)
+ assert.Equal(t, []byte("early"), opened.Content)
+}
- serverCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
+type earlyDataTestStore struct {
+ sync.Mutex
+ session Session
+ claimed bool
+ claimErr error
+}
- for range 100 {
- _, cb := dpipe.Pipe()
- server, err := ServerWithOptions(dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), WithCertificates(serverCert))
- assert.NoError(t, err)
+func (s *earlyDataTestStore) Set([]byte, Session) error { return nil }
+func (s *earlyDataTestStore) Del([]byte) error { return nil }
- waitChan := make(chan struct{})
- go func() {
- close(waitChan)
- _ = server.Handshake()
- }()
+func (s *earlyDataTestStore) Get([]byte) (Session, error) {
+ s.Lock()
+ defer s.Unlock()
- <-waitChan
- assert.NoError(t, server.Close())
- }
+ return s.session, nil
}
-func TestCloseWithoutHandshake(t *testing.T) {
- defer test.CheckRoutines(t)()
- defer test.TimeOut(time.Second * 10).Stop()
-
- serverCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
+func (s *earlyDataTestStore) Claim(_ []byte, _ time.Time) (bool, error) {
+ s.Lock()
+ defer s.Unlock()
+ if s.claimErr != nil {
+ return false, s.claimErr
+ }
+ if s.claimed {
+ return false, nil
+ }
+ s.claimed = true
- _, cb := dpipe.Pipe()
- server, err := ServerWithOptions(dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), WithCertificates(serverCert))
- assert.NoError(t, err)
- assert.NoError(t, server.Close())
+ return true, nil
}
-// WIP! Tests if DTLS 1.3 handshake flow is enabled and the correct error is returned.
-func TestDTLS13Enabled(t *testing.T) {
- ca, cb := dpipe.Pipe()
-
- // Setup client
- clientCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
+const earlyDataSRTPChanged = "srtp changed"
- client, err := ClientWithOptions(
- dtlsnet.PacketConnFromConn(ca),
- ca.RemoteAddr(),
- WithCertificates(clientCert),
- WithInsecureSkipVerify(true),
- WithMinVersion(protocol.Version1_3),
- WithMaxVersion(protocol.Version1_3),
- )
- assert.NoError(t, err)
- defer func() {
- _ = client.Close()
- }()
+func TestDetachedEarlyDataNegotiation(t *testing.T) {
+ certificate, err := selfsign.GenerateSelfSigned()
+ require.NoError(t, err)
+ for _, scenario := range []string{"accepted", "replay", "policy error", "no policy", "retry", "stale age", "expired", "context changed", "reduced limit", "cid", "srtp", earlyDataSRTPChanged, "alpn", "alpn changed", "alpn removed"} {
+ t.Run(scenario, func(t *testing.T) {
+ ticket := Session{Version: 1, IsClient: true, ID: []byte("ticket"), Secret: bytes.Repeat([]byte{0x42}, 32), Ticket: &SessionTicket{
+ CipherSuite: cryptosuite.TLS_AES_128_GCM_SHA256, CreatedAt: time.Now().Add(-time.Second),
+ Lifetime: 60, AgeAdd: ^uint32(0), MaxEarlyDataSize: 32, PeerCertificates: certificate.Certificate,
+ }}
+ srtpOption := earlyDataSRTPOption(scenario)
+ clientALPN, serverALPN := earlyDataALPNOptions(scenario, ticket.Ticket)
+ clientStore := &earlyDataTestStore{session: ticket}
+ serverTicket := *ticket.Ticket
+ ticket.Ticket = &serverTicket
+ ticket.IsClient = false
+ serverStore := &earlyDataTestStore{session: ticket, claimed: scenario == "replay"}
+ serverTicket.CreatedAt = serverTicket.CreatedAt.Add(-map[string]time.Duration{
+ "stale age": 29 * time.Second, "expired": time.Minute,
+ }[scenario])
+ if scenario == "context changed" {
+ serverTicket.NegotiatedProtocol = "previous-protocol"
+ }
+ if scenario == "policy error" {
+ serverStore.claimErr = context.DeadlineExceeded
+ }
+ client, err := DetachedClient(&net.UDPAddr{Port: 5555}, srtpOption, clientALPN, WithSessionStore(clientStore, WithMaxEarlyDataSize(32)),
+ WithInsecureSkipVerify(true), WithMinVersion(protocol.Version1_3), WithMaxVersion(protocol.Version1_3), WithFlightInterval(time.Hour))
+ require.NoError(t, err)
+ limit := uint32(32)
+ if scenario == "no policy" {
+ limit = 0
+ }
+ if scenario == "reduced limit" {
+ limit = 4
+ }
+ server, err := DetachedServer(&net.UDPAddr{Port: 4444}, srtpOption, serverALPN, WithCertificates(certificate), WithSessionStore(serverStore, WithMaxEarlyDataSize(limit)),
+ WithInsecureSkipVerifyHello(scenario != "retry"), WithMinVersion(protocol.Version1_3), WithMaxVersion(protocol.Version1_3), WithFlightInterval(time.Hour))
+ require.NoError(t, err)
+ if scenario == "cid" {
+ client.conn.handshakeConfig.ConnectionIDGenerator = func() []byte { return []byte{1, 2} }
+ server.conn.handshakeConfig.ConnectionIDGenerator = func() []byte { return []byte{3, 4} }
+ client.conn.handshakeConfig.ReceiveCIDLength = 2
+ server.conn.handshakeConfig.ReceiveCIDLength = 2
+ }
+ t.Cleanup(func() { _ = client.Close(); _ = server.Close() })
+ ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
+ defer cancel()
+ require.NoError(t, server.Start(ctx))
+ require.NoError(t, client.Start(ctx))
+ n, err := client.WriteEarlyData([]byte("early"))
+ require.NoError(t, err)
+ require.Equal(t, 5, n)
+ _, err = client.WriteEarlyData(make([]byte, 32))
+ require.ErrorIs(t, err, dtlserrors.ErrInvalidPacketLength)
+ events, data := pumpEarlyDataPair(t, client, server)
+ require.Contains(t, events, DetachedEarlyDataReady)
+ want := DetachedEarlyDataRejected
+ if map[string]bool{"accepted": true, "cid": true, "srtp": true, earlyDataSRTPChanged: true, "alpn": true}[scenario] {
+ want = DetachedEarlyDataAccepted
+ }
+ require.Contains(t, events, want)
+ require.True(t, client.established.Load())
+ require.True(t, server.established.Load())
+ checkEarlyDataSRTP(t, client, server)
+ checkEarlyDataALPN(t, scenario, client, server)
+ if want == DetachedEarlyDataAccepted {
+ assert.Equal(t, []byte("early"), data)
+ } else {
+ assert.Empty(t, data)
+ }
+ _, err = client.Write([]byte("normal"))
+ require.NoError(t, err)
+ _, data = pumpEarlyDataPair(t, client, server)
+ require.Equal(t, []byte("normal"), data)
+ })
+ }
+}
- _, ok := client.ConnectionState()
- assert.False(t, ok)
+func pumpEarlyDataPair(t *testing.T, client, server *DetachedConn) ([]DetachedEventKind, []byte) {
+ t.Helper()
+ var events []DetachedEventKind
+ var data []byte
+ for range 30 {
+ progress := false
+ for _, pair := range [][2]*DetachedConn{{client, server}, {server, client}} {
+ for event := pair[0].NextEvent(); event.Kind != DetachedNoEvent; event = pair[0].NextEvent() {
+ progress = true
+ switch event.Kind {
+ case DetachedWriteDatagrams:
+ for _, packet := range event.Datagrams {
+ require.NoError(t, pair[1].HandleDatagram(packet, nil))
+ }
+ case DetachedApplicationData:
+ data = append(data, event.Data...)
+ default:
+ if pair[0] == client {
+ events = append(events, event.Kind)
+ }
+ }
+ }
+ }
+ if !progress {
+ return events, data
+ }
+ }
+ require.FailNow(t, "detached handshake did not quiesce")
- ctxClient, cancelClient := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancelClient()
- errorChannel := make(chan error)
- go func() {
- errC := client.HandshakeContext(ctxClient)
- errorChannel <- errC
- }()
+ return nil, nil
+}
- err = <-errorChannel
- assert.Error(t, err)
- assert.ErrorIs(t, err, dtlserrors.ErrStateUnimplemented13)
+func TestEarlyDataStoreValidation(t *testing.T) {
+ store := &earlyDataTestStore{}
+ for _, tc := range []struct {
+ name string
+ store SessionStore
+ limit uint32
+ invalid bool
+ }{
+ {"ordinary", struct{ SessionStore }{store}, 0, false},
+ {"claim only", store, 0, false},
+ {"missing claim", struct{ SessionStore }{store}, 32, true},
+ {"missing store", nil, 32, true},
+ {"early data", store, 32, false},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ cfg := &dtlsConfig{}
+ err := WithSessionStore(tc.store, WithMaxEarlyDataSize(tc.limit)).applyServer(cfg)
+ if tc.invalid {
+ require.ErrorIs(t, err, dtlserrors.ErrEarlyDataRequiresClaimStore)
+ } else {
+ require.NoError(t, err)
+ require.NoError(t, validateConfig(cfg))
+ }
+ })
+ }
+}
- // Setup server
- serverCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
+func earlyDataSRTPOption(scenario string) Option {
+ profile := map[string]SRTPProtectionProfile{
+ "srtp": SRTP_AES128_CM_HMAC_SHA1_80, earlyDataSRTPChanged: SRTP_AEAD_AES_128_GCM,
+ }[scenario]
- server, err := ServerWithOptions(
- dtlsnet.PacketConnFromConn(cb),
- cb.RemoteAddr(),
- WithCertificates(serverCert),
- WithInsecureSkipVerify(true),
- WithMinVersion(protocol.Version1_3),
- WithMaxVersion(protocol.Version1_3),
- )
- assert.NoError(t, err)
- defer func() {
- _ = server.Close()
- }()
+ return sharedOption(func(c *dtlsConfig) error {
+ if profile != 0 {
+ c.SRTPProtectionProfiles = []SRTPProtectionProfile{profile}
+ c.SRTPMasterKeyIdentifier = []byte{1, 2}
+ }
- _, ok = server.ConnectionState()
- assert.False(t, ok)
+ return nil
+ })
+}
- ctxServer, cancelServer := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancelServer()
- go func() {
- errS := server.HandshakeContext(ctxServer)
- errorChannel <- errS
- }()
- err = <-errorChannel
- assert.Error(t, err)
- assert.ErrorIs(t, err, dtlserrors.ErrStateUnimplemented13)
+func checkEarlyDataSRTP(t *testing.T, client, server *DetachedConn) {
+ t.Helper()
+ profiles := client.conn.handshakeConfig.LocalSRTPProtectionProfiles
+ if len(profiles) == 0 {
+ return
+ }
+ require.Equal(t, profiles[0], dtlsstate.CommonState(client.conn.state).SRTPProtectionProfile())
+ require.Equal(t, profiles[0], dtlsstate.CommonState(server.conn.state).SRTPProtectionProfile())
+ clientState, _ := client.conn.ConnectionState()
+ serverState, _ := server.conn.ConnectionState()
+ clientKeys, err := clientState.ExportKeyingMaterial("EXTRACTOR-dtls_srtp", nil, 60)
+ require.NoError(t, err)
+ serverKeys, err := serverState.ExportKeyingMaterial("EXTRACTOR-dtls_srtp", nil, 60)
+ require.NoError(t, err)
+ require.Equal(t, clientKeys, serverKeys)
}
-// WIP! Tests if the dual stack mode client managed to negotiate a version successfully.
-func TestDTLSDualStackClient(t *testing.T) {
- defer test.CheckRoutines(t)()
- defer test.TimeOut(time.Second * 10).Stop()
+const earlyDataALPN = "old"
+
+func earlyDataALPNOptions(scenario string, ticket *SessionTicket) (Option, Option) {
+ clientProtocols, serverProtocols := []string(nil), []string(nil)
+ switch scenario {
+ case "alpn":
+ ticket.NegotiatedProtocol = earlyDataALPN
+ clientProtocols, serverProtocols = []string{earlyDataALPN}, []string{earlyDataALPN}
+ case "alpn changed":
+ ticket.NegotiatedProtocol = earlyDataALPN
+ clientProtocols, serverProtocols = []string{earlyDataALPN, "new"}, []string{"new", earlyDataALPN}
+ case "alpn removed":
+ ticket.NegotiatedProtocol = earlyDataALPN
+ clientProtocols = []string{earlyDataALPN}
+ }
- // Setup client
- clientCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
+ return valueOption(func(c *dtlsConfig) *[]string { return &c.SupportedProtocols }, clientProtocols),
+ valueOption(func(c *dtlsConfig) *[]string { return &c.SupportedProtocols }, serverProtocols)
+}
- clientOpts := []ClientOption{
- WithCertificates(clientCert),
- WithInsecureSkipVerify(true),
- WithMinVersion(protocol.Version1_2),
- WithMaxVersion(protocol.Version1_3),
- }
+func checkEarlyDataALPN(t *testing.T, scenario string, client, server *DetachedConn) {
+ t.Helper()
+ expected := map[string]string{"alpn": earlyDataALPN, "alpn changed": "new"}[scenario]
+ require.Equal(t, expected, dtlsstate.CommonState(client.conn.state).NegotiatedProtocol)
+ require.Equal(t, expected, dtlsstate.CommonState(server.conn.state).NegotiatedProtocol)
+}
- // Setup server
- serverCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
+type deadlineTestTransport struct {
+ netctx.PacketConn
+ started chan error
+ blockWrite bool
+}
- serverOpts := []ServerOption{
- WithCertificates(serverCert),
- WithInsecureSkipVerify(true),
- WithMinVersion(protocol.Version1_2),
- WithMaxVersion(protocol.Version1_2),
+func (p *deadlineTestTransport) ReadFromContext(ctx context.Context, _ []byte) (int, net.Addr, error) {
+ if !p.blockWrite {
+ p.started <- nil
}
+ <-ctx.Done()
- testDTLSDualStack(t, clientOpts, serverOpts)
+ return 0, nil, ctx.Err()
}
-// WIP! Tests if the dual stack mode server managed to negotiate a version successfully.
-func TestDTLSDualStackServer(t *testing.T) {
- defer test.CheckRoutines(t)()
- defer test.TimeOut(time.Second * 10).Stop()
-
- // Setup client
- clientCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
+func (p *deadlineTestTransport) WriteToContext(ctx context.Context, b []byte, _ net.Addr) (int, error) {
+ if p.blockWrite {
+ p.started <- nil
+ <-ctx.Done()
- clientOpts := []ClientOption{
- WithCertificates(clientCert),
- WithInsecureSkipVerify(true),
- WithMinVersion(protocol.Version1_2),
- WithMaxVersion(protocol.Version1_2),
+ return 0, ctx.Err()
}
- // Setup server
- serverCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
+ return len(b), nil
+}
- serverOpts := []ServerOption{
- WithCertificates(serverCert),
- WithInsecureSkipVerify(true),
- WithMinVersion(protocol.Version1_2),
- WithMaxVersion(protocol.Version1_3),
+func awaitDeadlineTest(t *testing.T, ch <-chan error) error {
+ t.Helper()
+ select {
+ case err := <-ch:
+ return err
+ case <-time.After(5 * time.Second):
+ require.FailNow(t, "handshake I/O did not respond")
}
- testDTLSDualStack(t, clientOpts, serverOpts)
+ return nil
}
-// WIP! Tests if the dual stack mode managed to negotiate a version successfully.
-func testDTLSDualStack(t *testing.T, clientOpts []ClientOption, serverOpts []ServerOption) {
+func newDeadlineTestClient(t *testing.T, minVersion, maxVersion protocol.Version, blockWrite bool) (*Conn, *deadlineTestTransport) {
t.Helper()
- ca, cb := dpipe.Pipe()
+ a, b := packetPipe()
+ conn, err := Client(a, a.RemoteAddr(), WithInsecureSkipVerify(true), WithMinVersion(minVersion), WithMaxVersion(maxVersion), WithFlightInterval(time.Hour))
+ require.NoError(t, err)
+ transport := &deadlineTestTransport{PacketConn: conn.nextConn, started: make(chan error, 1), blockWrite: blockWrite}
+ conn.nextConn = transport
+ t.Cleanup(func() { _ = conn.Close(); _ = a.Close(); _ = b.Close() })
+
+ return conn, transport
+}
- client, err := ClientWithOptions(dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), clientOpts...)
- assert.NoError(t, err)
- defer func() {
- _ = client.Close()
- }()
+func TestImplicitHandshakeDeadlines(t *testing.T) {
+ operations := map[string]func(*Conn) error{
+ "Read": func(c *Conn) error {
+ _, err := c.Read(make([]byte, 1))
- _, ok := client.ConnectionState()
- assert.False(t, ok)
+ return err
+ },
+ "Write": func(c *Conn) error {
+ _, err := c.Write([]byte{1})
- ctxClient, cancelClient := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancelClient()
- errorChannel := make(chan error, 2)
+ return err
+ },
+ }
+ for _, versions := range [][2]protocol.Version{
+ {protocol.Version1_2, protocol.Version1_2},
+ {protocol.Version1_3, protocol.Version1_3},
+ {protocol.Version1_2, protocol.Version1_3},
+ } {
+ for name, operation := range operations {
+ for _, blockWrite := range []bool{false, true} {
+ t.Run(fmt.Sprintf("%v/%s/write=%v", versions, name, blockWrite), func(t *testing.T) {
+ conn, transport := newDeadlineTestClient(t, versions[0], versions[1], blockWrite)
+ done := make(chan error, 1)
+ go func() { done <- operation(conn) }()
+ require.NoError(t, awaitDeadlineTest(t, transport.started))
+ setDeadline := conn.SetReadDeadline
+ if blockWrite {
+ setDeadline = conn.SetWriteDeadline
+ }
+ require.NoError(t, setDeadline(time.Now().Add(-time.Second)))
+ require.ErrorIs(t, awaitDeadlineTest(t, done), context.DeadlineExceeded)
+ })
+ }
+ }
+ }
+}
- server, err := ServerWithOptions(dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), serverOpts...)
- assert.NoError(t, err)
- defer func() {
- _ = server.Close()
- }()
+func TestReadDeadlineAfterHandshake(t *testing.T) {
+ cert, err := selfsign.GenerateSelfSigned()
+ require.NoError(t, err)
+ for _, version := range []protocol.Version{protocol.Version1_2, protocol.Version1_3} {
+ a, b := packetPipe()
+ client, err := Client(a, a.RemoteAddr(), WithInsecureSkipVerify(true), WithMinVersion(version), WithMaxVersion(version))
+ require.NoError(t, err)
+ server, err := Server(b, b.RemoteAddr(), WithCertificates(cert), WithMinVersion(version), WithMaxVersion(version))
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = client.Close(); _ = server.Close(); _ = a.Close(); _ = b.Close() })
+ deadline := time.Now().Add(5 * time.Second)
+ require.NoError(t, client.SetDeadline(deadline))
+ require.NoError(t, server.SetDeadline(deadline))
+ done := make(chan error, 1)
+ go func() { done <- client.Handshake() }()
+ require.NoError(t, server.Handshake())
+ require.NoError(t, <-done)
+ readDeadline, _ := client.readDeadline.Deadline()
+ writeDeadline, _ := client.writeDeadline.Deadline()
+ require.Equal(t, deadline, readDeadline)
+ require.Equal(t, deadline, writeDeadline)
+ require.NoError(t, client.SetReadDeadline(time.Now().Add(-time.Second)))
+ _, err = client.Read(make([]byte, 16))
+ require.ErrorIs(t, err, context.DeadlineExceeded)
+ require.NoError(t, client.SetReadDeadline(time.Now().Add(5*time.Second)))
+ require.NoError(t, server.SetWriteDeadline(time.Now().Add(5*time.Second)))
+ _, err = server.Write([]byte("hello"))
+ require.NoError(t, err)
+ buf := make([]byte, 16)
+ n, err := client.Read(buf)
+ require.NoError(t, err)
+ require.Equal(t, "hello", string(buf[:n]))
+ }
+}
- _, ok = server.ConnectionState()
- assert.False(t, ok)
+func pushEpochTestHandshake(t *testing.T, conn *Conn, header handshake.Header, epoch uint64) packetOutcome {
+ t.Helper()
+ raw, err := header.Marshal()
+ require.NoError(t, err)
+ raw = append(raw, make([]byte, header.FragmentLength)...)
+ outcome, _, err := conn.bufferHandshakeRecord(raw, protocol.RecordNumber{Epoch: epoch}, func() bool { return true })
+ require.NoError(t, err)
- ctxServer, cancelServer := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancelServer()
+ return outcome
+}
- go func() {
- errC := client.HandshakeContext(ctxClient)
- errorChannel <- errC
- }()
+func TestDTLS13HandshakeEpochValidation(t *testing.T) {
+ conn, _ := newTestConnWithReadProtection(t)
+ for _, typ := range []handshake.Type{
+ handshake.TypeFinished, handshake.TypeEncryptedExtensions,
+ handshake.TypeNewSessionTicket, handshake.TypeKeyUpdate, handshake.TypeNewConnectionID, handshake.TypeRequestConnectionID,
+ } {
+ for _, epoch := range []uint64{0, 1} {
+ assert.False(t, pushEpochTestHandshake(t, conn, handshake.Header{Type: typ}, epoch).containsHandshake)
+ }
+ if typ != handshake.TypeFinished && typ != handshake.TypeEncryptedExtensions {
+ assert.False(t, pushEpochTestHandshake(t, conn, handshake.Header{Type: typ}, 2).containsHandshake)
+ }
+ }
+ // Invalid records must leave sequence zero available for the real flight.
+ pushEpochTestHandshake(t, conn, handshake.Header{Type: handshake.TypeEncryptedExtensions}, 2)
+ item, ok := conn.handshakeCache.PullExact(0, false)
+ require.True(t, ok)
+ assert.Equal(t, uint64(2), item.Epoch)
+ for _, sequence := range []uint16{1, 2} {
+ assert.True(t, pushEpochTestHandshake(t, conn, handshake.Header{Type: handshake.TypeNewSessionTicket, MessageSequence: sequence}, uint64(sequence)+2).containsHandshake)
+ }
+}
- go func() {
- errS := server.HandshakeContext(ctxServer)
- errorChannel <- errS
- }()
+func TestDTLS13FutureHelloDoesNotConsumeProtectedSequence(t *testing.T) {
+ for _, typ := range []handshake.Type{handshake.TypeClientHello, handshake.TypeServerHello} {
+ conn, _ := newTestConnWithReadProtection(t)
+ state, err := dtlsstate.As13(conn.state)
+ require.NoError(t, err)
+ state.IsClient, state.HandshakeRecvSequence = typ == handshake.TypeServerHello, 1
+ conn.setRemoteEpoch(0)
+ for _, length := range []uint32{1, 2} {
+ future := handshake.Header{Type: typ, MessageSequence: 2, Length: 2, FragmentLength: length}
+ assert.False(t, pushEpochTestHandshake(t, conn, future, 0).containsHandshake)
+ }
+ hello := handshake.Header{Type: typ, MessageSequence: 1}
+ assert.True(t, pushEpochTestHandshake(t, conn, hello, 0).containsHandshake)
+ state.HandshakeRecvSequence = 2
+ conn.setLocalEpoch(2)
+ assert.True(t, pushEpochTestHandshake(t, conn, hello, 0).retransmit)
+ assert.False(t, pushEpochTestHandshake(t, conn, handshake.Header{Type: typ, MessageSequence: 2}, 0).containsHandshake)
+ pushEpochTestHandshake(t, conn, handshake.Header{Type: handshake.TypeFinished, MessageSequence: 2}, 2)
+ item, ok := conn.handshakeCache.PullExact(2, !state.IsClient)
+ require.True(t, ok)
+ assert.Equal(t, uint64(2), item.Epoch)
+ assert.Equal(t, handshake.TypeFinished, item.Typ)
+ }
+}
- err = <-errorChannel
- assert.NoError(t, err)
+func TestVersionSelectionDiscardsSpeculativeDTLS13Handshake(t *testing.T) {
+ for _, version := range []protocol.Version{protocol.Version1_2, protocol.Version1_3} {
+ for _, isClient := range []bool{false, true} {
+ conn, _ := newTestConnWithReadProtection(t)
+ common := dtlsstate.CommonState(conn.state)
+ common.LocalVersion, common.IsClient = 0, isClient
+ conn.setRemoteEpoch(0)
+ helloType := handshake.TypeClientHello
+ if isClient {
+ helloType = handshake.TypeServerHello
+ }
+ pushEpochTestHandshake(t, conn, handshake.Header{Type: helloType}, 0)
+ pushEpochTestHandshake(t, conn, handshake.Header{Type: handshake.TypeFinished, MessageSequence: 1}, 0)
+ pushEpochTestHandshake(t, conn, handshake.Header{Type: handshake.TypeCertificate, MessageSequence: 2, Length: 2, FragmentLength: 1}, 0)
+ conn.handshakeCache.Push(nil, 0, 1, helloType, isClient)
+ oldBuffer := conn.fragmentBuffer
+ conn.setNegotiatedVersion([]protocol.Version{version}, version)
+ _, ok := conn.handshakeCache.PullExact(0, !isClient)
+ require.True(t, ok, "preserve the negotiating hello")
+ _, ok = conn.handshakeCache.PullExact(1, isClient)
+ require.True(t, ok, "preserve the local transcript")
+ _, ok = conn.handshakeCache.PullExact(1, !isClient)
+ require.Equal(t, version == protocol.Version1_2, ok)
+ if version == protocol.Version1_2 {
+ require.Same(t, oldBuffer, conn.fragmentBuffer)
+
+ continue
+ }
+ for _, sequence := range []uint16{1, 2} {
+ pushEpochTestHandshake(t, conn, handshake.Header{Type: handshake.TypeFinished, MessageSequence: sequence}, 2)
+ item, cached := conn.handshakeCache.PullExact(sequence, !isClient)
+ require.True(t, cached, "discard speculative messages and partial fragments")
+ assert.Equal(t, uint64(2), item.Epoch)
+ }
+ }
+ }
+}
- err = <-errorChannel
- assert.NoError(t, err)
+func TestEmptyHandshakeRecordIsIgnored(t *testing.T) {
+ conn, _ := newTestConnWithReadProtection(t)
+ for _, epoch := range []uint64{0, 2, 3} {
+ outcome, latest, err := conn.bufferHandshakeRecord(nil, protocol.RecordNumber{Epoch: epoch}, func() bool {
+ assert.Fail(t, "empty records must not update replay state")
- assert.NoError(t, server.Close())
- assert.NoError(t, client.Close())
+ return true
+ })
+ require.NoError(t, err)
+ assert.False(t, outcome.containsHandshake)
+ assert.False(t, latest)
+ }
}
diff --git a/connection_id.go b/connection_id.go
index a8622eb03..eed0558a7 100644
--- a/connection_id.go
+++ b/connection_id.go
@@ -4,12 +4,37 @@
package dtls
import (
+ "bytes"
+ "context"
"crypto/rand"
+ "errors"
+ "net"
+ "slices"
+ "sync"
+ "time"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
+ dtlserrors "github.com/pion/dtls/v4/internal/errors"
+ dtlsflight "github.com/pion/dtls/v4/internal/flight"
+ dtlshandshake "github.com/pion/dtls/v4/internal/handshake"
+ idtlsnet "github.com/pion/dtls/v4/internal/net"
+ dtlsstate "github.com/pion/dtls/v4/internal/state"
+ "github.com/pion/dtls/v4/pkg/protocol"
+ "github.com/pion/dtls/v4/pkg/protocol/alert"
+ "github.com/pion/dtls/v4/pkg/protocol/handshake"
+ "github.com/pion/dtls/v4/pkg/protocol/recordlayer"
+ "github.com/pion/transport/v5/netctx"
+)
+
+//nolint:gochecknoglobals
+var (
+ // errPathMigrationUnavailable indicates that migration or probing requirements are not met.
+ errPathMigrationUnavailable = errors.New("path migration or probing unavailable")
+ // errPathNotValidated indicates that a path needs a successful Probe before Switch.
+ errPathNotValidated = errors.New("path has not been validated")
+ // errPathInUse indicates that a path is already being probed or is the active path.
+ errPathInUse = errors.New("path is in use")
+ // errNoConnectionID indicates that the peer supplied no unused nonempty CID for a new path.
+ errNoConnectionID = errors.New("no unused peer connection ID")
)
// RandomCIDGenerator is a random Connection ID generator where CID is the
@@ -35,71 +60,886 @@ func OnlySendCIDGenerator() func() []byte {
}
}
+type returnRoutabilityConn struct {
+ conn *Conn
+}
+
+func (c returnRoutabilityConn) WriteRRC(ctx context.Context, addr net.Addr, messageType protocol.ReturnRoutabilityCheckMessageType, cookie [protocol.ReturnRoutabilityCheckCookieLength]byte) error {
+ c.conn.writeLock.Lock()
+ defer c.conn.writeLock.Unlock()
+
+ c.conn.lock.Lock()
+ common := dtlsstate.CommonState(c.conn.state)
+ if c.conn.cidPathMigrationPolicy != CIDPathMigrationRRC || !common.RRCNegotiated {
+ c.conn.lock.Unlock()
+
+ return dtlserrors.ErrUnexpectedPostHandshakeMessage
+ }
+ message := &protocol.ReturnRoutabilityCheck{MessageType: messageType, Cookie: cookie}
+ raw, err := c.prepareRecord(message, addr)
+ if err == nil {
+ err = c.conn.rrc.Reserve(addr, c.conn.rAddr, len(raw))
+ }
+ c.conn.lock.Unlock()
+ if err != nil {
+ return err
+ }
+
+ if c.conn.detached != nil {
+ c.conn.detached.publishDatagrams([][]byte{raw}, addr)
+ } else {
+ _, err = c.conn.nextConn.WriteToContext(ctx, raw, addr)
+ }
+ if err != nil {
+ if errors.Is(err, context.Canceled) && c.conn.isConnectionClosed() {
+ return ErrConnClosed
+ }
+
+ return netError(err)
+ }
+
+ return nil
+}
+
+func (c returnRoutabilityConn) HandleRecord(ctx context.Context, message *protocol.ReturnRoutabilityCheck, prepared incomingPacketState, addr net.Addr) (bool, packetOutcome, error) {
+ if c.conn.cidPathMigrationPolicy != CIDPathMigrationRRC || prepared.number.Epoch == 0 || !dtlsstate.CommonState(c.conn.state).RRCNegotiated {
+ return false, packetOutcome{responseAlert: &alert.Alert{Level: alert.Fatal, Description: alert.UnexpectedMessage}}, dtlserrors.ErrUnexpectedPostHandshakeMessage
+ }
+ prepared.markPacketAsValid()
+ var err error
+ switch message.MessageType {
+ case protocol.ReturnRoutabilityCheckPathChallenge:
+ err = c.WriteRRC(ctx, addr, protocol.ReturnRoutabilityCheckPathResponse, message.Cookie)
+ case protocol.ReturnRoutabilityCheckPathResponse:
+ err = c.handleResponse(addr, message.Cookie)
+ case protocol.ReturnRoutabilityCheckPathDrop:
+ c.conn.rrc.Cancel(addr, message.Cookie)
+ default:
+ // Ignore unknown message types.
+ // https://datatracker.ietf.org/doc/html/rfc9853#section-4
+ }
+ if err != nil {
+ c.conn.log.Debugf("unable to handle return routability message: %v", err)
+ }
+
+ // A reachability probe does not request migration
+ // https://www.rfc-editor.org/rfc/rfc9853.html#section-1
+ return false, packetOutcome{}, nil
+}
+
+func (c returnRoutabilityConn) handleResponse(addr net.Addr, cookie [protocol.ReturnRoutabilityCheckCookieLength]byte) error {
+ c.conn.lock.Lock()
+ defer c.conn.lock.Unlock()
+ if source, ok := addr.(pathAddress); ok {
+ if source.path.acceptResponse(source.Addr, cookie) || source.path != source.path.transport.active {
+ return nil
+ }
+ }
+ if c.conn.rrc.HandleResponse(addr, cookie) {
+ return c.conn.updateRemoteAddr(addr)
+ }
+
+ return nil
+}
+
+func (c returnRoutabilityConn) HandleCandidate(
+ ctx context.Context,
+ rrcNegotiated, hasCID, latest bool,
+ addr net.Addr,
+) {
+ if !hasCID || !latest || !c.isActivePath(addr) {
+ return
+ }
+
+ currentAddr := c.conn.RemoteAddr()
+ if sameNetworkAddress(currentAddr, addr) {
+ return
+ }
+ if !c.useCandidatePath(rrcNegotiated, currentAddr, addr) {
+ return
+ }
+
+ c.startCandidateRRC(ctx, currentAddr, addr)
+}
+
+func (c returnRoutabilityConn) useCandidatePath(
+ rrcNegotiated bool,
+ currentAddr, candidateAddr net.Addr,
+) bool {
+ switch c.conn.cidPathMigrationPolicy {
+ case CIDPathMigrationReject:
+ c.conn.log.Errorf(
+ "rejected CID path migration from %s to %s: path migration is disabled",
+ currentAddr,
+ candidateAddr,
+ )
+ case CIDPathMigrationUnsafe:
+ c.conn.lock.Lock()
+ if err := c.conn.updateRemoteAddr(candidateAddr); err != nil {
+ c.conn.log.Debugf("unable to move address route: %v", err)
+ }
+ c.conn.lock.Unlock()
+ case CIDPathMigrationRRC:
+ if rrcNegotiated {
+ return true
+ }
+ c.conn.log.Errorf(
+ "rejected CID path migration from %s to %s: RRC was not negotiated",
+ currentAddr,
+ candidateAddr,
+ )
+ default:
+ c.conn.log.Errorf("rejected CID path migration from %s to %s: invalid path migration policy", currentAddr, candidateAddr)
+ }
+
+ return false
+}
+
+func (c returnRoutabilityConn) startCandidateRRC(ctx context.Context, currentAddr, candidateAddr net.Addr) {
+ cookie, ok, err := c.conn.rrc.Start(true, candidateAddr, currentAddr)
+ if err == nil && ok {
+ err = c.WriteRRC(ctx, candidateAddr, protocol.ReturnRoutabilityCheckPathChallenge, cookie)
+ if err != nil {
+ c.conn.rrc.Cancel(candidateAddr, cookie)
+ }
+ }
+ if err != nil {
+ c.conn.log.Debugf("unable to start return routability check: %v", err)
+ }
+}
+
+func sameNetworkAddress(a, b net.Addr) bool {
+ if a == nil || b == nil {
+ return a == nil && b == nil
+ }
+
+ return a.Network() == b.Network() && a.String() == b.String()
+}
+
// cidDatagramRouter extracts connection IDs from incoming datagram payloads and
// uses them to route to the proper connection.
// NOTE: properly routing datagrams based on connection IDs requires using
// constant size connection IDs.
func cidDatagramRouter(size int) func([]byte) (string, bool) {
return func(packet []byte) (string, bool) {
- pkts, err := recordlayer.ContentAwareUnpackDatagram(packet, size)
- if err != nil || len(pkts) < 1 {
+ pkts, _ := recordlayer.UnpackDatagram(packet, recordlayer.UnpackDatagramConfig{
+ CIDLength: size,
+ CIDRequired: true,
+ })
+ if len(pkts) == 0 {
return "", false
}
for _, pkt := range pkts {
- h := &recordlayer.Header{
- ConnectionID: make([]byte, size),
+ if cid := recordConnectionID(pkt, size); len(cid) > 0 {
+ return string(cid), true
}
- if err := h.Unmarshal(pkt); err != nil {
- continue
- }
- if h.ContentType != protocol.ContentTypeConnectionID {
- continue
- }
-
- return string(h.ConnectionID), true
}
return "", false
}
}
-// cidConnIdentifier extracts connection IDs from outgoing ServerHello records
-// and associates them with the associated connection.
-// NOTE: a ServerHello should always be the first record in a datagram if
-// multiple are present, so we avoid iterating through all packets if the first
-// is not a ServerHello.
-func cidConnIdentifier() func([]byte) (string, bool) { //nolint:cyclop
- return func(packet []byte) (string, bool) {
- pkts, err := recordlayer.UnpackDatagram(packet)
- if err != nil || len(pkts) < 1 {
- return "", false
+// recordConnectionID returns CID bytes borrowed from an already scanned record.
+func recordConnectionID(record []byte, size int) []byte {
+ parsed, err := recordlayer.ParseRecord(record, size)
+ if err != nil {
+ return nil
+ }
+
+ return parsed.ConnectionID()
+}
+
+// registerLocalCID is called with c.lock held when preparing a handshake or importing a session.
+func (c *Conn) registerLocalCID() error {
+ if c.packetConn == nil {
+ return nil
+ }
+ cid := dtlsstate.CommonState(c.state).LocalConnectionIDForInboundRecords()
+ var ids *dtlsstate.CIDReceiveSet
+ if state, ok := c.state.(*dtlsstate.State13); ok {
+ ids = state.CID.Receive.IDs
+ }
+ if bytes.Equal(cid, c.registeredLocalCID) && ids == c.registeredReceiveCIDs {
+ return nil
+ }
+ if err := c.packetConn.RegisterCID(cid); err != nil {
+ return err
+ }
+ if err := c.packetConn.RegisterCIDs(ids.Values()); err != nil {
+ return err
+ }
+ c.registeredLocalCID = bytes.Clone(cid)
+ c.registeredReceiveCIDs = ids
+
+ return nil
+}
+
+// acceptsInboundCID uses pre-negotiation DTLS 1.2 CID CID,
+// or DTLS 1.3 CID set.
+func (c *Conn) acceptsInboundCID(cid []byte) bool {
+ if state, ok := c.state.(*dtlsstate.State13); ok && state.CID.Negotiated {
+ return state.CID.Receive.IDs.Contains(cid)
+ }
+
+ return bytes.Equal(dtlsstate.CommonState(c.state).LocalConnectionIDForInboundRecords(), cid)
+}
+
+// reserveLocalCIDs reserves CID before publishing acceptance.
+func (c *Conn) reserveLocalCIDs(cids [][]byte) ([][]byte, error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ if c.isConnectionClosed() {
+ return nil, ErrConnClosed
+ }
+
+ state, ok := c.state.(*dtlsstate.State13)
+ if !ok || !state.CID.Negotiated || !state.CID.Receive.CanSendNewConnectionID {
+ return nil, dtlserrors.ErrUnexpectedPostHandshakeMessage
+ }
+ receive := &state.CID.Receive
+ added, err := prepareLocalCIDs(receive, cids)
+ if err != nil {
+ return nil, err
+ }
+ if receive.IDs.Len()+len(added) > dtlsstate.MaxConnectionIDs {
+ return nil, dtlserrors.ErrConnectionIDLimit
+ }
+ if c.packetConn != nil {
+ if err := c.packetConn.RegisterCIDs(added); err != nil {
+ return nil, err
}
- var h recordlayer.Header
- if hErr := h.Unmarshal(pkts[0]); hErr != nil {
- return "", false
+ }
+ for _, cid := range added {
+ receive.IDs.Add(cid)
+ }
+
+ return added, nil
+}
+
+// prepareLocalCIDs validates and copies new IDs.
+func prepareLocalCIDs(receive *dtlsstate.CIDReceiveState, cids [][]byte) ([][]byte, error) {
+ seen := make(map[string]bool, len(cids))
+ var added [][]byte
+ for _, cid := range cids {
+ if len(cid) != receive.Length {
+ return nil, dtlserrors.ErrInvalidConnectionIDLength
}
- if h.ContentType != protocol.ContentTypeHandshake {
- return "", false
+ key := string(cid)
+ if seen[key] || receive.IDs.Contains(cid) {
+ continue
}
- var hh handshake.Header
- var sh handshake.MessageServerHello
- for _, pkt := range pkts {
- if hhErr := hh.Unmarshal(pkt[recordlayer.FixedHeaderSize:]); hhErr != nil {
- continue
- }
- if err = sh.Unmarshal(pkt[recordlayer.FixedHeaderSize+handshake.HeaderLength:]); err == nil {
- break
- }
+ seen[key] = true
+ added = append(added, bytes.Clone(cid))
+ }
+
+ return added, nil
+}
+
+func (c handshakeConn) ReserveLocalCIDs(cids [][]byte) ([][]byte, error) {
+ return c.conn.reserveLocalCIDs(cids)
+}
+
+func (c handshakeConn) RemoveLocalCIDs(cids [][]byte) {
+ c.conn.removeLocalCIDs(cids)
+}
+
+// CommitPeerConnectionIDs serializes selection with every protected writer.
+func (c handshakeConn) CommitPeerConnectionIDs(message *handshake.MessageNewConnectionID) error {
+ c.conn.writeLock.Lock()
+ defer c.conn.writeLock.Unlock()
+ c.conn.lock.Lock()
+ defer c.conn.lock.Unlock()
+ if c.conn.isConnectionClosed() {
+ return ErrConnClosed
+ }
+ state, ok := c.conn.state.(*dtlsstate.State13)
+ if !ok {
+ return dtlserrors.ErrInvalidProtocolVersionState
+ }
+ send := &state.CID.Send
+ cids := message.CIDs
+ if message.Usage == handshake.ConnectionIDImmediate {
+ if len(cids) == 0 {
+ return dtlserrors.ErrInvalidCIDFormat
}
- if err != nil {
- return "", false
+ send.Active = bytes.Clone(cids[0])
+ send.UseCID = len(send.Active) != 0
+ cids = cids[1:]
+ send.Spares = slices.DeleteFunc(send.Spares, func(cid []byte) bool {
+ return bytes.Equal(cid, send.Active)
+ })
+ }
+ for _, cid := range cids {
+ if len(send.Spares) == dtlsstate.MaxConnectionIDs {
+ break
+ }
+ if bytes.Equal(cid, send.Active) || slices.ContainsFunc(send.Spares, func(spare []byte) bool {
+ return bytes.Equal(cid, spare)
+ }) {
+ continue
+ }
+ send.Spares = append(send.Spares, bytes.Clone(cid))
+ }
+
+ return nil
+}
+
+func (c *Conn) clearConnectionIDs() {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ if state, ok := c.state.(*dtlsstate.State13); ok {
+ state.CID.Receive.IDs.Clear()
+ state.CID.Send.Spares = nil
+ clear(c.pendingCIDACKs)
+ }
+}
+
+// removeLocalCIDs removes retired or never-advertised IDs.
+func (c *Conn) removeLocalCIDs(cids [][]byte) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ state, ok := c.state.(*dtlsstate.State13)
+ if !ok || state.CID.Receive.IDs == nil {
+ return
+ }
+ for _, cid := range cids {
+ state.CID.Receive.IDs.Remove(cid)
+ if c.packetConn != nil {
+ c.packetConn.UnregisterCID(cid)
+ }
+ }
+}
+
+func (c *Conn) pendingCIDNegotiation() bool {
+ common := dtlsstate.CommonState(c.state)
+
+ return common.ConnectionIDPending() && len(common.LocalConnectionIDForInboundRecords()) > 0 && common.LocalVersion != protocol.Version1_2
+}
+
+// updateRemoteAddr is called only after the migration policy accepts a path.
+// Sending an RRC probe must not move the listener's address route.
+func (c *Conn) updateRemoteAddr(addr net.Addr) error {
+ if source, ok := addr.(pathAddress); ok {
+ addr = source.Addr
+ }
+ if c.packetConn != nil {
+ if err := c.packetConn.SetRemoteAddr(addr); err != nil {
+ return err
+ }
+ }
+ c.rAddr = addr
+
+ return nil
+}
+
+func (c *Conn) takePendingACKs() []protocol.RecordNumber {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+
+ records := c.pendingACKs
+ c.pendingACKs = nil
+ for number, sequence := range c.pendingCIDACKs {
+ if int(sequence) < dtlsstate.HandshakeRecvSequence(c.state) {
+ records = append(records, number)
+ delete(c.pendingCIDACKs, number)
}
- for _, ext := range sh.Extensions {
- if e, ok := ext.(*extension.ConnectionID); ok {
- return string(e.CID), true
+ }
+
+ return records
+}
+
+func (c *Conn) queueHandshakeACK(content []byte, number protocol.RecordNumber) {
+ lastCIDSequence := -1
+ for len(content) != 0 {
+ var header handshake.Header
+ if err := header.Unmarshal(content); err != nil {
+ return
+ }
+ if header.Type == handshake.TypeNewConnectionID {
+ lastCIDSequence = max(lastCIDSequence, int(header.MessageSequence))
+ }
+ content = content[handshake.HeaderLength+int(header.FragmentLength):]
+ }
+ if lastCIDSequence < dtlsstate.HandshakeRecvSequence(c.state) {
+ c.pendingACKs = append(c.pendingACKs, number)
+
+ return
+ }
+ const maxPendingCIDACKs = 256
+ if len(c.pendingCIDACKs) == maxPendingCIDACKs {
+ return
+ }
+ if c.pendingCIDACKs == nil {
+ c.pendingCIDACKs = make(map[protocol.RecordNumber]uint16)
+ }
+ c.pendingCIDACKs[number] = uint16(lastCIDSequence) //nolint:gosec // copied from a uint16 message sequence.
+}
+
+// Path is a local transport to the connection's current peer. Obtain one with
+// [Conn.AddPath], validate it with [Path.Probe], then use [Path.Switch] to move
+// application writes. With CIDPathMigrationUnsafe, Switch skips validation.
+// All methods may be called concurrently.
+type Path struct {
+ *pathSocket
+}
+
+// pathSocket owns one socket and its receiving goroutine.
+type pathSocket struct {
+ transport *pathTransport
+ socket netctx.PacketConn
+ remote net.Addr
+ lifetime context.Context //nolint:containedctx
+ cancel context.CancelCauseFunc
+ closeSocket func() error
+
+ cid []byte
+ probe *pathProbe
+ validated bool
+}
+
+type pathProbe struct {
+ done chan struct{}
+ cookies [][protocol.ReturnRoutabilityCheckCookieLength]byte
+ expires time.Time
+}
+
+// AddPath adds a dedicated, unconnected packet socket to an established DTLS 1.3
+// connection with a nonempty peer CID. CIDPathMigrationRRC requires negotiated
+// RRC, CIDPathMigrationUnsafe allows switching without validation.
+// The peer address stays unchanged. On success, the connection owns socket and
+// closes it when the path or connection closes.
+func (c *Conn) AddPath(socket net.PacketConn) (*Path, error) {
+ c.lock.Lock()
+ defer c.lock.Unlock()
+ if c.isConnectionClosed() {
+ return nil, ErrConnClosed
+ }
+ if !c.isHandshakeCompletedSuccessfully() {
+ return nil, dtlserrors.ErrHandshakeInProgress
+ }
+ transport, supported := c.nextConn.(*pathTransport)
+ if !supported || !c.canMigratePath() {
+ return nil, errPathMigrationUnavailable
+ }
+ if socket == nil {
+ return nil, dtlserrors.ErrNilNextConn
+ }
+ for path := range transport.paths {
+ if sameNetworkAddress(path.socket.LocalAddr(), socket.LocalAddr()) {
+ return nil, errPathInUse
+ }
+ }
+ path := transport.add(netctx.NewPacketConn(socket))
+ go path.read()
+
+ return &Path{pathSocket: path}, nil
+}
+
+// canMigratePath is called with c.lock held.
+func (c *Conn) canMigratePath() bool {
+ state, ok := c.state.(*dtlsstate.State13)
+
+ return ok && state.CID.Send.UseCID &&
+ (c.cidPathMigrationPolicy == CIDPathMigrationUnsafe || state.RRCNegotiated)
+}
+
+// Probe checks reachability using RRC, reserving a peer CID on the first call.
+// Each call performs a new check and honors ctx and the write deadline.
+// Probe requires CIDPathMigrationRRC, it is unavailable in unsafe mode.
+// RRC validation takes at most one second after obtaining a CID. A failed check
+// leaves application writes on the current path and requires a successful retry
+// before switching.
+//
+//nolint:contextcheck
+func (p *Path) Probe(ctx context.Context) error {
+ if err := ctx.Err(); err != nil {
+ return err
+ }
+ conn := p.transport.conn
+ if conn.writeDeadline.Err() != nil {
+ return dtlserrors.ErrDeadlineExceeded
+ }
+ operationCtx, cancel := conn.contextWithCloseAndWriteDeadline(ctx)
+ defer cancel()
+ detach := context.AfterFunc(p.lifetime, cancel)
+ defer detach()
+
+ probe, err := p.beginProbe()
+ if err != nil {
+ return err
+ }
+ if err = p.obtainCID(operationCtx); err == nil {
+ err = p.probePath(operationCtx, probe)
+ }
+
+ return conn.normalizeKeyUpdateError(ctx, operationCtx, p.finishProbe(err))
+}
+
+func (p *Path) beginProbe() (*pathProbe, error) {
+ conn := p.transport.conn
+ conn.lock.Lock()
+ defer conn.lock.Unlock()
+ if conn.isConnectionClosed() {
+ return nil, ErrConnClosed
+ }
+ if err := context.Cause(p.lifetime); err != nil {
+ return nil, err
+ }
+ if conn.cidPathMigrationPolicy != CIDPathMigrationRRC {
+ return nil, errPathMigrationUnavailable
+ }
+ if p.probe != nil {
+ return nil, errPathInUse
+ }
+ if !sameNetworkAddress(p.remote, conn.rAddr) {
+ return nil, errPathNotValidated
+ }
+ p.validated = false
+ p.probe = &pathProbe{done: make(chan struct{})}
+
+ return p.probe, nil
+}
+
+func (p *Path) finishProbe(err error) error {
+ p.transport.conn.lock.Lock()
+ defer p.transport.conn.lock.Unlock()
+ select {
+ case <-p.probe.done:
+ err = nil
+ default:
+ }
+ p.probe = nil
+ if cause := context.Cause(p.lifetime); cause != nil {
+ err = cause
+ }
+ p.validated = err == nil
+
+ return err
+}
+
+func (p *Path) obtainCID(ctx context.Context) error {
+ select {
+ case p.transport.cidRequest <- struct{}{}:
+ defer func() { <-p.transport.cidRequest }()
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ if p.takeCID() {
+ return nil
+ }
+ updater, ok := p.transport.conn.fsm.(dtlshandshake.ConnectionIDUpdater)
+ if !ok {
+ return dtlserrors.ErrNotImplemented
+ }
+ if err := updater.RequestConnectionIDs(ctx, 1); err != nil {
+ return err
+ }
+ if !p.takeCID() {
+ return errNoConnectionID
+ }
+
+ return nil
+}
+
+func (p *Path) takeCID() bool {
+ conn := p.transport.conn
+ conn.lock.Lock()
+ defer conn.lock.Unlock()
+ if len(p.cid) != 0 {
+ return true
+ }
+ state, ok := conn.state.(*dtlsstate.State13)
+ if !ok || conn.isConnectionClosed() {
+ return false
+ }
+ p.transport.usedCIDs[string(state.CID.Send.Active)] = true
+ for len(state.CID.Send.Spares) != 0 {
+ cid := state.CID.Send.Spares[0]
+ state.CID.Send.Spares = state.CID.Send.Spares[1:]
+ if len(cid) == 0 || p.transport.usedCIDs[string(cid)] {
+ continue
+ }
+ p.transport.usedCIDs[string(cid)] = true
+ p.cid = cid
+
+ return true
+ }
+
+ return false
+}
+
+func (p *Path) probePath(ctx context.Context, probe *pathProbe) error {
+ // Use one second when no RTT estimate is available.
+ // https://www.rfc-editor.org/rfc/rfc9853.html#section-5.5
+ ctx, cancel := context.WithTimeout(ctx, time.Second)
+ defer cancel()
+ conn := p.transport.conn
+ ticker := time.NewTicker(250 * time.Millisecond)
+ defer ticker.Stop()
+ for ctx.Err() == nil {
+ var cookie [protocol.ReturnRoutabilityCheckCookieLength]byte
+ if _, err := rand.Read(cookie[:]); err != nil {
+ return err
+ }
+ conn.lock.Lock()
+ probe.expires, _ = ctx.Deadline()
+ probe.cookies = append(probe.cookies, cookie)
+ conn.lock.Unlock()
+ if err := (returnRoutabilityConn{conn: conn}).WriteRRC(ctx, pathAddress{Addr: p.remote, path: p.pathSocket}, protocol.ReturnRoutabilityCheckPathChallenge, cookie); err != nil {
+ return err
+ }
+ select {
+ case <-probe.done:
+ return nil
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-ticker.C:
+ }
+ }
+
+ return ctx.Err()
+}
+
+// Switch makes this validated path the connection's application write path.
+// It changes LocalAddr and the outgoing CID atomically with respect to writers.
+// Existing sockets keep receiving until closed. The peer address must still
+// match the address validated by Probe.
+// With CIDPathMigrationUnsafe, Switch skips validation and reuses the active CID
+// for a new path.
+func (p *Path) Switch() error {
+ conn := p.transport.conn
+ conn.writeLock.Lock()
+ defer conn.writeLock.Unlock()
+ conn.lock.Lock()
+ defer conn.lock.Unlock()
+ if conn.isConnectionClosed() {
+ return ErrConnClosed
+ }
+ if err := context.Cause(p.lifetime); err != nil {
+ return err
+ }
+ if (!p.validated && conn.cidPathMigrationPolicy != CIDPathMigrationUnsafe) || !sameNetworkAddress(p.remote, conn.rAddr) {
+ return errPathNotValidated
+ }
+ if p.transport.active == p.pathSocket {
+ return nil
+ }
+ state, ok := conn.state.(*dtlsstate.State13)
+ if !ok || !state.CID.Send.UseCID {
+ return errPathMigrationUnavailable
+ }
+ p.transport.active.cid = bytes.Clone(state.CID.Send.Active)
+ if len(p.cid) == 0 {
+ p.cid = bytes.Clone(state.CID.Send.Active)
+ }
+ state.CID.Send.Active = bytes.Clone(p.cid)
+ p.transport.active = p.pathSocket
+ conn.rrc.Reset()
+
+ return nil
+}
+
+// Close releases an inactive path's socket and cancels any pending Probe.
+// Closing the active path returns an error.
+func (p *Path) Close() error {
+ conn := p.transport.conn
+ conn.lock.Lock()
+ if p.transport.active == p.pathSocket && !conn.isConnectionClosed() {
+ conn.lock.Unlock()
+
+ return errPathInUse
+ }
+ p.cancel(net.ErrClosed)
+ delete(p.transport.paths, p.pathSocket)
+ conn.lock.Unlock()
+
+ return p.closeSocket()
+}
+
+// pathAddress carries the receiving socket through record processing and queued
+// records.
+type pathAddress struct {
+ net.Addr
+ path *pathSocket
+}
+
+type pathDatagram struct {
+ buffer *[]byte
+ size int
+ addr pathAddress
+ err error
+}
+
+type pathTransport struct {
+ conn *Conn
+ start func()
+ incoming chan pathDatagram
+ cidRequest chan struct{}
+ // Protected by conn.lock.
+ active *pathSocket
+ paths map[*pathSocket]struct{}
+ usedCIDs map[string]bool
+}
+
+func newPathTransport(conn *Conn, socket netctx.PacketConn) *pathTransport {
+ transport := &pathTransport{
+ conn: conn, incoming: make(chan pathDatagram, 16), cidRequest: make(chan struct{}, 1),
+ paths: make(map[*pathSocket]struct{}), usedCIDs: make(map[string]bool),
+ }
+ initial := transport.add(socket)
+ transport.active = initial
+ transport.start = sync.OnceFunc(func() { go initial.read() })
+
+ return transport
+}
+
+// add is called with conn.lock held, or during connection construction.
+func (t *pathTransport) add(socket netctx.PacketConn) *pathSocket {
+ lifetime, cancel := context.WithCancelCause(t.conn.closed)
+ path := &pathSocket{transport: t, socket: socket, remote: t.conn.rAddr, lifetime: lifetime, cancel: cancel, closeSocket: sync.OnceValue(socket.Close)}
+ t.paths[path] = struct{}{}
+
+ return path
+}
+
+func (p *pathSocket) read() {
+ conn := p.transport.conn
+ for {
+ buffer, ok := conn.readBufferPool.Get().(*[]byte)
+ if !ok {
+ return
+ }
+ n, addr, err := p.socket.Conn().ReadFrom(*buffer)
+ packet := pathDatagram{buffer: buffer, size: n, addr: pathAddress{Addr: addr, path: p}, err: err}
+ select {
+ case p.transport.incoming <- packet:
+ case <-p.lifetime.Done():
+ conn.readBufferPool.Put(buffer)
+
+ return
+ }
+ if err != nil && !idtlsnet.IsShortBuffer(err) {
+ conn.lock.RLock()
+ if p != p.transport.active || conn.classifyReadLoopError(netError(err)) != readLoopDeliverAndContinue {
+ p.cancel(err)
+ conn.lock.RUnlock()
+
+ return
}
+ conn.lock.RUnlock()
}
+ }
+}
- return "", false
+func (t *pathTransport) ReadFromContext(ctx context.Context, buffer []byte) (int, net.Addr, error) {
+ t.start()
+ for {
+ select {
+ case packet := <-t.incoming:
+ n := copy(buffer, (*packet.buffer)[:packet.size])
+ t.conn.readBufferPool.Put(packet.buffer)
+ t.conn.lock.RLock()
+ path := packet.addr.path
+ accept := path == t.active || (packet.err == nil && path.lifetime.Err() == nil && sameNetworkAddress(packet.addr.Addr, path.remote))
+ t.conn.lock.RUnlock()
+ if !accept {
+ continue
+ }
+
+ return n, packet.addr, packet.err
+ case <-ctx.Done():
+ return 0, nil, ctx.Err()
+ }
+ }
+}
+
+func (t *pathTransport) WriteToContext(ctx context.Context, packet []byte, addr net.Addr) (int, error) {
+ t.conn.lock.RLock()
+ path := t.active
+ if source, ok := addr.(pathAddress); ok {
+ path, addr = source.path, source.Addr
+ }
+ t.conn.lock.RUnlock()
+
+ return path.socket.WriteToContext(ctx, packet, addr)
+}
+
+func (t *pathTransport) LocalAddr() net.Addr {
+ return t.Conn().LocalAddr()
+}
+
+func (t *pathTransport) Conn() net.PacketConn {
+ t.conn.lock.RLock()
+ defer t.conn.lock.RUnlock()
+
+ return t.active.socket.Conn()
+}
+
+func (t *pathTransport) Close() error {
+ t.conn.lock.Lock()
+ paths := t.paths
+ t.paths = nil
+ t.conn.lock.Unlock()
+ var err error
+ for path := range paths {
+ path.cancel(ErrConnClosed)
+ err = errors.Join(err, path.closeSocket())
+ }
+
+ return err
+}
+
+func (c returnRoutabilityConn) prepareRecord(message *protocol.ReturnRoutabilityCheck, addr net.Addr) ([]byte, error) {
+ packet := &dtlsflight.Outbound{Epoch: dtlsstate.CommonState(c.conn.state).LocalEpoch(), Content: message, Protection: dtlsflight.ProtectionCiphertext}
+ source, tagged := addr.(pathAddress)
+ if !tagged || source.path == source.path.transport.active {
+ return c.conn.prepareRecord(packet)
+ }
+ path := source.path
+ if err := context.Cause(path.lifetime); err != nil {
+ return nil, err
+ }
+ if len(path.cid) == 0 {
+ return nil, errNoConnectionID
}
+ state, ok := c.conn.state.(*dtlsstate.State13)
+ if !ok || !state.CID.Send.UseCID {
+ return nil, errPathMigrationUnavailable
+ }
+ if message.MessageType == protocol.ReturnRoutabilityCheckPathResponse {
+ // This socket is not the preferred path.
+ // https://www.rfc-editor.org/rfc/rfc9853.html#section-5.4
+ message.MessageType = protocol.ReturnRoutabilityCheckPathDrop
+ }
+ active := state.CID.Send.Active
+ state.CID.Send.Active = path.cid
+ defer func() { state.CID.Send.Active = active }()
+
+ return c.conn.prepareRecord(packet)
+}
+
+func (c returnRoutabilityConn) isActivePath(addr net.Addr) bool {
+ c.conn.lock.RLock()
+ defer c.conn.lock.RUnlock()
+ source, tagged := addr.(pathAddress)
+
+ return !tagged || source.path == source.path.transport.active
+}
+
+// acceptResponse is called with conn.lock held, after record authentication.
+func (p *pathSocket) acceptResponse(addr net.Addr, cookie [protocol.ReturnRoutabilityCheckCookieLength]byte) bool {
+ probe := p.probe
+ if probe == nil || p.lifetime.Err() != nil || !sameNetworkAddress(addr, p.remote) {
+ return false
+ }
+ if !time.Now().Before(probe.expires) || !slices.Contains(probe.cookies, cookie) {
+ return false
+ }
+ select {
+ case <-probe.done:
+ default:
+ close(probe.done)
+ }
+
+ return true
}
diff --git a/connection_id_test.go b/connection_id_test.go
index 2dda7933c..be62be399 100644
--- a/connection_id_test.go
+++ b/connection_id_test.go
@@ -4,16 +4,82 @@
package dtls
import (
+ "bytes"
+ "context"
+ "net"
+ "sync"
"testing"
"time"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
+ "github.com/pion/dtls/v4/internal/closer"
+ dtlserrors "github.com/pion/dtls/v4/internal/errors"
+ dtlsflight13 "github.com/pion/dtls/v4/internal/flight/flight13"
+ "github.com/pion/dtls/v4/internal/negotiation"
+ dtlsstate "github.com/pion/dtls/v4/internal/state"
+ cryptosuite "github.com/pion/dtls/v4/pkg/crypto/ciphersuite"
+ "github.com/pion/dtls/v4/pkg/crypto/elliptic"
+ "github.com/pion/dtls/v4/pkg/protocol"
+ "github.com/pion/dtls/v4/pkg/protocol/alert"
+ "github.com/pion/dtls/v4/pkg/protocol/extension"
+ extension13 "github.com/pion/dtls/v4/pkg/protocol/extension/dtls13"
+ "github.com/pion/dtls/v4/pkg/protocol/handshake"
+ "github.com/pion/dtls/v4/pkg/protocol/recordlayer"
+ "github.com/pion/transport/v5/netctx"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
+func TestPathTransport(t *testing.T) {
+ listen := func() net.PacketConn {
+ socket, err := (&net.ListenConfig{}).ListenPacket(context.Background(), "udp4", "127.0.0.1:0")
+ require.NoError(t, err)
+ t.Cleanup(func() { _ = socket.Close() })
+
+ return socket
+ }
+ peer, initial, candidate := listen(), listen(), listen()
+ conn := &Conn{closed: closer.NewCloser(), rAddr: peer.LocalAddr(), readBufferPool: readBufferPoolForSize(1500)}
+ transport := newPathTransport(conn, netctx.NewPacketConn(initial))
+ t.Cleanup(func() { _ = transport.Close() })
+ path := transport.add(netctx.NewPacketConn(candidate))
+ go path.read()
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ buffer := make([]byte, 1500)
+ for _, socket := range []net.PacketConn{initial, candidate} {
+ _, err := peer.WriteTo([]byte("request"), socket.LocalAddr())
+ require.NoError(t, err)
+ n, addr, err := transport.ReadFromContext(ctx, buffer)
+ require.NoError(t, err)
+ assert.Equal(t, "request", string(buffer[:n]))
+ assert.True(t, sameNetworkAddress(peer.LocalAddr(), addr))
+ _, err = transport.WriteToContext(ctx, []byte("reply"), addr)
+ require.NoError(t, err)
+ require.NoError(t, peer.SetReadDeadline(time.Now().Add(time.Second)))
+ n, source, err := peer.ReadFrom(buffer)
+ require.NoError(t, err)
+ assert.Equal(t, "reply", string(buffer[:n]))
+ assert.Equal(t, socket.LocalAddr(), source)
+ require.NoError(t, conn.updateRemoteAddr(addr))
+ assert.Equal(t, peer.LocalAddr(), conn.rAddr)
+ }
+ _, err := transport.WriteToContext(ctx, []byte("active"), peer.LocalAddr())
+ require.NoError(t, err)
+ _, source, err := peer.ReadFrom(buffer)
+ require.NoError(t, err)
+ assert.Equal(t, initial.LocalAddr(), source)
+ for _, socket := range []net.PacketConn{initial, candidate} {
+ require.NoError(t, socket.SetReadDeadline(time.Now().Add(time.Second)))
+ }
+ require.NoError(t, transport.Close())
+ require.NoError(t, transport.Close())
+ for _, socket := range []net.PacketConn{initial, candidate} {
+ _, _, err = socket.ReadFrom(buffer)
+ assert.ErrorIs(t, err, net.ErrClosed)
+ }
+ assert.ErrorIs(t, context.Cause(path.lifetime), ErrConnClosed)
+}
+
func TestRandomConnectionIDGenerator(t *testing.T) {
cases := map[string]struct {
reason string
@@ -53,15 +119,9 @@ func TestOnlySendCIDGenerator(t *testing.T) {
func TestCIDDatagramRouter(t *testing.T) {
cid := []byte("abcd1234")
cidLen := 8
- appRecord, err := (&recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Epoch: 1,
- Version: protocol.Version1_2,
- },
- Content: &protocol.ApplicationData{
- Data: []byte("application data"),
- },
- }).Marshal()
+ epochZeroRecord, err := marshalTestRecord(recordlayer.RecordConfig{Epoch: 0, Version: protocol.Version1_2}, &alert.Alert{Level: alert.Warning, Description: alert.CloseNotify})
+ assert.NoError(t, err)
+ protectedWithoutCIDRecord, err := marshalTestRecord(recordlayer.RecordConfig{Epoch: 1, Version: protocol.Version1_2}, &protocol.ApplicationData{Data: []byte("application data")})
assert.NoError(t, err)
appData, err := (&protocol.ApplicationData{
@@ -69,20 +129,13 @@ func TestCIDDatagramRouter(t *testing.T) {
}).Marshal()
assert.NoError(t, err)
- inner, err := (&recordlayer.InnerPlaintext{
- Content: appData,
- RealType: protocol.ContentTypeApplicationData,
- }).Marshal()
+ inner, err := recordlayer.MarshalInnerPlaintext(appData, protocol.ContentTypeApplicationData, 0)
assert.NoError(t, err)
- cidHeader, err := (&recordlayer.Header{
- Epoch: 1,
- Version: protocol.Version1_2,
- ContentType: protocol.ContentTypeConnectionID,
- ContentLen: uint16(len(inner)), //nolint:gosec // G115
- ConnectionID: cid,
- SequenceNumber: 1,
- }).Marshal()
+ cidRecord, err := recordlayer.MarshalRecord(recordlayer.RecordConfig{
+ ContentType: protocol.ContentTypeConnectionID, Version: protocol.Version1_2,
+ Epoch: 1, SequenceNumber: 1, ConnectionID: cid,
+ }, inner)
assert.NoError(t, err)
cases := map[string]struct {
@@ -92,79 +145,51 @@ func TestCIDDatagramRouter(t *testing.T) {
ok bool
want string
}{
- "EmptyDatagram": {
- reason: "If datagram is empty, we cannot extract an identifier",
+ "EmptyDatagram": {reason: "If datagram is empty, we cannot extract an identifier", size: cidLen, datagram: []byte{}, ok: false, want: ""},
+ "NotADTLSRecord": {reason: "If datagram is not a DTLS record, we cannot extract an identifier", size: cidLen, datagram: []byte("not a DTLS record"), ok: false, want: ""},
+ "NotAConnectionIDDatagram": {reason: "If datagram does not contain any Connection ID records, we cannot extract an identifier", size: cidLen, datagram: epochZeroRecord, ok: false, want: ""},
+ "ProtectedRecordWithoutCIDPrefix": {
+ reason: "A protected DTLS 1.2 record without type 25 is invalid after CID negotiation and must not route through a later CID.",
size: cidLen,
- datagram: []byte{},
+ datagram: append(bytes.Clone(protectedWithoutCIDRecord), cidRecord...),
ok: false,
want: "",
},
- "NotADTLSRecord": {
- reason: "If datagram is not a DTLS record, we cannot extract an identifier",
- size: cidLen,
- datagram: []byte("not a DTLS record"),
- ok: false,
- want: "",
- },
- "NotAConnectionIDDatagram": {
- reason: "If datagram does not contain any Connection ID records, we cannot extract an identifier",
- size: cidLen,
- datagram: appRecord,
- ok: false,
- want: "",
- },
- "OneRecordConnectionID": {
- reason: "If datagram contains one Connection ID record, we should be able to extract it.",
- size: cidLen,
- datagram: append(cidHeader, inner...),
- ok: true,
- want: string(cid),
- },
+ "OneRecordConnectionID": {reason: "If datagram contains one Connection ID record, we should be able to extract it.", size: cidLen, datagram: cidRecord, ok: true, want: string(cid)},
"OneRecordConnectionIDAltLength": {
- //nolint:lll
reason: "If datagram contains one Connection ID record, but it has the wrong length we should not be able to extract it.",
size: cidLen,
datagram: func() []byte {
- altCIDHeader, err := (&recordlayer.Header{
- Epoch: 1,
- Version: protocol.Version1_2,
- ContentType: protocol.ContentTypeConnectionID,
- ContentLen: uint16(len(inner)), //nolint:gosec // G115
- ConnectionID: []byte("abcd"),
- SequenceNumber: 1,
- }).Marshal()
+ altCIDRecord, err := recordlayer.MarshalRecord(recordlayer.RecordConfig{
+ ContentType: protocol.ContentTypeConnectionID, Version: protocol.Version1_2,
+ Epoch: 1, SequenceNumber: 1, ConnectionID: []byte("abcd"),
+ }, inner)
assert.NoError(t, err)
- return append(altCIDHeader, inner...)
+ return altCIDRecord
}(),
ok: false,
want: "",
},
"MultipleRecordOneConnectionID": {
- //nolint:lll
- reason: "If datagram contains multiple records and one is a Connection ID record, we should be able to extract it.",
+ reason: "An epoch-zero DTLS 1.2 record may precede a protected Connection ID record in the same datagram.",
size: 8,
- datagram: append(append(appRecord, cidHeader...), inner...),
+ datagram: append(bytes.Clone(epochZeroRecord), cidRecord...),
ok: true,
want: string(cid),
},
"MultipleRecordMultipleConnectionID": {
- //nolint:lll
reason: "If datagram contains multiple records and multiple are Connection ID records, we should extract the first one.",
size: 8,
- datagram: append(append(append(appRecord, func() []byte {
- altCIDHeader, err := (&recordlayer.Header{
- Epoch: 1,
- Version: protocol.Version1_2,
- ContentType: protocol.ContentTypeConnectionID,
- ContentLen: uint16(len(inner)), //nolint:gosec // G115
- ConnectionID: []byte("1234abcd"),
- SequenceNumber: 1,
- }).Marshal()
+ datagram: append(append(bytes.Clone(epochZeroRecord), func() []byte {
+ altCIDRecord, err := recordlayer.MarshalRecord(recordlayer.RecordConfig{
+ ContentType: protocol.ContentTypeConnectionID, Version: protocol.Version1_2,
+ Epoch: 1, SequenceNumber: 1, ConnectionID: []byte("1234abcd"),
+ }, inner)
assert.NoError(t, err)
- return append(altCIDHeader, inner...)
- }()...), cidHeader...), inner...),
+ return altCIDRecord
+ }()...), cidRecord...),
ok: true,
want: "1234abcd",
},
@@ -178,92 +203,297 @@ func TestCIDDatagramRouter(t *testing.T) {
}
}
-func TestCIDConnIdentifier(t *testing.T) {
+func TestCIDDatagramRouter13(t *testing.T) {
cid := []byte("abcd1234")
- cs := uint16(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256)
- sh, err := (&recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Epoch: 0,
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageServerHello{
- Version: protocol.Version1_2,
- Random: handshake.Random{GMTUnixTime: time.Unix(500, 0), RandomBytes: [28]byte{}},
- SessionID: []byte("hello"),
- CipherSuiteID: &cs,
- CompressionMethod: defaultCompressionMethods()[0],
- Extensions: []extension.Extension{
- &extension.ConnectionID{
- CID: cid,
- },
- },
- },
- },
- }).Marshal()
+ plaintextPrefix, err := marshalTestRecord(recordlayer.RecordConfig{Version: protocol.Version1_2}, &alert.Alert{Level: alert.Warning, Description: alert.CloseNotify})
assert.NoError(t, err)
- appRecord, err := (&recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Epoch: 1,
- Version: protocol.Version1_2,
- },
- Content: &protocol.ApplicationData{
- Data: []byte("application data"),
- },
- }).Marshal()
- assert.NoError(t, err)
+ makeRecord := func(t *testing.T, connectionID []byte, sequenceNumber uint16) []byte {
+ t.Helper()
+
+ record, err := recordlayer.MarshalCiphertext(recordlayer.CiphertextConfig{ConnectionID: connectionID, SequenceNumber: sequenceNumber, TwoByteSequence: true, LengthPresent: true}, make([]byte, 16))
+ assert.NoError(t, err)
+
+ return record
+ }
+
+ recordWithCID := makeRecord(t, cid, 1)
+ recordWithoutCID := makeRecord(t, nil, 2)
+ otherCID := []byte("1234abcd")
+ recordWithOtherCID := makeRecord(t, otherCID, 3)
cases := map[string]struct {
reason string
+ size int
datagram []byte
ok bool
want string
}{
- "EmptyDatagram": {
- reason: "If datagram is empty, we cannot extract an identifier",
- datagram: []byte{},
- ok: false,
- want: "",
- },
- "NotADTLSRecord": {
- reason: "If datagram is not a DTLS record, we cannot extract an identifier",
- datagram: []byte("not a DTLS record"),
- ok: false,
- want: "",
- },
- "NotAServerhelloDatagram": {
- reason: "If datagram does not contain any ServerHello record, we cannot extract an identifier",
- datagram: appRecord,
- ok: false,
- want: "",
- },
- "OneRecordServerHello": {
- reason: "If datagram contains one ServerHello record, we should be able to extract an identifier.",
- datagram: sh,
- ok: true,
- want: string(cid),
- },
- "MultipleRecordFirstServerHello": {
- //nolint:lll
- reason: "If datagram contains multiple records and the first is a ServerHello record, we should be able to extract an identifier.",
- datagram: append(sh, appRecord...),
- ok: true,
- want: string(cid),
- },
- "MultipleRecordNotFirstServerHello": {
- //nolint:lll
- reason: "If datagram contains multiple records and the first is not a ServerHello record, we should not be able to extract an identifier.",
- datagram: append(appRecord, sh...),
- ok: false,
- want: "",
- },
+ "OneRecordConnectionID": {reason: "A unified-header record with the C bit should expose its CID.", size: len(cid), datagram: recordWithCID, ok: true, want: string(cid)},
+ "NoConnectionIDBit": {reason: "A unified-header record without the C bit has no routing identifier.", size: len(cid), datagram: recordWithoutCID, ok: false},
+ "WrongConfiguredLength": {reason: "A unified-header CID must have the listener's configured fixed length.", size: len(cid) - 1, datagram: recordWithCID, ok: false},
+ "MultipleRecords": {reason: "The first CID in a datagram containing unified-header records should be used.", size: len(cid), datagram: append(append([]byte{}, recordWithCID...), recordWithOtherCID...), ok: true, want: string(cid)},
+ "FixedPrefix": {reason: "A CID in a unified-header record should route after a fixed-header CID-less prefix.", size: len(cid), datagram: append(append([]byte{}, plaintextPrefix...), recordWithCID...), ok: true, want: string(cid)},
+ "MalformedSuffix": {reason: "A malformed suffix should not hide a CID in an already framed record.", size: len(cid), datagram: append(append([]byte{}, recordWithCID...), 0xff), ok: true, want: string(cid)},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
- cid, ok := cidConnIdentifier()(tc.datagram)
- assert.Equalf(t, tc.ok, ok, "%s\ncidConnIdentifier mismatch", tc.reason)
- assert.Equalf(t, tc.want, cid, "%s\ncidConnIdentifier mismatch", tc.reason)
+ got, ok := cidDatagramRouter(tc.size)(tc.datagram)
+ assert.Equal(t, tc.ok, ok, "%s\ncidDatagramRouter mismatch", tc.reason)
+ assert.Equal(t, tc.want, got, "%s\ncidDatagramRouter mismatch", tc.reason)
})
}
}
+
+func TestConnectionIDPreflight(t *testing.T) {
+ cid := []byte("own-cid!")
+ state12 := dtlsstate.NewState12(false)
+ state12.SetLocalConnectionID(cid)
+ state13 := dtlsstate.NewState13(false)
+ state13.CommitNegotiatedExtensions(&negotiation.ConnectionID{ServerCID: cid})
+ for versionName, state := range map[string]dtlsstate.Active{"DTLS12": &state12, "DTLS13": &state13} {
+ t.Run(versionName, func(t *testing.T) {
+ conn := &Conn{state: state}
+ version := dtlsstate.CommonState(state).LocalVersion
+ own := marshalCIDPreflightRecord(t, version, cid)
+ unknown := marshalCIDPreflightRecord(t, version, []byte("missing!"))
+ prefix := marshalCIDPreflightRecord(t, version, nil)
+ for _, testCase := range []struct {
+ name string
+ records [][]byte
+ want int
+ }{
+ {name: "MatchingCID", records: [][]byte{prefix, own}, want: 2},
+ {name: "UnknownSuffix", records: [][]byte{prefix, own, unknown, own}, want: 2},
+ {name: "UnknownFirstCID", records: [][]byte{prefix, unknown, own}},
+ } {
+ t.Run(testCase.name, func(t *testing.T) {
+ records, unpackErr := conn.unpackDatagram(bytes.Join(testCase.records, nil))
+ if testCase.want == 0 {
+ assert.ErrorIs(t, unpackErr, dtlserrors.ErrInvalidCiphertextHeader)
+ assert.Empty(t, records)
+
+ return
+ }
+ assert.NoError(t, unpackErr)
+ assert.Equal(t, testCase.records[:testCase.want], records)
+ })
+ }
+ })
+ }
+}
+
+func marshalCIDPreflightRecord(t *testing.T, version protocol.Version, cid []byte) []byte {
+ t.Helper()
+ if version == protocol.Version1_3 {
+ raw, err := recordlayer.MarshalCiphertext(recordlayer.CiphertextConfig{ConnectionID: cid, SequenceNumber: 1, TwoByteSequence: true, LengthPresent: true}, make([]byte, 16))
+ assert.NoError(t, err)
+
+ return raw
+ }
+ if len(cid) == 0 {
+ raw, err := marshalTestRecord(recordlayer.RecordConfig{Version: version}, &protocol.ChangeCipherSpec{})
+ assert.NoError(t, err)
+
+ return raw
+ }
+ raw, err := recordlayer.MarshalRecord(recordlayer.RecordConfig{
+ ContentType: protocol.ContentTypeConnectionID, Version: version,
+ Epoch: 1, SequenceNumber: 1, ConnectionID: cid,
+ }, make([]byte, 16))
+ assert.NoError(t, err)
+
+ return raw
+}
+
+func pendingCIDTestOffer(t *testing.T, conn *Conn) (*dtlsstate.State13, *dtlsstate.TrafficKeyState) {
+ t.Helper()
+ state, err := dtlsstate.As13(conn.state)
+ assert.NoError(t, err)
+ trafficKeys := state.TrafficKeys
+ state.TrafficKeys = nil
+ state.SetRemoteEpoch(dtlsflight13.EpochInitial)
+ _, snapshot, err := negotiation.FinalizeClientHello(&handshake.MessageClientHello{
+ Version: protocol.Version1_2, CipherSuiteIDs: []uint16{uint16(cryptosuite.TLS_AES_128_GCM_SHA256)},
+ Extensions: []extension.Value{
+ &extension13.OfferedVersions{Versions: []protocol.Version{protocol.Version1_3}},
+ &extension.SignatureAlgorithms{Schemes: []uint16{0x0403}},
+ &extension.SupportedGroups{Groups: []elliptic.Curve{elliptic.X25519}},
+ &extension13.ClientKeyShare{},
+ &extension.ConnectionID{CID: []byte{0xc1}},
+ },
+ }, nil)
+ assert.NoError(t, err)
+ assert.NoError(t, state.RecordLocalClientHello(snapshot))
+
+ return state, trafficKeys
+}
+
+func pendingCIDTestRecord(t *testing.T, peer *testRecordProtection13, cid []byte, sequence uint16) []byte {
+ t.Helper()
+ record, err := peer.SealRecord(recordlayer.CiphertextConfig{EpochLow: 2, ConnectionID: cid}, uint64(sequence), protocol.ContentTypeHandshake, encryptedExtensionsHandshakeWithSequence(t, sequence))
+ assert.NoError(t, err)
+ raw, err := record.Marshal()
+ assert.NoError(t, err)
+
+ return raw
+}
+
+func TestPendingCIDDatagramFinalDecision(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ response *extension.ConnectionID
+ wantWithoutCID int
+ wantWithCID int
+ }{
+ {name: "omitted", wantWithoutCID: 2, wantWithCID: 1},
+ {name: "present empty", response: &extension.ConnectionID{}, wantWithCID: 2},
+ {name: "present nonempty", response: &extension.ConnectionID{CID: []byte{0x51}}, wantWithCID: 2},
+ } {
+ for _, withCID := range []bool{false, true} {
+ name := test.name + "/CID-less"
+ wantRecords := test.wantWithoutCID
+ if withCID {
+ name = test.name + "/CID-bearing"
+ wantRecords = test.wantWithCID
+ }
+ t.Run(name, func(t *testing.T) {
+ conn, peer := newTestConnWithReadProtection(t)
+ state, trafficKeys := pendingCIDTestOffer(t, conn)
+ var finalCID []byte
+ if withCID {
+ finalCID = []byte{0xc1}
+ }
+ first := pendingCIDTestRecord(t, peer, nil, 0)
+ second := pendingCIDTestRecord(t, peer, finalCID, 1)
+ datagram := append(bytes.Clone(first), second...)
+ assert.False(t, conn.hasInboundRecordProtection())
+ summary, err := conn.processDatagram(t.Context(), datagram, nil, &readBufferLease{conn: conn})
+ assert.NoError(t, err)
+ assert.False(t, summary.containsHandshake)
+ assert.Len(t, conn.encryptedPackets, 2)
+ for _, packet := range conn.encryptedPackets {
+ assert.True(t, packet.pendingCID)
+ assert.Equal(t, withCID, packet.datagramContainsCID)
+ }
+ clear(datagram)
+ assert.Equal(t, first, conn.encryptedPackets[0].data)
+ assert.Equal(t, second, conn.encryptedPackets[1].data)
+ assert.NoError(t, conn.handleQueuedPackets(t.Context()))
+ assert.Len(t, conn.encryptedPackets, 2)
+ _, opened := state.HighestRemoteSequenceNumber(dtlsflight13.EpochHandshake)
+ assert.False(t, opened)
+ assert.Empty(t, conn.pendingACKs)
+
+ var response []extension.Value
+ if test.response != nil {
+ response = append(response, test.response)
+ }
+ state.CommitNegotiatedExtensions(negotiation.DecideConnectionID(state.LocalClientHelloSnapshots.Current(), response))
+ state.TrafficKeys = trafficKeys
+ state.SetRemoteEpoch(dtlsflight13.EpochHandshake)
+ assert.True(t, conn.hasInboundRecordProtection())
+ assert.NoError(t, conn.handleQueuedPackets(t.Context()))
+ assert.Empty(t, conn.encryptedPackets)
+ for sequence := range uint16(2) {
+ _, delivered := conn.handshakeCache.PullExact(sequence, false)
+ assert.Equal(t, int(sequence) < wantRecords, delivered)
+ }
+ highest, opened := state.HighestRemoteSequenceNumber(dtlsflight13.EpochHandshake)
+ assert.Equal(t, wantRecords > 0, opened)
+ if opened {
+ assert.EqualValues(t, wantRecords-1, highest)
+ }
+ assert.Len(t, conn.pendingACKs, wantRecords)
+ for sequence, ack := range conn.pendingACKs {
+ assert.EqualValues(t, dtlsflight13.EpochHandshake, ack.Epoch)
+ assert.EqualValues(t, sequence, ack.SequenceNumber)
+ }
+ })
+ }
+ }
+}
+
+func TestDynamicCIDAcceptance(t *testing.T) {
+ state := dtlsstate.NewState13(false)
+ state.CommitNegotiatedExtensions(&negotiation.ConnectionID{ServerCID: []byte("original")})
+ conn := &Conn{state: &state, closed: closer.NewCloser()}
+ alias := []byte("newalias")
+ added, err := conn.reserveLocalCIDs([][]byte{alias, alias, []byte("original")})
+ assert.NoError(t, err)
+ assert.Len(t, added, 1)
+ alias[0] = 'X'
+ for _, cid := range []string{"original", "newalias"} {
+ raw := marshalCIDPreflightRecord(t, protocol.Version1_3, []byte(cid))
+ records, unpackErr := conn.unpackDatagram(raw)
+ assert.NoError(t, unpackErr)
+ assert.Len(t, records, 1)
+ _, err = conn.unmarshalCiphertextRecord(raw, false)
+ assert.NoError(t, err)
+ }
+ unknown := marshalCIDPreflightRecord(t, protocol.Version1_3, []byte("unknown!"))
+ _, err = conn.unpackDatagram(unknown)
+ assert.ErrorIs(t, err, dtlserrors.ErrInvalidCiphertextHeader)
+ _, err = conn.unmarshalCiphertextRecord(unknown, false)
+ assert.ErrorIs(t, err, dtlserrors.ErrInvalidCiphertextHeader)
+ prefix := marshalCIDPreflightRecord(t, protocol.Version1_3, nil)
+ aliasRecord := marshalCIDPreflightRecord(t, protocol.Version1_3, added[0])
+ records, err := conn.unpackDatagram(bytes.Join([][]byte{prefix, aliasRecord}, nil))
+ assert.NoError(t, err)
+ assert.Len(t, records, 2)
+ _, err = conn.unmarshalCiphertextRecord(prefix, true)
+ assert.NoError(t, err)
+
+ _, err = conn.reserveLocalCIDs([][]byte{[]byte("rollback"), []byte("short")})
+ assert.ErrorIs(t, err, dtlserrors.ErrInvalidConnectionIDLength)
+ assert.False(t, conn.acceptsInboundCID([]byte("rollback")))
+ conn.removeLocalCIDs(added)
+ assert.False(t, conn.acceptsInboundCID([]byte("newalias")))
+ assert.True(t, conn.acceptsInboundCID([]byte("original")))
+
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for range 100 {
+ _, _ = conn.unpackDatagram(aliasRecord)
+ _, _ = conn.unmarshalCiphertextRecord(aliasRecord, false)
+ }
+ }()
+ for range 100 {
+ added, err = conn.reserveLocalCIDs([][]byte{[]byte("newalias")})
+ assert.NoError(t, err)
+ conn.removeLocalCIDs(added)
+ }
+ wg.Wait()
+ conn.closed.Close()
+ _, err = conn.reserveLocalCIDs([][]byte{[]byte("newalias")})
+ assert.ErrorIs(t, err, ErrConnClosed)
+}
+
+func TestPeerConnectionIDSelection(t *testing.T) {
+ state := dtlsstate.NewState13(false)
+ state.CommitNegotiatedExtensions(&negotiation.ConnectionID{ServerCID: []byte("local"), ClientCID: []byte("peer")})
+ conn := &Conn{state: &state, closed: closer.NewCloser()}
+ adapter := handshakeConn{conn: conn}
+ spare := []byte("a-longer-peer-id")
+ require.NoError(t, adapter.CommitPeerConnectionIDs(&handshake.MessageNewConnectionID{
+ Usage: handshake.ConnectionIDSpare, CIDs: [][]byte{spare, spare, nil},
+ }))
+ spare[0] = 'X'
+ assert.Equal(t, []byte("peer"), state.CID.Send.Active)
+ assert.Equal(t, [][]byte{[]byte("a-longer-peer-id"), nil}, state.CID.Send.Spares)
+ require.NoError(t, adapter.CommitPeerConnectionIDs(&handshake.MessageNewConnectionID{Usage: handshake.ConnectionIDImmediate, CIDs: [][]byte{nil}}))
+ assert.False(t, state.CID.Send.UseCID)
+ assert.Empty(t, state.CID.Send.Active)
+ assert.Equal(t, [][]byte{[]byte("a-longer-peer-id")}, state.CID.Send.Spares)
+ for i := range dtlsstate.MaxConnectionIDs + 5 {
+ require.NoError(t, adapter.CommitPeerConnectionIDs(&handshake.MessageNewConnectionID{
+ Usage: handshake.ConnectionIDSpare, CIDs: [][]byte{{byte(i)}}, //nolint:gosec
+ }))
+ }
+ assert.Len(t, state.CID.Send.Spares, dtlsstate.MaxConnectionIDs)
+ require.NoError(t, adapter.CommitPeerConnectionIDs(&handshake.MessageNewConnectionID{Usage: handshake.ConnectionIDImmediate, CIDs: [][]byte{[]byte("restored")}}))
+ assert.True(t, state.CID.Send.UseCID)
+ assert.Equal(t, []byte("restored"), state.CID.Send.Active)
+}
diff --git a/detached_conn.go b/detached_conn.go
new file mode 100644
index 000000000..e27a64ccf
--- /dev/null
+++ b/detached_conn.go
@@ -0,0 +1,470 @@
+// SPDX-FileCopyrightText: 2026 The Pion community
+// SPDX-License-Identifier: MIT
+
+package dtls
+
+import (
+ "context"
+ "errors"
+ "net"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ dtlsconfig "github.com/pion/dtls/v4/internal/config"
+ dtlserrors "github.com/pion/dtls/v4/internal/errors"
+ dtlsflight "github.com/pion/dtls/v4/internal/flight"
+ dtlsflight13 "github.com/pion/dtls/v4/internal/flight/flight13"
+ dtlsstate "github.com/pion/dtls/v4/internal/state"
+ "github.com/pion/dtls/v4/pkg/protocol"
+ "github.com/pion/dtls/v4/pkg/protocol/alert"
+)
+
+// DetachedEventKind identifies an event produced by a DetachedConn.
+type DetachedEventKind uint8
+
+const (
+ // DetachedNoEvent indicates that no event is available.
+ DetachedNoEvent DetachedEventKind = iota
+ // DetachedWriteDatagrams provides one complete DTLS output batch in
+ // Datagrams and its destination in Addr.
+ DetachedWriteDatagrams
+ // DetachedApplicationData provides plaintext received from the peer in Data.
+ // Servers opting into early data may receive it before DetachedHandshakeDone.
+ DetachedApplicationData
+ // DetachedHandshakeDone indicates that the handshake has completed.
+ DetachedHandshakeDone
+ // DetachedClosed indicates that the connection has terminated with Err.
+ DetachedClosed
+ // DetachedEarlyDataReady indicates that the client may call WriteEarlyData.
+ // 0-RTT only.
+ DetachedEarlyDataReady
+ // DetachedEarlyDataAccepted indicates that the server accepted the client's
+ // early data. It does not indicate delivery or handshake completion.
+ // 0-RTT only.
+ DetachedEarlyDataAccepted
+ // DetachedEarlyDataRejected indicates that the server rejected the client's
+ // early data. The application decides whether to retry after the handshake.
+ // 0-RTT only.
+ DetachedEarlyDataRejected
+)
+
+// DetachedEvent is an event produced by a DetachedConn. Fields are populated
+// according to Kind. Its byte slices are owned by the caller.
+type DetachedEvent struct {
+ Kind DetachedEventKind
+
+ Datagrams [][]byte
+ Data []byte
+ Addr net.Addr
+ Err error
+}
+
+// DetachedConn is a DTLS connection whose datagram transport is supplied by
+// its caller. It remains active for application and post-handshake traffic.
+// This API is inspired by crypto/tls.QUICConn, Start, HandleDatagram, Write, and Close must not be
+// called concurrently with one another. EventReady may be selected concurrently.
+// After a driving method returns, call NextEvent until it returns DetachedNoEvent.
+type DetachedConn struct {
+ conn *Conn
+ inbound chan addrPkt
+ driveMu sync.Mutex
+
+ eventMu sync.Mutex
+ events []DetachedEvent
+ nextEvent int
+ // Match Conn's single-record pre-handshake application buffer.
+ pendingApplicationData *DetachedEvent
+ eventReady chan struct{}
+
+ started bool
+ earlyDataStatus dtlsstate.EarlyDataStatus
+ blocked chan struct{}
+
+ established atomic.Bool
+ quiescentSkip atomic.Bool
+
+ terminalOnce sync.Once
+ terminal chan struct{}
+ terminalErr error
+}
+
+var (
+ errDetachedConnStarted = errors.New("DetachedConn.Start called more than once")
+ errNilContext = errors.New("nil context")
+)
+
+// DetachedClient returns a new detached client connection. Call Start before
+// supplying datagrams.
+func DetachedClient(remoteAddr net.Addr, opts ...ClientOption) (*DetachedConn, error) {
+ if remoteAddr == nil {
+ return nil, dtlserrors.ErrNilRemoteAddr
+ }
+ config, err := buildClientConfig(opts...)
+ if err != nil {
+ return nil, err
+ }
+ if err = validateConfig(config); err != nil {
+ return nil, err
+ }
+
+ return newDetachedConn(remoteAddr, config, true)
+}
+
+// DetachedServer returns a new detached server connection. Call Start before
+// supplying datagrams.
+func DetachedServer(remoteAddr net.Addr, opts ...ServerOption) (*DetachedConn, error) {
+ if remoteAddr == nil {
+ return nil, dtlserrors.ErrNilRemoteAddr
+ }
+ config, err := buildServerConfig(opts...)
+ if err != nil {
+ return nil, err
+ }
+ if err = validateConfig(config); err != nil {
+ return nil, err
+ }
+ if config.OnConnectionAttempt != nil {
+ if err = config.OnConnectionAttempt(remoteAddr); err != nil {
+ return nil, err
+ }
+ }
+
+ return newDetachedConn(remoteAddr, config, false)
+}
+
+func newDetachedConn(remoteAddr net.Addr, config *dtlsConfig, isClient bool) (*DetachedConn, error) {
+ configValues, err := newConnConfigValues(config)
+ if err != nil {
+ return nil, err
+ }
+
+ detached := &DetachedConn{
+ blocked: make(chan struct{}),
+ eventReady: make(chan struct{}, 1),
+ inbound: make(chan addrPkt),
+ terminal: make(chan struct{}),
+ }
+ handshakeConfig := newHandshakeConfig(config, configValues, nil)
+ handshakeConfig.EnableEarlyData = config.maxEarlyDataSize > 0
+ detached.conn = newConn(nil, remoteAddr, configValues, handshakeConfig, isClient)
+ detached.conn.detached = detached
+ detached.conn.handshakeConfig.TimerFactory = detached.newTimer
+ detached.conn.setRemoteEpoch(0)
+ detached.conn.setLocalEpoch(0)
+
+ return detached, nil
+}
+
+// Start starts the handshake and runs it until it needs peer input. It may
+// produce events and must be called at most once.
+func (c *DetachedConn) Start(ctx context.Context) error {
+ if ctx == nil {
+ return errNilContext
+ }
+ c.driveMu.Lock()
+ defer c.driveMu.Unlock()
+
+ if c.started {
+ return errDetachedConnStarted
+ }
+ c.started = true
+
+ go func() {
+ if err := c.conn.HandshakeContext(ctx); err != nil {
+ c.terminate(err, true) //nolint:contextcheck
+ }
+ }()
+
+ return c.waitUntilBlocked()
+}
+
+// HandleDatagram supplies one datagram received from addr and runs DTLS until
+// it is blocked again. It may produce events. A nil addr uses the connection's
+// current remote address. It does not retain datagram after returning.
+func (c *DetachedConn) HandleDatagram(datagram []byte, addr net.Addr) error {
+ c.driveMu.Lock()
+ defer c.driveMu.Unlock()
+
+ if !c.started {
+ return dtlserrors.ErrHandshakeInProgress
+ }
+ if err := c.currentError(); err != nil {
+ return err
+ }
+ if addr == nil {
+ addr = c.conn.RemoteAddr()
+ }
+
+ select {
+ case c.inbound <- addrPkt{rAddr: addr, data: datagram}:
+ case <-c.terminal:
+ return c.terminalErr
+ }
+
+ return c.waitUntilBlocked()
+}
+
+// Write encrypts plaintext application data and may produce events. The
+// handshake must be complete.
+func (c *DetachedConn) Write(data []byte) (int, error) {
+ c.driveMu.Lock()
+ defer c.driveMu.Unlock()
+
+ if err := c.currentError(); err != nil {
+ return 0, err
+ }
+ if !c.established.Load() {
+ return 0, dtlserrors.ErrHandshakeInProgress
+ }
+
+ version := dtlsstate.CommonState(c.conn.state).LocalVersion
+ err := c.conn.writeApplicationData(context.Background(), []*dtlsflight.Outbound{{
+ Content: &protocol.ApplicationData{Data: data},
+ Protection: dtlsflight.ProtectionCiphertext,
+ }})
+ n := 0
+ if err == nil {
+ n = len(data)
+ }
+ if version == protocol.Version1_3 {
+ if quiescentErr := c.waitUntilBlocked(); err == nil {
+ err = quiescentErr
+ }
+ }
+
+ return n, err
+}
+
+// WriteEarlyData sends client application data before handshake completion,
+// after DetachedEarlyDataReady. Early data may be replayed or rejected; a
+// successful write does not imply server acceptance. Rejected data is not
+// automatically resent.
+func (c *DetachedConn) WriteEarlyData(data []byte) (int, error) {
+ c.driveMu.Lock()
+ defer c.driveMu.Unlock()
+ if err := c.currentError(); err != nil {
+ return 0, err
+ }
+ state, ok := c.conn.state.(*dtlsstate.State13)
+ if !ok || !state.IsClient || state.EarlyDataStatus != dtlsstate.EarlyDataReady || state.LocalEpoch() >= dtlsflight13.EpochHandshake {
+ return 0, dtlserrors.ErrHandshakeInProgress
+ }
+ if uint64(len(data)) > uint64(state.EarlyDataLimit)-state.EarlyDataBytes {
+ return 0, dtlserrors.ErrInvalidPacketLength
+ }
+ // count attempts and transport failures.
+ state.EarlyDataBytes += uint64(len(data))
+ _, err := c.conn.writePacketsWithResult(context.Background(), []*dtlsflight.Outbound{{
+ Epoch: dtlsflight13.EpochEarlyData, Protection: dtlsflight.ProtectionCiphertext,
+ Content: &protocol.ApplicationData{Data: data},
+ }})
+ if err != nil {
+ return 0, err
+ }
+
+ return len(data), nil
+}
+
+// EventReady is signaled when NextEvent may return an event and may be selected
+// alongside transport input. Signals are coalesced
+// It's the caller's responsibility to drain NextEvent until it returns DetachedNoEvent.
+func (c *DetachedConn) EventReady() <-chan struct{} { return c.eventReady }
+
+// NextEvent returns the next pending event, or DetachedNoEvent when the queue
+// is empty.
+func (c *DetachedConn) NextEvent() DetachedEvent {
+ c.eventMu.Lock()
+ defer c.eventMu.Unlock()
+
+ if c.nextEvent == len(c.events) {
+ c.events = c.events[:0]
+ c.nextEvent = 0
+ select {
+ case <-c.eventReady:
+ default:
+ }
+
+ return DetachedEvent{Kind: DetachedNoEvent}
+ }
+ event := c.events[c.nextEvent]
+ c.events[c.nextEvent] = DetachedEvent{}
+ c.nextEvent++
+
+ return event
+}
+
+// ConnectionState returns the current DTLS connection state.
+func (c *DetachedConn) ConnectionState() (State, bool) { return c.conn.ConnectionState() }
+
+// SelectedSRTPProtectionProfile returns the negotiated SRTP profile.
+func (c *DetachedConn) SelectedSRTPProtectionProfile() (SRTPProtectionProfile, bool) {
+ return c.conn.SelectedSRTPProtectionProfile()
+}
+
+// RemoteSRTPMasterKeyIdentifier returns the peer's negotiated SRTP MKI.
+func (c *DetachedConn) RemoteSRTPMasterKeyIdentifier() ([]byte, bool) {
+ return c.conn.RemoteSRTPMasterKeyIdentifier()
+}
+
+// Close closes the detached connection.
+func (c *DetachedConn) Close() error {
+ c.driveMu.Lock()
+ defer c.driveMu.Unlock()
+
+ return c.conn.Close()
+}
+
+func (c *DetachedConn) publishDatagrams(datagrams [][]byte, addr net.Addr) {
+ c.publishEvent(DetachedEvent{Kind: DetachedWriteDatagrams, Datagrams: datagrams, Addr: addr})
+}
+
+func (c *DetachedConn) publishApplicationData(data []byte, early bool) {
+ c.eventMu.Lock()
+ defer c.eventMu.Unlock()
+ event := DetachedEvent{Kind: DetachedApplicationData, Data: data}
+ if !early && !c.established.Load() {
+ if c.pendingApplicationData == nil {
+ c.pendingApplicationData = &event
+ }
+
+ return
+ }
+ c.publishEventLocked(event)
+}
+
+func (c *DetachedConn) publishEvent(event DetachedEvent) {
+ c.eventMu.Lock()
+ defer c.eventMu.Unlock()
+ c.publishEventLocked(event)
+}
+
+func (c *DetachedConn) publishEventLocked(event DetachedEvent) {
+ c.events = append(c.events, event)
+ select {
+ case c.eventReady <- struct{}{}:
+ default:
+ }
+}
+
+func (c *DetachedConn) markQuiescent() {
+ c.publishEarlyDataStatus()
+ c.eventMu.Lock()
+ if c.conn.isHandshakeCompletedSuccessfully() && c.established.CompareAndSwap(false, true) {
+ if state, ok := c.conn.state.(*dtlsstate.State13); ok && state.TrafficKeys != nil {
+ state.TrafficKeys.Discard(dtlsflight13.EpochEarlyData)
+ }
+ c.publishEventLocked(DetachedEvent{Kind: DetachedHandshakeDone})
+ if c.pendingApplicationData != nil {
+ c.publishEventLocked(*c.pendingApplicationData)
+ c.pendingApplicationData = nil
+ }
+ }
+
+ c.eventMu.Unlock()
+ select {
+ case c.blocked <- struct{}{}:
+ case <-c.terminal:
+ }
+}
+
+func (c *DetachedConn) waitUntilBlocked() error {
+ select {
+ case <-c.blocked:
+ return c.currentError()
+ case <-c.terminal:
+ return c.terminalErr
+ }
+}
+
+func (c *DetachedConn) connectionTerminated(err error) {
+ var receivedAlert *alertError
+ if errors.As(err, &receivedAlert) && receivedAlert.Description == alert.CloseNotify {
+ err = ErrConnClosed
+ }
+ c.terminate(err, false)
+}
+
+func (c *DetachedConn) terminate(err error, closeConn bool) {
+ if err == nil {
+ err = ErrConnClosed
+ }
+ c.terminalOnce.Do(func() {
+ c.terminalErr = err
+ if closeConn {
+ _ = c.conn.close(false) //nolint:contextcheck
+ }
+ c.eventMu.Lock()
+ c.pendingApplicationData = nil
+ c.publishEventLocked(DetachedEvent{Kind: DetachedClosed, Err: err})
+ c.eventMu.Unlock()
+ close(c.terminal)
+ })
+}
+
+func (c *DetachedConn) currentError() error {
+ select {
+ case <-c.terminal:
+ return c.terminalErr
+ default:
+ return nil
+ }
+}
+
+func (c *DetachedConn) readDatagram(ctx context.Context, buffer []byte) (int, net.Addr, error) {
+ select {
+ case packet := <-c.inbound:
+ return copy(buffer, packet.data), packet.rAddr, nil
+ case <-ctx.Done():
+ return 0, nil, ctx.Err()
+ case <-c.terminal:
+ return 0, nil, c.terminalErr
+ }
+}
+
+func (c *DetachedConn) newTimer(d time.Duration) dtlsconfig.Timer {
+ timer := &detachedTimer{owner: c, ch: make(chan time.Time, 1)}
+ timer.timer = time.AfterFunc(d, timer.fire)
+
+ return timer
+}
+
+type detachedTimer struct {
+ owner *DetachedConn
+ ch chan time.Time
+ timer *time.Timer
+ claimed atomic.Bool
+}
+
+func (t *detachedTimer) C() <-chan time.Time { return t.ch }
+
+func (t *detachedTimer) fire() {
+ t.owner.driveMu.Lock()
+ defer t.owner.driveMu.Unlock()
+ if !t.claimed.CompareAndSwap(false, true) {
+ return
+ }
+
+ t.ch <- time.Now()
+ _ = t.owner.waitUntilBlocked()
+}
+
+func (t *detachedTimer) Stop() {
+ if t.claimed.CompareAndSwap(false, true) {
+ t.timer.Stop()
+ }
+}
+
+func (c *DetachedConn) publishEarlyDataStatus() {
+ if state, ok := c.conn.state.(*dtlsstate.State13); ok && state.IsClient && state.EarlyDataStatus != c.earlyDataStatus {
+ c.earlyDataStatus = state.EarlyDataStatus
+ kind := map[dtlsstate.EarlyDataStatus]DetachedEventKind{
+ dtlsstate.EarlyDataReady: DetachedEarlyDataReady,
+ dtlsstate.EarlyDataAccepted: DetachedEarlyDataAccepted,
+ dtlsstate.EarlyDataRejected: DetachedEarlyDataRejected,
+ }[state.EarlyDataStatus]
+ if kind != DetachedNoEvent {
+ c.publishEvent(DetachedEvent{Kind: kind})
+ }
+ }
+}
diff --git a/dtls.go b/dtls.go
deleted file mode 100644
index 6d5295738..000000000
--- a/dtls.go
+++ /dev/null
@@ -1,5 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-// Package dtls implements Datagram Transport Layer Security (DTLS) 1.2
-package dtls
diff --git a/e2e/Dockerfile b/e2e/Dockerfile
deleted file mode 100644
index be8ea10e1..000000000
--- a/e2e/Dockerfile
+++ /dev/null
@@ -1,27 +0,0 @@
-# SPDX-FileCopyrightText: 2026 The Pion community
-# SPDX-License-Identifier: MIT
-
-FROM docker.io/library/golang:1.26-trixie
-
-# Enable OpenSSL legacy provider for CCM ciphers
-#
-# OpenSSL 3.x moved CCM (and other older ciphers) to the "legacy"
-# provider. By default, only the "default" provider is enabled.
-RUN cat <> /etc/ssl/openssl.cnf
-
-[provider_sect]
-default = default_sect
-legacy = legacy_sect
-
-[default_sect]
-activate = 1
-
-[legacy_sect]
-activate = 1
-
-EOF
-
-COPY . /go/src/github.com/pion/dtls
-WORKDIR /go/src/github.com/pion/dtls/e2e
-
-CMD ["go", "test", "-tags=openssl", "-v", "."]
diff --git a/e2e/e2e.go b/e2e/e2e.go
deleted file mode 100644
index d9ca99795..000000000
--- a/e2e/e2e.go
+++ /dev/null
@@ -1,5 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-// Package e2e contains end to end tests for pion/dtls
-package e2e
diff --git a/e2e/e2e_lossy_test.go b/e2e/e2e_lossy_test.go
deleted file mode 100644
index f2731e63d..000000000
--- a/e2e/e2e_lossy_test.go
+++ /dev/null
@@ -1,242 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package e2e
-
-import (
- "fmt"
- "math/rand"
- "strings"
- "testing"
- "time"
-
- "github.com/pion/dtls/v3"
- "github.com/pion/dtls/v3/pkg/crypto/selfsign"
- dtlsnet "github.com/pion/dtls/v3/pkg/net"
- transportTest "github.com/pion/transport/v4/test"
- "github.com/stretchr/testify/assert"
-)
-
-const (
- flightInterval = time.Millisecond * 100
- lossyTestTimeout = 30 * time.Second
-)
-
-// DTLS Client/Server over a lossy transport, just asserts it can handle at increasing increments
-
-func TestPionE2ELossy(t *testing.T) { //nolint:cyclop
- // Check for leaking routines
- report := transportTest.CheckRoutines(t)
- defer report()
-
- type runResult struct {
- dtlsConn *dtls.Conn
- err error
- }
-
- serverCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- clientCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- for _, test := range []struct {
- LossChanceRange int
- DoClientAuth bool
- CipherSuites []dtls.CipherSuiteID
- MTU int
- DisableServerFlightInterval bool
- }{
- {
- LossChanceRange: 0,
- },
- {
- LossChanceRange: 10,
- },
- {
- LossChanceRange: 20,
- },
- {
- LossChanceRange: 50,
- },
- {
- LossChanceRange: 0,
- DoClientAuth: true,
- },
- {
- LossChanceRange: 10,
- DoClientAuth: true,
- },
- {
- LossChanceRange: 20,
- DoClientAuth: true,
- },
- {
- LossChanceRange: 50,
- DoClientAuth: true,
- },
- {
- LossChanceRange: 0,
- CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA},
- },
- {
- LossChanceRange: 10,
- CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA},
- },
- {
- LossChanceRange: 20,
- CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA},
- },
- {
- LossChanceRange: 50,
- CipherSuites: []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA},
- },
- {
- LossChanceRange: 10,
- MTU: 100,
- DoClientAuth: true,
- },
- {
- LossChanceRange: 20,
- MTU: 100,
- DoClientAuth: true,
- },
- {
- LossChanceRange: 50,
- MTU: 100,
- DoClientAuth: true,
- },
- // Incoming retransmitted handshakes should cause us to retransmit. Disabling the FlightInterval on one side
- // means that a incoming re-transmissions causes the retransmission to be fired
- {
- LossChanceRange: 10,
- DisableServerFlightInterval: true,
- },
- {
- LossChanceRange: 20,
- DisableServerFlightInterval: true,
- },
- {
- LossChanceRange: 50,
- DisableServerFlightInterval: true,
- },
- } {
- var name strings.Builder
- fmt.Fprintf(&name, "Loss%d_MTU%d", test.LossChanceRange, test.MTU)
- if test.DoClientAuth {
- name.WriteString("_WithCliAuth")
- }
- for _, ciph := range test.CipherSuites {
- name.WriteString("_With" + ciph.String())
- }
- if test.DisableServerFlightInterval {
- name.WriteString("_WithNoServerFlightInterval")
- }
-
- test := test
- t.Run(name.String(), func(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := transportTest.TimeOut(lossyTestTimeout + time.Second)
- defer lim.Stop()
-
- chosenLoss := rand.Intn(9) + test.LossChanceRange //nolint:gosec
- serverDone := make(chan runResult)
- clientDone := make(chan runResult)
- br := transportTest.NewBridge()
-
- assert.NoError(t, br.SetLossChance(chosenLoss))
-
- go func() {
- clientOpts := []dtls.ClientOption{
- dtls.WithFlightInterval(flightInterval),
- dtls.WithInsecureSkipVerify(true),
- dtls.WithDisableRetransmitBackoff(true),
- }
- if len(test.CipherSuites) > 0 {
- clientOpts = append(clientOpts, dtls.WithCipherSuites(test.CipherSuites...))
- }
- if test.MTU > 0 {
- clientOpts = append(clientOpts, dtls.WithMTU(test.MTU))
- }
-
- if test.DoClientAuth {
- clientOpts = append(clientOpts, dtls.WithCertificates(clientCert))
- }
-
- client, startupErr := dtls.ClientWithOptions(
- dtlsnet.PacketConnFromConn(br.GetConn0()),
- br.GetConn0().RemoteAddr(),
- clientOpts...,
- )
- clientDone <- runResult{client, startupErr}
- }()
-
- go func() {
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(serverCert),
- dtls.WithFlightInterval(flightInterval),
- dtls.WithDisableRetransmitBackoff(true),
- }
- if test.MTU > 0 {
- serverOpts = append(serverOpts, dtls.WithMTU(test.MTU))
- }
-
- if test.DoClientAuth {
- serverOpts = append(serverOpts, dtls.WithClientAuth(dtls.RequireAnyClientCert))
- }
-
- if test.DisableServerFlightInterval {
- serverOpts = append(serverOpts, dtls.WithFlightInterval(time.Hour))
- }
-
- server, startupErr := dtls.ServerWithOptions(
- dtlsnet.PacketConnFromConn(br.GetConn1()),
- br.GetConn1().RemoteAddr(),
- serverOpts...,
- )
- serverDone <- runResult{server, startupErr}
- }()
-
- testTimer := time.NewTimer(lossyTestTimeout)
- var serverConn, clientConn *dtls.Conn
- defer func() {
- if serverConn != nil {
- assert.NoError(t, serverConn.Close())
- }
- if clientConn != nil {
- assert.NoError(t, clientConn.Close())
- }
- }()
-
- for serverConn == nil || clientConn == nil {
- br.Tick()
- select {
- case serverResult := <-serverDone:
- if serverResult.err != nil {
- assert.Failf(t, "Fail, serverError", "clientComplete(%t) serverComplete(%t) LossChance(%d) error(%v)",
- clientConn != nil, serverConn != nil, chosenLoss, serverResult.err)
-
- return
- }
-
- serverConn = serverResult.dtlsConn
- case clientResult := <-clientDone:
- if clientResult.err != nil {
- assert.Failf(t, "Fail, clientError", "clientComplete(%t) serverComplete(%t) LossChance(%d) error(%v)",
- clientConn != nil, serverConn != nil, chosenLoss, clientResult.err)
-
- return
- }
-
- clientConn = clientResult.dtlsConn
- case <-testTimer.C:
- assert.Failf(t, "Test expired", "clientComplete(%t) serverComplete(%t) LossChance(%d)",
- clientConn != nil, serverConn != nil, chosenLoss)
-
- return
- case <-time.After(10 * time.Millisecond):
- }
- }
- })
- }
-}
diff --git a/e2e/e2e_openssl_test.go b/e2e/e2e_openssl_test.go
deleted file mode 100644
index 797d309ca..000000000
--- a/e2e/e2e_openssl_test.go
+++ /dev/null
@@ -1,362 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-//go:build openssl && !js
-// +build openssl,!js
-
-package e2e
-
-import (
- "crypto/tls"
- "crypto/x509"
- "encoding/pem"
- "errors"
- "fmt"
- "io/ioutil"
- "net"
- "os"
- "os/exec"
- "strings"
- "testing"
- "time"
-
- "github.com/pion/dtls/v3"
-)
-
-func serverOpenSSL(c *comm) {
- go func() {
- c.serverMutex.Lock()
- defer c.serverMutex.Unlock()
-
- // Use information stored in comm struct
- cipherSuites := c.serverCipherSuites
- certs := c.serverCertificates
- psk := c.serverPSK
- pskHint := c.serverPSKIdentityHint
-
- // create openssl arguments
- args := []string{
- "s_server",
- "-dtls1_2",
- "-quiet",
- "-verify_quiet",
- "-verify_return_error",
- fmt.Sprintf("-accept=%d", c.serverPort),
- }
-
- // set ciphers
- //
- // NOTE: as of OpenSSL 3.0, CCM ciphers were moved to the "legacy"
- // provider and require lowering the security level (SECLEVEL) to 0.
- ciphers := ciphersFromSuites(cipherSuites)
- if ciphers != "" {
- args = append(args, fmt.Sprintf("-cipher=%s:@SECLEVEL=0", ciphers))
- } else {
- args = append(args, "-cipher=DEFAULT:@SECLEVEL=0")
- }
-
- // psk arguments
- if psk != nil {
- pskBytes, err := psk(nil)
- if err != nil {
- c.errChan <- err
- return
- }
- args = append(args, fmt.Sprintf("-psk=%X", pskBytes))
- if len(pskHint) > 0 {
- args = append(args, fmt.Sprintf("-psk_hint=%s", pskHint))
- }
- }
-
- // certs arguments
- if len(certs) > 0 {
- // create temporary cert files
- certPEM, keyPEM, err := writeTempPEMFromCerts(certs)
- if err != nil {
- c.errChan <- err
- return
- }
- args = append(args,
- fmt.Sprintf("-cert=%s", certPEM),
- fmt.Sprintf("-key=%s", keyPEM))
- defer func() {
- _ = os.Remove(certPEM)
- _ = os.Remove(keyPEM)
- }()
- } else {
- args = append(args, "-nocert")
- }
-
- // launch command
- // #nosec G204
- cmd := exec.Command("openssl", args...)
- var inner net.Conn
- inner, c.serverConn = net.Pipe()
- cmd.Stdin = inner
- cmd.Stdout = inner
- cmd.Stderr = os.Stderr
- if err := cmd.Start(); err != nil {
- c.errChan <- err
- _ = inner.Close()
- return
- }
-
- // Ensure that server has started
- time.Sleep(500 * time.Millisecond)
-
- c.serverReady <- struct{}{}
- simpleReadWrite(c.errChan, c.serverChan, c.serverConn, c.messageRecvCount)
- c.serverDone <- cmd.Process.Kill()
- close(c.serverDone)
- }()
-}
-
-func clientOpenSSL(c *comm) {
- select {
- case <-c.serverReady:
- // OK
- case <-time.After(time.Second):
- c.errChan <- errors.New("waiting on serverReady err: timeout")
- }
-
- c.clientMutex.Lock()
- defer c.clientMutex.Unlock()
-
- // Use information stored in comm struct
- cipherSuites := c.clientCipherSuites
- certs := c.clientCertificates
- psk := c.clientPSK
- insecureSkipVerify := c.clientInsecureSkipVerify
-
- // create openssl arguments
- args := []string{
- "s_client",
- "-dtls1_2",
- "-quiet",
- "-verify_quiet",
- "-servername=localhost",
- fmt.Sprintf("-connect=127.0.0.1:%d", c.serverPort),
- }
-
- // set ciphers
- //
- // NOTE: as of OpenSSL 3.0, CCM ciphers were moved to the "legacy"
- // provider and require lowering the security level (SECLEVEL) to 0.
- ciphers := ciphersFromSuites(cipherSuites)
- if ciphers != "" {
- args = append(args, fmt.Sprintf("-cipher=%s:@SECLEVEL=0", ciphers))
- } else {
- args = append(args, "-cipher=DEFAULT:@SECLEVEL=0")
- }
-
- // psk arguments
- if psk != nil {
- pskBytes, err := psk(nil)
- if err != nil {
- c.errChan <- err
- return
- }
- args = append(args, fmt.Sprintf("-psk=%X", pskBytes))
- }
-
- // certificate arguments
- if len(certs) > 0 {
- // create temporary cert files
- certPEM, keyPEM, err := writeTempPEMFromCerts(certs)
- if err != nil {
- c.errChan <- err
- return
- }
- args = append(args, fmt.Sprintf("-CAfile=%s", certPEM), fmt.Sprintf("-cert=%s", certPEM), fmt.Sprintf("-key=%s", keyPEM))
- defer func() {
- _ = os.Remove(certPEM)
- _ = os.Remove(keyPEM)
- }()
- }
- if !insecureSkipVerify {
- args = append(args, "-verify_return_error")
- }
-
- // launch command
- // #nosec G204
- cmd := exec.Command("openssl", args...)
- var inner net.Conn
- inner, c.clientConn = net.Pipe()
- cmd.Stdin = inner
- cmd.Stdout = inner
- cmd.Stderr = os.Stderr
- if err := cmd.Start(); err != nil {
- c.errChan <- err
- _ = inner.Close()
- return
- }
-
- simpleReadWrite(c.errChan, c.clientChan, c.clientConn, c.messageRecvCount)
- c.clientDone <- cmd.Process.Kill()
- close(c.clientDone)
-}
-
-func ciphersFromSuites(cipherSuites []dtls.CipherSuiteID) string {
- // See https://tls.mbed.org/supported-ssl-ciphersuites
- translate := map[dtls.CipherSuiteID]string{
- dtls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM: "ECDHE-ECDSA-AES128-CCM",
- dtls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8: "ECDHE-ECDSA-AES128-CCM8",
-
- dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256: "ECDHE-ECDSA-AES128-GCM-SHA256",
- dtls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384: "ECDHE-ECDSA-AES256-GCM-SHA384",
-
- dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256: "ECDHE-RSA-AES128-GCM-SHA256",
- dtls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384: "ECDHE-RSA-AES256-GCM-SHA384",
-
- dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: "ECDHE-ECDSA-AES256-SHA",
- dtls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: "ECDHE-RSA-AES256-SHA",
-
- dtls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: "ECDHE-ECDSA-CHACHA20-POLY1305",
- dtls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: "ECDHE-RSA-CHACHA20-POLY1305",
- dtls.TLS_PSK_WITH_CHACHA20_POLY1305_SHA256: "PSK-CHACHA20-POLY1305",
-
- dtls.TLS_PSK_WITH_AES_128_CCM: "PSK-AES128-CCM",
- dtls.TLS_PSK_WITH_AES_128_CCM_8: "PSK-AES128-CCM8",
- dtls.TLS_PSK_WITH_AES_256_CCM_8: "PSK-AES256-CCM8",
-
- dtls.TLS_PSK_WITH_AES_128_GCM_SHA256: "PSK-AES128-GCM-SHA256",
-
- dtls.TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256: "ECDHE-PSK-AES128-CBC-SHA256",
- }
-
- var ciphers []string
- for _, c := range cipherSuites {
- if text, ok := translate[c]; ok {
- ciphers = append(ciphers, text)
- }
- }
- return strings.Join(ciphers, ";")
-}
-
-func writeTempPEMFromCerts(certs []tls.Certificate) (string, string, error) {
- if len(certs) == 0 {
- return "", "", fmt.Errorf("no certificates provided")
- }
-
- certOut, err := ioutil.TempFile("", "cert.pem")
- if err != nil {
- return "", "", fmt.Errorf("failed to create temporary file: %w", err)
- }
- keyOut, err := ioutil.TempFile("", "key.pem")
- if err != nil {
- return "", "", fmt.Errorf("failed to create temporary file: %w", err)
- }
-
- cert := certs[0]
- derBytes := cert.Certificate[0]
- if err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
- return "", "", fmt.Errorf("failed to write data to cert.pem: %w", err)
- }
- if err = certOut.Close(); err != nil {
- return "", "", fmt.Errorf("error closing cert.pem: %w", err)
- }
-
- priv := cert.PrivateKey
- var privBytes []byte
- privBytes, err = x509.MarshalPKCS8PrivateKey(priv)
- if err != nil {
- return "", "", fmt.Errorf("unable to marshal private key: %w", err)
- }
- if err = pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
- return "", "", fmt.Errorf("failed to write data to key.pem: %w", err)
- }
- if err = keyOut.Close(); err != nil {
- return "", "", fmt.Errorf("error closing key.pem: %w", err)
- }
- return certOut.Name(), keyOut.Name(), nil
-}
-
-func TestPionOpenSSLE2ESimple(t *testing.T) {
- t.Run("OpenSSLServer", func(t *testing.T) {
- testPionE2ESimple(t, serverOpenSSL, clientPion)
- })
- t.Run("OpenSSLClient", func(t *testing.T) {
- testPionE2ESimple(t, serverPion, clientOpenSSL)
- })
-}
-
-func TestPionOpenSSLE2ESimplePSK(t *testing.T) {
- t.Run("OpenSSLServer", func(t *testing.T) {
- testPionE2ESimplePSK(t, serverOpenSSL, clientPion)
- })
- t.Run("OpenSSLClient", func(t *testing.T) {
- testPionE2ESimplePSK(t, serverPion, clientOpenSSL)
- })
-}
-
-func TestPionOpenSSLE2EMTUs(t *testing.T) {
- t.Run("OpenSSLServer", func(t *testing.T) {
- testPionE2EMTUs(t, serverOpenSSL, clientPion)
- })
- t.Run("OpenSSLClient", func(t *testing.T) {
- testPionE2EMTUs(t, serverPion, clientOpenSSL)
- })
-}
-
-func TestPionOpenSSLE2ESimpleED25519(t *testing.T) {
- t.Run("OpenSSLServer", func(t *testing.T) {
- t.Skip("OpenSSL 3.x does not support ED25519 certificates with ECDHE_ECDSA cipher suites as server: https://github.com/openssl/openssl/issues/20122")
- })
- t.Run("OpenSSLClient", func(t *testing.T) {
- testPionE2ESimpleED25519(t, serverPion, clientOpenSSL)
- })
-}
-
-func TestPionOpenSSLE2ESimpleED25519ClientCert(t *testing.T) {
- t.Run("OpenSSLServer", func(t *testing.T) {
- t.Skip("OpenSSL 3.x does not support ED25519 certificates with ECDHE_ECDSA cipher suites as server: https://github.com/openssl/openssl/issues/20122")
- })
- t.Run("OpenSSLClient", func(t *testing.T) {
- testPionE2ESimpleED25519ClientCert(t, serverPion, clientOpenSSL)
- })
-}
-
-func TestPionOpenSSLE2ESimpleECDSAClientCert(t *testing.T) {
- t.Run("OpenSSLServer", func(t *testing.T) {
- testPionE2ESimpleECDSAClientCert(t, serverOpenSSL, clientPion)
- })
- t.Run("OpenSSLClient", func(t *testing.T) {
- testPionE2ESimpleECDSAClientCert(t, serverPion, clientOpenSSL)
- })
-}
-
-func TestPionOpenSSLE2ESimpleRSA(t *testing.T) {
- t.Run("OpenSSLServer", func(t *testing.T) {
- testPionE2ESimpleRSA(t, serverOpenSSL, clientPion)
- })
- t.Run("OpenSSLClient", func(t *testing.T) {
- testPionE2ESimpleRSA(t, serverPion, clientOpenSSL)
- })
-}
-
-func TestPionOpenSSLE2ESimpleRSAClientCert(t *testing.T) {
- t.Run("OpenSSLServer", func(t *testing.T) {
- testPionE2ESimpleRSAClientCert(t, serverOpenSSL, clientPion)
- })
- t.Run("OpenSSLClient", func(t *testing.T) {
- testPionE2ESimpleRSAClientCert(t, serverPion, clientOpenSSL)
- })
-}
-
-func TestPionOpenSSLE2EChaCha20Poly1305ECDSA(t *testing.T) {
- t.Run("OpenSSLServer", func(t *testing.T) {
- testPionE2EChaCha20Poly1305(t, serverOpenSSL, clientPion)
- })
- t.Run("OpenSSLClient", func(t *testing.T) {
- testPionE2EChaCha20Poly1305(t, serverPion, clientOpenSSL)
- })
-}
-
-func TestPionOpenSSLE2EChaCha20Poly1305RSA(t *testing.T) {
- t.Run("OpenSSLServer", func(t *testing.T) {
- testPionE2EChaCha20Poly1305RSA(t, serverOpenSSL, clientPion)
- })
- t.Run("OpenSSLClient", func(t *testing.T) {
- testPionE2EChaCha20Poly1305RSA(t, serverPion, clientOpenSSL)
- })
-}
diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go
deleted file mode 100644
index b6f3809ab..000000000
--- a/e2e/e2e_test.go
+++ /dev/null
@@ -1,1676 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-//go:build !js
-
-package e2e
-
-import (
- "context"
- "crypto"
- "crypto/ecdsa"
- "crypto/ed25519"
- "crypto/elliptic"
- "crypto/rand"
- "crypto/rsa"
- "crypto/tls"
- "crypto/x509"
- "crypto/x509/pkix"
- "errors"
- "fmt"
- "io"
- "math/big"
- "net"
- "sync"
- "sync/atomic"
- "testing"
- "time"
-
- "github.com/pion/dtls/v3"
- "github.com/pion/dtls/v3/pkg/crypto/selfsign"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/transport/v4/test"
- "github.com/stretchr/testify/assert"
-)
-
-const (
- testMessage = "Hello World"
- testTimeLimit = 5 * time.Second
- messageRetry = 200 * time.Millisecond
-)
-
-var (
- errServerTimeout = errors.New("waiting on serverReady err: timeout")
- errHookCiphersFailed = errors.New("hook failed to modify cipherlist")
- errHookAPLNFailed = errors.New("hook failed to modify APLN extension")
-)
-
-func randomPort(tb testing.TB) int {
- tb.Helper()
- conn, err := net.ListenPacket("udp4", "127.0.0.1:0") // nolint: noctx
- assert.NoError(tb, err, "failed to pick port")
-
- defer func() {
- _ = conn.Close()
- }()
- switch addr := conn.LocalAddr().(type) {
- case *net.UDPAddr:
- return addr.Port
- default:
- assert.Fail(tb, "failed to acquire port", "unknown addr type %T", addr)
-
- return 0
- }
-}
-
-func simpleReadWrite(errChan chan error, outChan chan string, conn io.ReadWriter, messageRecvCount *uint64) {
- go func() {
- buffer := make([]byte, 8192)
- n, err := conn.Read(buffer)
- if err != nil {
- errChan <- err
-
- return
- }
-
- outChan <- string(buffer[:n])
- atomic.AddUint64(messageRecvCount, 1)
- }()
-
- for {
- if atomic.LoadUint64(messageRecvCount) == 2 {
- break
- } else if _, err := conn.Write([]byte(testMessage)); err != nil {
- errChan <- err
-
- break
- }
-
- time.Sleep(messageRetry)
- }
-}
-
-type comm struct {
- ctx context.Context //nolint:containedctx
- clientOpts []dtls.ClientOption
- serverOpts []dtls.ServerOption
- // OpenSSL test helpers need this information
- clientCipherSuites []dtls.CipherSuiteID
- serverCipherSuites []dtls.CipherSuiteID
- clientCertificates []tls.Certificate
- serverCertificates []tls.Certificate
- clientPSK dtls.PSKCallback
- serverPSK dtls.PSKCallback
- clientPSKIdentityHint []byte
- serverPSKIdentityHint []byte
- clientInsecureSkipVerify bool
- serverPort int
- messageRecvCount *uint64 // Counter to make sure both sides got a message
- clientMutex *sync.Mutex
- clientConn net.Conn
- clientDone chan error
- serverMutex *sync.Mutex
- serverConn net.Conn
- serverListener net.Listener
- serverReady chan struct{}
- serverDone chan error
- errChan chan error
- clientChan chan string
- serverChan chan string
- client func(*comm)
- server func(*comm)
-}
-
-func newComm(
- ctx context.Context,
- clientOpts []dtls.ClientOption,
- serverOpts []dtls.ServerOption,
- serverPort int,
- server, client func(*comm),
-) *comm {
- messageRecvCount := uint64(0)
-
- com := &comm{
- ctx: ctx,
- clientOpts: clientOpts,
- serverOpts: serverOpts,
- serverPort: serverPort,
- messageRecvCount: &messageRecvCount,
- clientMutex: &sync.Mutex{},
- serverMutex: &sync.Mutex{},
- serverReady: make(chan struct{}),
- serverDone: make(chan error),
- clientDone: make(chan error),
- errChan: make(chan error),
- clientChan: make(chan string),
- serverChan: make(chan string),
- server: server,
- client: client,
- }
-
- return com
-}
-
-// setOpenSSLInfo sets OpenSSL-specific information in the comm struct.
-// This is called by test functions that have this information available.
-func (c *comm) setOpenSSLInfo(
- clientCipherSuites []dtls.CipherSuiteID,
- serverCipherSuites []dtls.CipherSuiteID,
- clientCertificates []tls.Certificate,
- serverCertificates []tls.Certificate,
- clientPSK dtls.PSKCallback,
- serverPSK dtls.PSKCallback,
- clientPSKIdentityHint []byte,
- serverPSKIdentityHint []byte,
- clientInsecureSkipVerify bool,
-) {
- c.clientCipherSuites = clientCipherSuites
- c.serverCipherSuites = serverCipherSuites
- c.clientCertificates = clientCertificates
- c.serverCertificates = serverCertificates
- c.clientPSK = clientPSK
- c.serverPSK = serverPSK
- c.clientPSKIdentityHint = clientPSKIdentityHint
- c.serverPSKIdentityHint = serverPSKIdentityHint
- c.clientInsecureSkipVerify = clientInsecureSkipVerify
-}
-
-func (c *comm) assert(t *testing.T) { //nolint:cyclop
- t.Helper()
-
- // DTLS Client
- go c.client(c)
-
- // DTLS Server
- go c.server(c)
-
- defer func() {
- if c.clientConn != nil {
- assert.NoError(t, c.clientConn.Close())
- }
- if c.serverConn != nil {
- assert.NoError(t, c.serverConn.Close())
- }
- if c.serverListener != nil {
- assert.NoError(t, c.serverListener.Close())
- }
- }()
-
- func() {
- seenClient, seenServer := false, false
- for {
- select {
- case err := <-c.errChan:
- assert.NoError(t, err)
- case <-time.After(testTimeLimit):
- assert.Failf(t, "Test timeout", "seenClient %t seenServer %t", seenClient, seenServer)
- case clientMsg := <-c.clientChan:
- assert.Equal(t, testMessage, clientMsg)
-
- seenClient = true
- if seenClient && seenServer {
- return
- }
- case serverMsg := <-c.serverChan:
- assert.Equal(t, testMessage, serverMsg)
-
- seenServer = true
- if seenClient && seenServer {
- return
- }
- }
- }
- }()
-}
-
-func (c *comm) cleanup(t *testing.T) {
- t.Helper()
-
- clientDone, serverDone := false, false
- for {
- select {
- case err := <-c.clientDone:
- assert.NoError(t, err)
- clientDone = true
- if clientDone && serverDone {
- return
- }
- case err := <-c.serverDone:
- assert.NoError(t, err)
- serverDone = true
- if clientDone && serverDone {
- return
- }
- case <-time.After(testTimeLimit):
- assert.Fail(t, "Test timeout waiting for server shutdown")
- }
- }
-}
-
-func clientPion(c *comm) { //nolint:varnamelen
- select {
- case <-c.serverReady:
- // OK
- case <-time.After(time.Second):
- c.errChan <- errServerTimeout
- }
-
- c.clientMutex.Lock()
- defer c.clientMutex.Unlock()
-
- conn, err := dtls.DialWithOptions("udp",
- &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: c.serverPort},
- c.clientOpts...,
- )
- if err != nil {
- c.errChan <- err
-
- return
- }
-
- if err := conn.HandshakeContext(c.ctx); err != nil {
- c.errChan <- err
-
- return
- }
-
- c.clientConn = conn
-
- simpleReadWrite(c.errChan, c.clientChan, c.clientConn, c.messageRecvCount)
- c.clientDone <- nil
- close(c.clientDone)
-}
-
-func serverPion(c *comm) { //nolint:varnamelen
- c.serverMutex.Lock()
- defer c.serverMutex.Unlock()
-
- var err error
- c.serverListener, err = dtls.ListenWithOptions("udp",
- &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: c.serverPort},
- c.serverOpts...,
- )
- if err != nil {
- c.errChan <- err
-
- return
- }
- c.serverReady <- struct{}{}
- c.serverConn, err = c.serverListener.Accept()
- if err != nil {
- c.errChan <- err
-
- return
- }
-
- dtlsConn, ok := c.serverConn.(*dtls.Conn)
- if ok {
- if err := dtlsConn.HandshakeContext(c.ctx); err != nil {
- c.errChan <- err
-
- return
- }
- }
-
- simpleReadWrite(c.errChan, c.serverChan, c.serverConn, c.messageRecvCount)
- c.serverDone <- nil
- close(c.serverDone)
-}
-
-type dtlsTestOpts struct {
- clientOpts []dtls.ClientOption
- serverOpts []dtls.ServerOption
-}
-
-func withConnectionIDGenerator(g func() []byte) dtlsTestOpts {
- return dtlsTestOpts{
- clientOpts: []dtls.ClientOption{dtls.WithConnectionIDGenerator(g)},
- serverOpts: []dtls.ServerOption{dtls.WithConnectionIDGenerator(g)},
- }
-}
-
-// Simple DTLS Client/Server can communicate
-// - Assert that you can send messages both ways
-// - Assert that Close() on both ends work
-// - Assert that no Goroutines are leaked
-//
-//nolint:dupl
-func testPionE2ESimple(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- for _, cipherSuite := range []dtls.CipherSuiteID{
- dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- dtls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
- dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
- } {
- t.Run(cipherSuite.String(), func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeLimit)
- defer cancel()
-
- cert, err := selfsign.GenerateSelfSignedWithDNS("localhost")
- assert.NoError(t, err)
-
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(cipherSuite),
- dtls.WithInsecureSkipVerify(true),
- }
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(cipherSuite),
- dtls.WithInsecureSkipVerify(true),
- }
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{cipherSuite}, []dtls.CipherSuiteID{cipherSuite},
- []tls.Certificate{cert}, []tls.Certificate{cert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
- })
- }
-}
-
-//nolint:dupl
-func testPionE2ESimpleRSA(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- for _, cipherSuite := range []dtls.CipherSuiteID{
- dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
- dtls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
- dtls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
- } {
- t.Run(cipherSuite.String(), func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeLimit)
- defer cancel()
-
- priv, err := rsa.GenerateKey(rand.Reader, 2048)
- assert.NoError(t, err)
- cert, err := selfsign.SelfSign(priv)
- assert.NoError(t, err)
-
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(cipherSuite),
- dtls.WithInsecureSkipVerify(true),
- }
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(cipherSuite),
- dtls.WithInsecureSkipVerify(true),
- }
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{cipherSuite}, []dtls.CipherSuiteID{cipherSuite},
- []tls.Certificate{cert}, []tls.Certificate{cert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
- })
- }
-}
-
-//nolint:dupl
-func testPionE2EChaCha20Poly1305(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- for _, cipherSuite := range []dtls.CipherSuiteID{
- dtls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
- } {
- t.Run(cipherSuite.String(), func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeLimit)
- defer cancel()
-
- cert, err := selfsign.GenerateSelfSignedWithDNS("localhost")
- assert.NoError(t, err)
-
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(cipherSuite),
- dtls.WithInsecureSkipVerify(true),
- }
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(cipherSuite),
- dtls.WithInsecureSkipVerify(true),
- }
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{cipherSuite}, []dtls.CipherSuiteID{cipherSuite},
- []tls.Certificate{cert}, []tls.Certificate{cert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
- })
- }
-}
-
-//nolint:dupl
-func testPionE2EChaCha20Poly1305RSA(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- for _, cipherSuite := range []dtls.CipherSuiteID{
- dtls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
- } {
- t.Run(cipherSuite.String(), func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeLimit)
- defer cancel()
-
- priv, err := rsa.GenerateKey(rand.Reader, 2048)
- assert.NoError(t, err)
- cert, err := selfsign.SelfSign(priv)
- assert.NoError(t, err)
-
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(cipherSuite),
- dtls.WithInsecureSkipVerify(true),
- }
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(cipherSuite),
- dtls.WithInsecureSkipVerify(true),
- }
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{cipherSuite}, []dtls.CipherSuiteID{cipherSuite},
- []tls.Certificate{cert}, []tls.Certificate{cert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
- })
- }
-}
-
-func testPionE2ESimplePSK(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
-
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- for _, cipherSuite := range []dtls.CipherSuiteID{
- dtls.TLS_PSK_WITH_AES_128_CCM,
- dtls.TLS_PSK_WITH_AES_128_CCM_8,
- dtls.TLS_PSK_WITH_AES_256_CCM_8,
- dtls.TLS_PSK_WITH_AES_128_GCM_SHA256,
- dtls.TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256,
- } {
- t.Run(cipherSuite.String(), func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeLimit)
- defer cancel()
-
- clientOpts := []dtls.ClientOption{
- dtls.WithPSK(func([]byte) ([]byte, error) {
- return []byte{0xAB, 0xC1, 0x23}, nil
- }),
- dtls.WithPSKIdentityHint([]byte{0x01, 0x02, 0x03, 0x04, 0x05}),
- dtls.WithCipherSuites(cipherSuite),
- }
- serverOpts := []dtls.ServerOption{
- dtls.WithPSK(func([]byte) ([]byte, error) {
- return []byte{0xAB, 0xC1, 0x23}, nil
- }),
- dtls.WithPSKIdentityHint([]byte{0x01, 0x02, 0x03, 0x04, 0x05}),
- dtls.WithCipherSuites(cipherSuite),
- }
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- pskFunc := func([]byte) ([]byte, error) {
- return []byte{0xAB, 0xC1, 0x23}, nil
- }
- pskHint := []byte{0x01, 0x02, 0x03, 0x04, 0x05}
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{cipherSuite}, []dtls.CipherSuiteID{cipherSuite},
- nil, nil,
- pskFunc, pskFunc, pskHint, pskHint, false)
- defer comm.cleanup(t)
- comm.assert(t)
- })
- }
-}
-
-func testPionE2EChaCha20Poly1305PSK(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
-
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- cipherSuite := dtls.TLS_PSK_WITH_CHACHA20_POLY1305_SHA256
- t.Run(cipherSuite.String(), func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeLimit)
- defer cancel()
-
- pskFunc := func([]byte) ([]byte, error) {
- return []byte{0xAB, 0xC1, 0x23}, nil
- }
- pskHint := []byte{0x01, 0x02, 0x03}
-
- clientOpts := []dtls.ClientOption{
- dtls.WithPSK(pskFunc),
- dtls.WithPSKIdentityHint(pskHint),
- dtls.WithCipherSuites(cipherSuite),
- }
- serverOpts := []dtls.ServerOption{
- dtls.WithPSK(pskFunc),
- dtls.WithCipherSuites(cipherSuite),
- }
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{cipherSuite}, []dtls.CipherSuiteID{cipherSuite},
- nil, nil,
- pskFunc, pskFunc, pskHint, pskHint, false)
- defer comm.cleanup(t)
- comm.assert(t)
- })
-}
-
-func testPionE2EMTUs(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
-
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- cipherSuite := dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
-
- for _, mtu := range []int{
- 10000,
- 1000,
- 100,
- } {
- t.Run(fmt.Sprintf("MTU%d", mtu), func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- cert, err := selfsign.GenerateSelfSignedWithDNS("localhost")
- assert.NoError(t, err)
-
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(cipherSuite),
- dtls.WithInsecureSkipVerify(true),
- dtls.WithMTU(mtu),
- }
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(cipherSuite),
- dtls.WithInsecureSkipVerify(true),
- dtls.WithMTU(mtu),
- }
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{cipherSuite},
- []dtls.CipherSuiteID{cipherSuite},
- []tls.Certificate{cert}, []tls.Certificate{cert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
- })
- }
-}
-
-func testPionE2ESimpleED25519(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
-
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- for _, cipherSuite := range []dtls.CipherSuiteID{
- dtls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM,
- dtls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8,
- dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
- dtls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
- dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
- } {
- t.Run(cipherSuite.String(), func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- _, key, err := ed25519.GenerateKey(rand.Reader)
- assert.NoError(t, err)
- cert, err := selfsign.SelfSign(key)
- assert.NoError(t, err)
-
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(cipherSuite),
- dtls.WithInsecureSkipVerify(true),
- }
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(cipherSuite),
- dtls.WithInsecureSkipVerify(true),
- }
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{cipherSuite},
- []dtls.CipherSuiteID{cipherSuite},
- []tls.Certificate{cert}, []tls.Certificate{cert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
- })
- }
-}
-
-func testPionE2ESimpleED25519ClientCert(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
-
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- _, skey, err := ed25519.GenerateKey(rand.Reader)
- assert.NoError(t, err)
- scert, err := selfsign.SelfSign(skey)
- assert.NoError(t, err)
-
- _, ckey, err := ed25519.GenerateKey(rand.Reader)
- assert.NoError(t, err)
- ccert, err := selfsign.SelfSign(ckey)
- assert.NoError(t, err)
-
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(ccert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- dtls.WithInsecureSkipVerify(true),
- }
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(scert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- dtls.WithClientAuth(dtls.RequireAnyClientCert),
- }
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
- []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
- []tls.Certificate{ccert}, []tls.Certificate{scert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
-}
-
-func testPionE2ESimpleECDSAClientCert(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
-
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- scert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- ccert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- clientCAs := x509.NewCertPool()
- caCert, err := x509.ParseCertificate(ccert.Certificate[0])
- assert.NoError(t, err)
- clientCAs.AddCert(caCert)
-
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(ccert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- dtls.WithInsecureSkipVerify(true),
- }
- serverOpts := []dtls.ServerOption{
- dtls.WithClientCAs(clientCAs),
- dtls.WithCertificates(scert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- dtls.WithClientAuth(dtls.RequireAnyClientCert),
- }
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
- []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
- []tls.Certificate{ccert}, []tls.Certificate{scert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
-}
-
-func testPionE2ESimpleRSAClientCert(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
-
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- spriv, err := rsa.GenerateKey(rand.Reader, 2048)
- assert.NoError(t, err)
- scert, err := selfsign.SelfSign(spriv)
- assert.NoError(t, err)
-
- cpriv, err := rsa.GenerateKey(rand.Reader, 2048)
- assert.NoError(t, err)
- ccert, err := selfsign.SelfSign(cpriv)
- assert.NoError(t, err)
-
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(ccert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
- dtls.WithInsecureSkipVerify(true),
- }
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(scert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
- dtls.WithClientAuth(dtls.RequireAnyClientCert),
- }
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
- []dtls.CipherSuiteID{dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
- []tls.Certificate{ccert}, []tls.Certificate{scert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
-}
-
-func testPionE2ESimpleClientHelloHook(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
-
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- t.Run("ClientHello hook", func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeLimit)
- defer cancel()
-
- cert, err := selfsign.GenerateSelfSignedWithDNS("localhost")
- assert.NoError(t, err)
-
- modifiedCipher := dtls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
- supportedList := []dtls.CipherSuiteID{
- dtls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM,
- modifiedCipher,
- }
-
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(cert),
- dtls.WithVerifyConnection(func(s *dtls.State) error {
- if s.CipherSuiteID != modifiedCipher {
- return errHookCiphersFailed
- }
-
- return nil
- }),
- dtls.WithCipherSuites(supportedList...),
- dtls.WithClientHelloMessageHook(func(ch handshake.MessageClientHello) handshake.Message {
- ch.CipherSuiteIDs = []uint16{uint16(modifiedCipher)}
-
- return &ch
- }),
- dtls.WithInsecureSkipVerify(true),
- }
-
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(supportedList...),
- dtls.WithInsecureSkipVerify(true),
- }
-
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- comm.setOpenSSLInfo(
- supportedList, supportedList,
- []tls.Certificate{cert}, []tls.Certificate{cert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
- })
-}
-
-func testPionE2ESimpleServerHelloHook(t *testing.T, server, client func(*comm), opts ...dtlsTestOpts) {
- t.Helper()
-
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- t.Run("ServerHello hook", func(t *testing.T) {
- ctx, cancel := context.WithTimeout(context.Background(), testTimeLimit)
- defer cancel()
-
- cert, err := selfsign.GenerateSelfSignedWithDNS("localhost")
- assert.NoError(t, err)
-
- supportedList := []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM}
-
- apln := "APLN"
-
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(cert),
- dtls.WithVerifyConnection(func(s *dtls.State) error {
- if s.NegotiatedProtocol != apln {
- return errHookAPLNFailed
- }
-
- return nil
- }),
- dtls.WithCipherSuites(supportedList...),
- dtls.WithInsecureSkipVerify(true),
- }
-
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(supportedList...),
- dtls.WithServerHelloMessageHook(func(sh handshake.MessageServerHello) handshake.Message {
- sh.Extensions = append(sh.Extensions, &extension.ALPN{
- ProtocolNameList: []string{apln},
- })
-
- return &sh
- }),
- dtls.WithInsecureSkipVerify(true),
- }
-
- for _, o := range opts {
- clientOpts = append(clientOpts, o.clientOpts...)
- serverOpts = append(serverOpts, o.serverOpts...)
- }
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, server, client)
- comm.setOpenSSLInfo(
- supportedList, supportedList,
- []tls.Certificate{cert}, []tls.Certificate{cert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
- })
-}
-
-func TestPionE2ESimple(t *testing.T) {
- testPionE2ESimple(t, serverPion, clientPion)
-}
-
-func TestPionE2ESimpleRSA(t *testing.T) {
- testPionE2ESimpleRSA(t, serverPion, clientPion)
-}
-
-func TestPionE2EChaCha20Poly1305(t *testing.T) {
- testPionE2EChaCha20Poly1305(t, serverPion, clientPion)
-}
-
-func TestPionE2EChaCha20Poly1305RSA(t *testing.T) {
- testPionE2EChaCha20Poly1305RSA(t, serverPion, clientPion)
-}
-
-func TestPionE2ESimplePSK(t *testing.T) {
- testPionE2ESimplePSK(t, serverPion, clientPion)
-}
-
-func TestPionE2EChaCha20Poly1305PSK(t *testing.T) {
- testPionE2EChaCha20Poly1305PSK(t, serverPion, clientPion)
-}
-
-func TestPionE2EMTUs(t *testing.T) {
- testPionE2EMTUs(t, serverPion, clientPion)
-}
-
-func TestPionE2ESimpleED25519(t *testing.T) {
- testPionE2ESimpleED25519(t, serverPion, clientPion)
-}
-
-func TestPionE2ESimpleED25519ClientCert(t *testing.T) {
- testPionE2ESimpleED25519ClientCert(t, serverPion, clientPion)
-}
-
-func TestPionE2ESimpleECDSAClientCert(t *testing.T) {
- testPionE2ESimpleECDSAClientCert(t, serverPion, clientPion)
-}
-
-func TestPionE2ESimpleRSAClientCert(t *testing.T) {
- testPionE2ESimpleRSAClientCert(t, serverPion, clientPion)
-}
-
-func TestPionE2ESimpleCID(t *testing.T) {
- testPionE2ESimple(t, serverPion, clientPion, withConnectionIDGenerator(dtls.RandomCIDGenerator(8)))
-}
-
-func TestPionE2EChaCha20Poly1305CID(t *testing.T) {
- testPionE2EChaCha20Poly1305(t, serverPion, clientPion, withConnectionIDGenerator(dtls.RandomCIDGenerator(8)))
-}
-
-func TestPionE2EChaCha20Poly1305RSACID(t *testing.T) {
- testPionE2EChaCha20Poly1305RSA(t, serverPion, clientPion, withConnectionIDGenerator(dtls.RandomCIDGenerator(8)))
-}
-
-func TestPionE2ESimplePSKCID(t *testing.T) {
- testPionE2ESimplePSK(t, serverPion, clientPion, withConnectionIDGenerator(dtls.RandomCIDGenerator(8)))
-}
-
-func TestPionE2EChaCha20Poly1305PSKCID(t *testing.T) {
- testPionE2EChaCha20Poly1305PSK(t, serverPion, clientPion, withConnectionIDGenerator(dtls.RandomCIDGenerator(8)))
-}
-
-func TestPionE2EMTUsCID(t *testing.T) {
- testPionE2EMTUs(t, serverPion, clientPion, withConnectionIDGenerator(dtls.RandomCIDGenerator(8)))
-}
-
-func TestPionE2ESimpleED25519CID(t *testing.T) {
- testPionE2ESimpleED25519(t, serverPion, clientPion, withConnectionIDGenerator(dtls.RandomCIDGenerator(8)))
-}
-
-func TestPionE2ESimpleED25519ClientCertCID(t *testing.T) {
- testPionE2ESimpleED25519ClientCert(t, serverPion, clientPion, withConnectionIDGenerator(dtls.RandomCIDGenerator(8)))
-}
-
-func TestPionE2ESimpleECDSAClientCertCID(t *testing.T) {
- testPionE2ESimpleECDSAClientCert(t, serverPion, clientPion, withConnectionIDGenerator(dtls.RandomCIDGenerator(8)))
-}
-
-func TestPionE2ESimpleRSAClientCertCID(t *testing.T) {
- testPionE2ESimpleRSAClientCert(t, serverPion, clientPion, withConnectionIDGenerator(dtls.RandomCIDGenerator(8)))
-}
-
-func TestPionE2ESimpleClientHelloHook(t *testing.T) {
- testPionE2ESimpleClientHelloHook(t, serverPion, clientPion)
-}
-
-func TestPionE2ESimpleServerHelloHook(t *testing.T) {
- testPionE2ESimpleServerHelloHook(t, serverPion, clientPion)
-}
-
-// TestCertificateSignatureSchemesAllowed tests that connections succeed when
-// certificate chains use only allowed signature algorithms.
-func TestCertificateSignatureSchemesAllowed(t *testing.T) {
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- // Generate ECDSA certificate (uses ECDSA-SHA256)
- cert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- // Client allows ECDSA signature schemes for certificates
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- dtls.WithInsecureSkipVerify(true),
- dtls.WithCertificateSignatureSchemes(
- tls.ECDSAWithP256AndSHA256,
- tls.ECDSAWithP384AndSHA384,
- tls.ECDSAWithP521AndSHA512,
- ),
- }
-
- // Server allows ECDSA signature schemes for certificates
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- dtls.WithInsecureSkipVerify(true),
- dtls.WithCertificateSignatureSchemes(
- tls.ECDSAWithP256AndSHA256,
- tls.ECDSAWithP384AndSHA384,
- tls.ECDSAWithP521AndSHA512,
- ),
- }
-
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, serverPion, clientPion)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
- []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
- []tls.Certificate{cert}, []tls.Certificate{cert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
-}
-
-// TestCertificateSignatureSchemesRSA tests RSA certificates with signature schemes.
-func TestCertificateSignatureSchemesRSA(t *testing.T) {
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- // Generate RSA certificate
- priv, err := rsa.GenerateKey(rand.Reader, 2048)
- assert.NoError(t, err)
- cert, err := selfsign.SelfSign(priv)
- assert.NoError(t, err)
-
- // Allow RSA-PSS signature schemes for certificates
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
- dtls.WithInsecureSkipVerify(true),
- dtls.WithCertificateSignatureSchemes(
- tls.PSSWithSHA256,
- tls.PSSWithSHA384,
- tls.PSSWithSHA512,
- tls.PKCS1WithSHA256,
- tls.PKCS1WithSHA384,
- tls.PKCS1WithSHA512,
- ),
- }
-
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(cert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
- dtls.WithInsecureSkipVerify(true),
- dtls.WithCertificateSignatureSchemes(
- tls.PSSWithSHA256,
- tls.PSSWithSHA384,
- tls.PSSWithSHA512,
- tls.PKCS1WithSHA256,
- tls.PKCS1WithSHA384,
- tls.PKCS1WithSHA512,
- ),
- }
-
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, serverPion, clientPion)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
- []dtls.CipherSuiteID{dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
- []tls.Certificate{cert}, []tls.Certificate{cert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
-}
-
-// TestCertificateSignatureSchemesClientCert tests certificate signature validation
-// with client certificates using different ECDSA curves.
-func TestCertificateSignatureSchemesClientCert(t *testing.T) {
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- // Server uses P-256 ECDSA
- serverCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- // Client uses P-384 ECDSA
- clientKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
- assert.NoError(t, err)
- clientCert, err := selfsign.SelfSign(clientKey)
- assert.NoError(t, err)
-
- clientCAs := x509.NewCertPool()
- caCert, err := x509.ParseCertificate(clientCert.Certificate[0])
- assert.NoError(t, err)
- clientCAs.AddCert(caCert)
-
- // Both sides accept P-256 and P-384 ECDSA
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(clientCert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- dtls.WithInsecureSkipVerify(true),
- dtls.WithCertificateSignatureSchemes(
- tls.ECDSAWithP256AndSHA256,
- tls.ECDSAWithP384AndSHA384,
- ),
- }
-
- serverOpts := []dtls.ServerOption{
- dtls.WithClientCAs(clientCAs),
- dtls.WithCertificates(serverCert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- dtls.WithClientAuth(dtls.RequireAnyClientCert),
- dtls.WithCertificateSignatureSchemes(
- tls.ECDSAWithP256AndSHA256,
- tls.ECDSAWithP384AndSHA384,
- ),
- }
-
- serverPort := randomPort(t)
- comm := newComm(ctx, clientOpts, serverOpts, serverPort, serverPion, clientPion)
- comm.setOpenSSLInfo(
- []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
- []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
- []tls.Certificate{clientCert}, []tls.Certificate{serverCert},
- nil, nil, nil, nil, true)
- defer comm.cleanup(t)
- comm.assert(t)
-}
-
-// createCertChain creates a certificate chain with a root CA and a leaf certificate
-// signed by the CA. The CA uses the same key type as the leaf to ensure consistent
-// signature algorithms in the chain. This allows testing signature algorithm validation.
-func createCertChain(t *testing.T, leafKeyType string) (tls.Certificate, *x509.CertPool) {
- t.Helper()
-
- // Create root CA with matching key type
- var caKey crypto.Signer
- var caPubKey crypto.PublicKey
- var err error
-
- switch leafKeyType {
- case "ecdsa-p256":
- var k *ecdsa.PrivateKey
- k, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
- assert.NoError(t, err)
- caKey = k
- caPubKey = &k.PublicKey
- case "ecdsa-p384":
- var k *ecdsa.PrivateKey
- k, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
- assert.NoError(t, err)
- caKey = k
- caPubKey = &k.PublicKey
- case "rsa":
- var k *rsa.PrivateKey
- k, err = rsa.GenerateKey(rand.Reader, 2048)
- assert.NoError(t, err)
- caKey = k
- caPubKey = &k.PublicKey
- default:
- assert.FailNowf(t, "unknown key type", "unknown key type: %s", leafKeyType)
- }
-
- caTemplate := &x509.Certificate{
- SerialNumber: big.NewInt(1),
- Subject: pkix.Name{CommonName: "Test CA"},
- NotBefore: time.Now(),
- NotAfter: time.Now().Add(24 * time.Hour),
- KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
- BasicConstraintsValid: true,
- IsCA: true,
- }
-
- var caCertDER []byte
- caCertDER, err = x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, caPubKey, caKey)
- assert.NoError(t, err)
-
- var caCert *x509.Certificate
- caCert, err = x509.ParseCertificate(caCertDER)
- assert.NoError(t, err)
-
- // Create leaf certificate with same key type, signed by CA
- var leafKey crypto.Signer
- var leafPubKey crypto.PublicKey
-
- switch leafKeyType {
- case "ecdsa-p256":
- var k *ecdsa.PrivateKey
- k, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
- assert.NoError(t, err)
- leafKey = k
- leafPubKey = &k.PublicKey
- case "ecdsa-p384":
- var k *ecdsa.PrivateKey
- k, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
- assert.NoError(t, err)
- leafKey = k
- leafPubKey = &k.PublicKey
- case "rsa":
- var k *rsa.PrivateKey
- k, err = rsa.GenerateKey(rand.Reader, 2048)
- assert.NoError(t, err)
- leafKey = k
- leafPubKey = &k.PublicKey
- default:
- assert.FailNowf(t, "unknown key type", "unknown key type: %s", leafKeyType)
- }
-
- leafTemplate := &x509.Certificate{
- SerialNumber: big.NewInt(2),
- Subject: pkix.Name{CommonName: "Test Leaf"},
- NotBefore: time.Now(),
- NotAfter: time.Now().Add(24 * time.Hour),
- KeyUsage: x509.KeyUsageDigitalSignature,
- ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
- }
-
- var leafCertDER []byte
- leafCertDER, err = x509.CreateCertificate(rand.Reader, leafTemplate, caCert, leafPubKey, caKey)
- assert.NoError(t, err)
-
- // Create tls.Certificate with full chain
- tlsCert := tls.Certificate{
- Certificate: [][]byte{leafCertDER, caCertDER},
- PrivateKey: leafKey,
- }
-
- // Create root CA pool
- rootCAs := x509.NewCertPool()
- rootCAs.AddCert(caCert)
-
- return tlsCert, rootCAs
-}
-
-// TestCertificateSignatureSchemesServerCertRejected tests that client rejects
-// server certificate when it uses a disallowed signature algorithm.
-//
-//nolint:dupl
-func TestCertificateSignatureSchemesServerCertRejected(t *testing.T) {
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- // Server uses P-256 ECDSA certificate chain
- serverCert, serverRootCAs := createCertChain(t, "ecdsa-p256")
-
- // Client uses self-signed cert (doesn't matter since server doesn't validate)
- clientCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- // Client only allows P-384 and P-521, but server leaf cert is P-256
- // This should cause the handshake to fail
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(clientCert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- dtls.WithRootCAs(serverRootCAs),
- dtls.WithCertificateSignatureSchemes(
- tls.ECDSAWithP384AndSHA384,
- tls.ECDSAWithP521AndSHA512,
- ),
- }
-
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(serverCert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- dtls.WithInsecureSkipVerify(true),
- }
-
- serverPort := randomPort(t)
-
- // Start server
- serverReady := make(chan struct{})
- serverDone := make(chan error, 1)
- var serverConn net.Conn
-
- go func() {
- listener, listenerErr := dtls.ListenWithOptions("udp",
- &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: serverPort},
- serverOpts...,
- )
- if listenerErr != nil {
- serverDone <- listenerErr
-
- return
- }
- defer func() { _ = listener.Close() }()
-
- serverReady <- struct{}{}
- var acceptErr error
- serverConn, acceptErr = listener.Accept()
- if acceptErr != nil {
- serverDone <- acceptErr
-
- return
- }
- defer func() { _ = serverConn.Close() }()
-
- // Try to do handshake
- var handshakeErr error
- if dtlsConn, ok := serverConn.(*dtls.Conn); ok {
- handshakeErr = dtlsConn.HandshakeContext(ctx)
- }
- serverDone <- handshakeErr
- }()
-
- // Wait for server to be ready
- select {
- case <-serverReady:
- case <-time.After(time.Second):
- assert.FailNow(t, "server not ready in time")
- }
-
- // Client should fail to connect
- conn, err := dtls.DialWithOptions("udp",
- &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: serverPort},
- clientOpts...,
- )
-
- if err == nil && conn != nil {
- err = conn.HandshakeContext(ctx)
- _ = conn.Close()
- }
-
- // We expect the handshake to fail due to invalid certificate signature algorithm
- assert.Error(t, err, "expected handshake to fail with disallowed signature scheme")
-
- // Wait for server to complete
- select {
- case serverErr := <-serverDone:
- // Server should also see an error
- assert.Error(t, serverErr)
- case <-time.After(2 * time.Second):
- t.Log("server did not complete in time")
- }
-}
-
-// TestCertificateSignatureSchemesClientCertRejected tests that server rejects
-// client certificate when it uses a disallowed signature algorithm.
-func TestCertificateSignatureSchemesClientCertRejected(t *testing.T) {
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- // Server uses self-signed cert (client won't validate)
- serverCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- // Client uses P-256 ECDSA certificate chain
- clientCert, clientRootCAs := createCertChain(t, "ecdsa-p256")
-
- // Client allows P-256 for server cert (won't validate anyway)
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(clientCert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- dtls.WithInsecureSkipVerify(true),
- }
-
- // Server only allows P-384 and P-521 for client cert, but client uses P-256
- // This should cause the handshake to fail
- serverOpts := []dtls.ServerOption{
- dtls.WithClientCAs(clientRootCAs),
- dtls.WithCertificates(serverCert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- dtls.WithClientAuth(dtls.RequireAndVerifyClientCert),
- dtls.WithCertificateSignatureSchemes(
- tls.ECDSAWithP384AndSHA384,
- tls.ECDSAWithP521AndSHA512,
- ),
- }
-
- serverPort := randomPort(t)
-
- // Start server
- serverReady := make(chan struct{})
- serverDone := make(chan error, 1)
- var serverConn net.Conn
-
- go func() {
- listener, listenerErr := dtls.ListenWithOptions("udp",
- &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: serverPort},
- serverOpts...,
- )
- if listenerErr != nil {
- serverDone <- listenerErr
-
- return
- }
- defer func() { _ = listener.Close() }()
-
- serverReady <- struct{}{}
- var acceptErr error
- serverConn, acceptErr = listener.Accept()
- if acceptErr != nil {
- serverDone <- acceptErr
-
- return
- }
- defer func() { _ = serverConn.Close() }()
-
- // Try to do handshake
- var handshakeErr error
- if dtlsConn, ok := serverConn.(*dtls.Conn); ok {
- handshakeErr = dtlsConn.HandshakeContext(ctx)
- }
- serverDone <- handshakeErr
- }()
-
- // Wait for server to be ready
- select {
- case <-serverReady:
- case <-time.After(time.Second):
- assert.FailNow(t, "server not ready in time")
- }
-
- // Client tries to connect
- conn, err := dtls.DialWithOptions("udp",
- &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: serverPort},
- clientOpts...,
- )
-
- if err == nil && conn != nil {
- err = conn.HandshakeContext(ctx)
- _ = conn.Close()
- }
-
- // We expect the handshake to fail due to invalid client certificate signature algorithm
- assert.Error(t, err, "expected handshake to fail with disallowed client cert signature scheme")
-
- // Wait for server to complete
- select {
- case serverErr := <-serverDone:
- // Server should also see an error (certificate validation failed)
- assert.Error(t, serverErr)
- case <-time.After(2 * time.Second):
- t.Log("server did not complete in time")
- }
-}
-
-// TestCertificateSignatureSchemesRSAMismatch tests that connections fail when
-// RSA certificate is presented but only ECDSA schemes are allowed.
-//
-//nolint:dupl
-func TestCertificateSignatureSchemesRSAMismatch(t *testing.T) {
- lim := test.TimeOut(time.Second * 30)
- defer lim.Stop()
-
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
-
- // Server uses RSA certificate chain
- serverCert, serverRootCAs := createCertChain(t, "rsa")
-
- // Client uses self-signed cert
- clientCert, err := selfsign.GenerateSelfSigned()
- assert.NoError(t, err)
-
- // Client only allows ECDSA, but server uses RSA
- // This should cause the handshake to fail
- clientOpts := []dtls.ClientOption{
- dtls.WithCertificates(clientCert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
- dtls.WithRootCAs(serverRootCAs),
- dtls.WithCertificateSignatureSchemes(
- tls.ECDSAWithP256AndSHA256,
- tls.ECDSAWithP384AndSHA384,
- ),
- }
-
- serverOpts := []dtls.ServerOption{
- dtls.WithCertificates(serverCert),
- dtls.WithCipherSuites(dtls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256),
- dtls.WithInsecureSkipVerify(true),
- }
-
- serverPort := randomPort(t)
-
- // Start server
- serverReady := make(chan struct{})
- serverDone := make(chan error, 1)
- var serverConn net.Conn
-
- go func() {
- listener, listenerErr := dtls.ListenWithOptions("udp",
- &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: serverPort},
- serverOpts...,
- )
- if listenerErr != nil {
- serverDone <- listenerErr
-
- return
- }
- defer func() { _ = listener.Close() }()
-
- serverReady <- struct{}{}
- var acceptErr error
- serverConn, acceptErr = listener.Accept()
- if acceptErr != nil {
- serverDone <- acceptErr
-
- return
- }
- defer func() { _ = serverConn.Close() }()
-
- // Try to do handshake
- var handshakeErr error
- if dtlsConn, ok := serverConn.(*dtls.Conn); ok {
- handshakeErr = dtlsConn.HandshakeContext(ctx)
- }
- serverDone <- handshakeErr
- }()
-
- // Wait for server to be ready
- select {
- case <-serverReady:
- case <-time.After(time.Second):
- assert.FailNow(t, "server not ready in time")
- }
-
- // Client should fail to connect
- conn, err := dtls.DialWithOptions("udp",
- &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: serverPort},
- clientOpts...,
- )
-
- if err == nil && conn != nil {
- err = conn.HandshakeContext(ctx)
- _ = conn.Close()
- }
-
- // We expect the handshake to fail due to RSA cert with ECDSA-only schemes
- assert.Error(t, err, "expected handshake to fail with RSA cert but ECDSA-only schemes")
-
- // Wait for server to complete
- select {
- case serverErr := <-serverDone:
- // Server should also see an error
- assert.Error(t, serverErr)
- case <-time.After(2 * time.Second):
- t.Log("server did not complete in time")
- }
-}
diff --git a/errors.go b/errors.go
index 119b42762..72ec452cd 100644
--- a/errors.go
+++ b/errors.go
@@ -11,16 +11,22 @@ import (
"net"
"os"
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
+ "github.com/pion/dtls/v4/internal/ech"
+ dtlserrors "github.com/pion/dtls/v4/internal/errors"
+ cryptosuite "github.com/pion/dtls/v4/pkg/crypto/ciphersuite"
+ "github.com/pion/dtls/v4/pkg/protocol/alert"
)
// ErrConnClosed indicates that the connection is closed.
-var ErrConnClosed = dtlserrors.ErrConnClosed //nolint:gochecknoglobals
+var ErrConnClosed = dtlserrors.ErrConnClosed
+
+// ECHRejectionError reports authenticated ECH rejection and retry configurations
+// for a new connection. The rejected connection cannot carry application data.
+type ECHRejectionError = ech.RejectionError
// errInvalidCipherSuite indicates an attempt at using an unsupported cipher suite.
type invalidCipherSuiteError struct {
- id CipherSuiteID
+ id cryptosuite.ID
}
func (e *invalidCipherSuiteError) Error() string {
@@ -71,7 +77,7 @@ func netError(err error) error {
se *os.SyscallError
)
- if errors.As(err, &opError) { //nolint:nestif
+ if errors.As(err, &opError) {
if errors.As(opError, &se) {
if isOpErrorTemporary(se) {
return temporaryNetworkError{err: err}
diff --git a/examples/dial/cid/main.go b/examples/dial/cid/main.go
index 026409a62..3f24c5303 100644
--- a/examples/dial/cid/main.go
+++ b/examples/dial/cid/main.go
@@ -10,8 +10,9 @@ import (
"net"
"time"
- "github.com/pion/dtls/v3"
- "github.com/pion/dtls/v3/examples/util"
+ "github.com/pion/dtls/v4"
+ "github.com/pion/dtls/v4/examples/util"
+ "github.com/pion/dtls/v4/pkg/crypto/ciphersuite"
)
func main() {
@@ -25,16 +26,13 @@ func main() {
// Connect to a DTLS server
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
- dtlsConn, err := dtls.DialWithOptions("udp", addr,
- dtls.WithPSK(func(hint []byte) ([]byte, error) {
- fmt.Printf("Server's hint: %s \n", hint)
-
- return []byte{0xAB, 0xC1, 0x23}, nil
- }),
- dtls.WithPSKIdentityHint([]byte("Pion DTLS Client")),
- dtls.WithCipherSuites(dtls.TLS_PSK_WITH_AES_128_CCM_8),
+ dtlsConn, err := dtls.Dial("udp", addr,
+ dtls.WithPSK(func() ([]dtls.PSK, error) {
+ return []dtls.PSK{{Identity: []byte("Pion DTLS Client"), Key: []byte{0xAB, 0xC1, 0x23}}}, nil
+ }, nil),
+ dtls.WithCipherSuites(ciphersuite.TLS_PSK_WITH_AES_128_CCM_8),
dtls.WithExtendedMasterSecret(dtls.RequireExtendedMasterSecret),
- dtls.WithConnectionIDGenerator(dtls.OnlySendCIDGenerator()),
+ dtls.WithConnectionID(dtls.OnlySendCIDGenerator(), dtls.CIDPathMigrationUnsafe),
)
util.Check(err)
defer func() {
diff --git a/examples/dial/psk/main.go b/examples/dial/psk/main.go
index f799ba9ee..dfb457c2b 100644
--- a/examples/dial/psk/main.go
+++ b/examples/dial/psk/main.go
@@ -10,8 +10,9 @@ import (
"net"
"time"
- "github.com/pion/dtls/v3"
- "github.com/pion/dtls/v3/examples/util"
+ "github.com/pion/dtls/v4"
+ "github.com/pion/dtls/v4/examples/util"
+ "github.com/pion/dtls/v4/pkg/crypto/ciphersuite"
)
func main() {
@@ -25,14 +26,11 @@ func main() {
// Connect to a DTLS server
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
- dtlsConn, err := dtls.DialWithOptions("udp", addr,
- dtls.WithPSK(func(hint []byte) ([]byte, error) {
- fmt.Printf("Server's hint: %s \n", hint)
-
- return []byte{0xAB, 0xC1, 0x23}, nil
- }),
- dtls.WithPSKIdentityHint([]byte{}),
- dtls.WithCipherSuites(dtls.TLS_PSK_WITH_AES_128_CCM_8),
+ dtlsConn, err := dtls.Dial("udp", addr,
+ dtls.WithPSK(func() ([]dtls.PSK, error) {
+ return []dtls.PSK{{Identity: []byte("Pion DTLS Client"), Key: []byte{0xAB, 0xC1, 0x23}}}, nil
+ }, nil),
+ dtls.WithCipherSuites(ciphersuite.TLS_PSK_WITH_AES_128_CCM_8),
dtls.WithExtendedMasterSecret(dtls.RequireExtendedMasterSecret),
)
util.Check(err)
diff --git a/examples/dial/selfsign/main.go b/examples/dial/selfsign/main.go
index 1c70f99fc..2d9fbdb08 100644
--- a/examples/dial/selfsign/main.go
+++ b/examples/dial/selfsign/main.go
@@ -10,9 +10,9 @@ import (
"net"
"time"
- "github.com/pion/dtls/v3"
- "github.com/pion/dtls/v3/examples/util"
- "github.com/pion/dtls/v3/pkg/crypto/selfsign"
+ "github.com/pion/dtls/v4"
+ "github.com/pion/dtls/v4/examples/util"
+ "github.com/pion/dtls/v4/pkg/crypto/selfsign"
)
func main() {
@@ -30,7 +30,7 @@ func main() {
// Connect to a DTLS server
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
- dtlsConn, err := dtls.DialWithOptions("udp", addr,
+ dtlsConn, err := dtls.Dial("udp", addr,
dtls.WithCertificates(certificate),
dtls.WithInsecureSkipVerify(true),
dtls.WithExtendedMasterSecret(dtls.RequireExtendedMasterSecret),
diff --git a/examples/dial/verify/main.go b/examples/dial/verify/main.go
index 5486bd683..8190d4a72 100644
--- a/examples/dial/verify/main.go
+++ b/examples/dial/verify/main.go
@@ -11,8 +11,8 @@ import (
"net"
"time"
- "github.com/pion/dtls/v3"
- "github.com/pion/dtls/v3/examples/util"
+ "github.com/pion/dtls/v4"
+ "github.com/pion/dtls/v4/examples/util"
)
func main() {
@@ -37,7 +37,7 @@ func main() {
// Connect to a DTLS server
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
- dtlsConn, err := dtls.DialWithOptions("udp", addr,
+ dtlsConn, err := dtls.Dial("udp", addr,
dtls.WithCertificates(certificate),
dtls.WithExtendedMasterSecret(dtls.RequireExtendedMasterSecret),
dtls.WithRootCAs(certPool),
diff --git a/examples/listen/cid/main.go b/examples/listen/cid/main.go
index 1b9be983c..72c69b216 100644
--- a/examples/listen/cid/main.go
+++ b/examples/listen/cid/main.go
@@ -10,8 +10,9 @@ import (
"net"
"time"
- "github.com/pion/dtls/v3"
- "github.com/pion/dtls/v3/examples/util"
+ "github.com/pion/dtls/v4"
+ "github.com/pion/dtls/v4/examples/util"
+ "github.com/pion/dtls/v4/pkg/crypto/ciphersuite"
)
func main() {
@@ -22,16 +23,15 @@ func main() {
// Everything below is the pion-DTLS API! Thanks for using it โค๏ธ.
//
- listener, err := dtls.ListenWithOptions("udp", addr,
- dtls.WithPSK(func(hint []byte) ([]byte, error) {
- fmt.Printf("Client's hint: %s \n", hint)
+ listener, err := dtls.ListenAddr("udp", addr,
+ dtls.WithPSK(nil, func(identities [][]byte) (*dtls.PSK, error) {
+ fmt.Printf("Client's identity: %s \n", identities[0])
- return []byte{0xAB, 0xC1, 0x23}, nil
+ return &dtls.PSK{Identity: identities[0], Key: []byte{0xAB, 0xC1, 0x23}}, nil
}),
- dtls.WithPSKIdentityHint([]byte("Pion DTLS Server")),
- dtls.WithCipherSuites(dtls.TLS_PSK_WITH_AES_128_CCM_8),
+ dtls.WithCipherSuites(ciphersuite.TLS_PSK_WITH_AES_128_CCM_8),
dtls.WithExtendedMasterSecret(dtls.RequireExtendedMasterSecret),
- dtls.WithConnectionIDGenerator(dtls.RandomCIDGenerator(8)),
+ dtls.WithConnectionID(dtls.RandomCIDGenerator(8), dtls.CIDPathMigrationUnsafe),
)
util.Check(err)
defer func() {
diff --git a/examples/listen/psk/main.go b/examples/listen/psk/main.go
index 0e2c77c01..895b966ec 100644
--- a/examples/listen/psk/main.go
+++ b/examples/listen/psk/main.go
@@ -10,8 +10,9 @@ import (
"net"
"time"
- "github.com/pion/dtls/v3"
- "github.com/pion/dtls/v3/examples/util"
+ "github.com/pion/dtls/v4"
+ "github.com/pion/dtls/v4/examples/util"
+ "github.com/pion/dtls/v4/pkg/crypto/ciphersuite"
)
func main() {
@@ -22,14 +23,13 @@ func main() {
// Everything below is the pion-DTLS API! Thanks for using it โค๏ธ.
//
- listener, err := dtls.ListenWithOptions("udp", addr,
- dtls.WithPSK(func(hint []byte) ([]byte, error) {
- fmt.Printf("Client's hint: %s \n", hint)
+ listener, err := dtls.ListenAddr("udp", addr,
+ dtls.WithPSK(nil, func(identities [][]byte) (*dtls.PSK, error) {
+ fmt.Printf("Client's identity: %s \n", identities[0])
- return []byte{0xAB, 0xC1, 0x23}, nil
+ return &dtls.PSK{Identity: identities[0], Key: []byte{0xAB, 0xC1, 0x23}}, nil
}),
- dtls.WithPSKIdentityHint([]byte("Pion DTLS Server")),
- dtls.WithCipherSuites(dtls.TLS_PSK_WITH_AES_128_CCM_8),
+ dtls.WithCipherSuites(ciphersuite.TLS_PSK_WITH_AES_128_CCM_8),
dtls.WithExtendedMasterSecret(dtls.RequireExtendedMasterSecret),
)
util.Check(err)
diff --git a/examples/listen/selfsign/main.go b/examples/listen/selfsign/main.go
index 8f15705b0..d69da1f33 100644
--- a/examples/listen/selfsign/main.go
+++ b/examples/listen/selfsign/main.go
@@ -10,9 +10,9 @@ import (
"net"
"time"
- "github.com/pion/dtls/v3"
- "github.com/pion/dtls/v3/examples/util"
- "github.com/pion/dtls/v3/pkg/crypto/selfsign"
+ "github.com/pion/dtls/v4"
+ "github.com/pion/dtls/v4/examples/util"
+ "github.com/pion/dtls/v4/pkg/crypto/selfsign"
)
func main() {
@@ -27,7 +27,7 @@ func main() {
// Everything below is the pion-DTLS API! Thanks for using it โค๏ธ.
//
- listener, err := dtls.ListenWithOptions("udp", addr,
+ listener, err := dtls.ListenAddr("udp", addr,
dtls.WithCertificates(certificate),
dtls.WithExtendedMasterSecret(dtls.RequireExtendedMasterSecret),
)
diff --git a/examples/listen/verify-brute-force-protection/main.go b/examples/listen/verify-brute-force-protection/main.go
index 189b57390..47a54360a 100644
--- a/examples/listen/verify-brute-force-protection/main.go
+++ b/examples/listen/verify-brute-force-protection/main.go
@@ -13,8 +13,8 @@ import (
"sync"
"time"
- "github.com/pion/dtls/v3"
- "github.com/pion/dtls/v3/examples/util"
+ "github.com/pion/dtls/v4"
+ "github.com/pion/dtls/v4/examples/util"
)
func main() {
@@ -43,7 +43,7 @@ func main() {
util.Check(err)
certPool.AddCert(cert)
- listener, err := dtls.ListenWithOptions("udp", addr,
+ listener, err := dtls.ListenAddr("udp", addr,
dtls.WithCertificates(certificate),
dtls.WithExtendedMasterSecret(dtls.RequireExtendedMasterSecret),
dtls.WithClientAuth(dtls.RequireAndVerifyClientCert),
diff --git a/examples/listen/verify/main.go b/examples/listen/verify/main.go
index c198b52a2..ab36093ea 100644
--- a/examples/listen/verify/main.go
+++ b/examples/listen/verify/main.go
@@ -11,8 +11,8 @@ import (
"net"
"time"
- "github.com/pion/dtls/v3"
- "github.com/pion/dtls/v3/examples/util"
+ "github.com/pion/dtls/v4"
+ "github.com/pion/dtls/v4/examples/util"
)
func main() {
@@ -34,7 +34,7 @@ func main() {
util.Check(err)
certPool.AddCert(cert)
- listener, err := dtls.ListenWithOptions("udp", addr,
+ listener, err := dtls.ListenAddr("udp", addr,
dtls.WithCertificates(certificate),
dtls.WithExtendedMasterSecret(dtls.RequireExtendedMasterSecret),
dtls.WithClientAuth(dtls.RequireAndVerifyClientCert),
diff --git a/examples/mdns/README.md b/examples/mdns/README.md
new file mode 100644
index 000000000..47da0c9cb
--- /dev/null
+++ b/examples/mdns/README.md
@@ -0,0 +1,32 @@
+# DTLS handshake over mDNS
+
+This demo carries each DTLS PSK handshake batch in an update to a DNS-SD TXT record.
+The server and client run as separate processes and exchange multicast DNS
+packets over the loopback interface. The same detached DTLS connection remains
+in use after the handshake; each side routes its later datagrams over unicast
+UDP. Before switching paths, each side confirms handshake completion in its TXT
+record so an application datagram cannot overtake the peer's final handshake
+batch. Interactive messages then travel over the UDP path.
+
+Each process binds a random UDP port and advertises that port in its DNS-SD SRV
+record. The peer discovers both the address and port through mDNS.
+
+Start the server in one terminal:
+
+```sh
+go run ./server
+```
+
+Then start one or more clients in other terminals:
+
+```sh
+go run ./client
+go run ./client
+```
+
+Each client advertises a unique DNS-SD instance. The server creates a separate
+DTLS connection, response TXT record, and UDP socket for every client, so their
+handshakes and application datagrams cannot be mixed. After they connect, a
+line typed in a client is sent to the server; a line typed in the server is
+broadcast to every connected client. Only the bounded handshake batches use
+DNS-SD TXT updates.
diff --git a/examples/mdns/client/main.go b/examples/mdns/client/main.go
new file mode 100644
index 000000000..88edfe817
--- /dev/null
+++ b/examples/mdns/client/main.go
@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: 2026 The Pion community
+// SPDX-License-Identifier: MIT
+
+// Command client performs a DTLS handshake with the mDNS demo server.
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/pion/dtls/v4/examples/mdns/internal/mdnsdemo"
+)
+
+func main() {
+ if err := mdnsdemo.RunClient(context.Background()); err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/examples/mdns/go.mod b/examples/mdns/go.mod
new file mode 100644
index 000000000..36fb44f29
--- /dev/null
+++ b/examples/mdns/go.mod
@@ -0,0 +1,16 @@
+module github.com/pion/dtls/v4/examples/mdns
+
+go 1.25.0
+
+require (
+ github.com/pion/dtls/v4 v4.0.0-rc.6
+ github.com/pion/mdns/v2 v2.2.2
+ golang.org/x/net v0.55.0
+)
+
+require (
+ github.com/pion/logging v0.2.4 // indirect
+ github.com/pion/transport/v5 v5.1.1 // indirect
+ golang.org/x/crypto v0.51.0 // indirect
+ golang.org/x/sys v0.45.0 // indirect
+)
diff --git a/examples/mdns/go.sum b/examples/mdns/go.sum
new file mode 100644
index 000000000..ca50291f2
--- /dev/null
+++ b/examples/mdns/go.sum
@@ -0,0 +1,18 @@
+github.com/pion/dtls/v4 v4.0.0-rc.6 h1:UjIIrH/hY/PJ5bvpJy89d5EKo3JBnK30g5eedGDkUqI=
+github.com/pion/dtls/v4 v4.0.0-rc.6/go.mod h1:jzj1RiWPy+T6uaBR7Bc/PJYHvF+VxNLOYYb/d1IINwE=
+github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
+github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
+github.com/pion/mdns/v2 v2.2.2 h1:qq9Kj3PnjF0mfSwd8Icu0CXy5aoYdQKnNUIsGPeUpgk=
+github.com/pion/mdns/v2 v2.2.2/go.mod h1:ZX5f0AAH1D6TOjdjvcBORZZHaZuG0t9+br78lHEwiJ4=
+github.com/pion/transport/v5 v5.1.1 h1:Sauq8YJqRopgRfIgnpfKpdpKG14g0N4jWlRgpWsEfN8=
+github.com/pion/transport/v5 v5.1.1/go.mod h1:Qxw6fCEjFWQkRDZOhS4Vf+neJBcihauvA3uyEa1J1F0=
+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
+golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
+golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
+golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
+golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
+golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
+golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
diff --git a/examples/mdns/internal/mdnsdemo/mdnsdemo.go b/examples/mdns/internal/mdnsdemo/mdnsdemo.go
new file mode 100644
index 000000000..f6ec95173
--- /dev/null
+++ b/examples/mdns/internal/mdnsdemo/mdnsdemo.go
@@ -0,0 +1,871 @@
+// SPDX-FileCopyrightText: 2026 The Pion community
+// SPDX-License-Identifier: MIT
+
+// Package mdnsdemo carries a DTLS handshake in DNS-SD TXT updates.
+package mdnsdemo
+
+import (
+ "bufio"
+ "bytes"
+ "context"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "net/netip"
+ "os"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/pion/dtls/v4"
+ cryptosuite "github.com/pion/dtls/v4/pkg/crypto/ciphersuite"
+ "github.com/pion/mdns/v2"
+ "golang.org/x/net/ipv4"
+)
+
+const (
+ serviceType = "_pion-dtls._udp"
+ clientInstancePrefix = "Pion DTLS client-"
+ serverInstancePrefix = "Pion DTLS server-"
+ serverInstance = "Pion DTLS server"
+ txtChunkSize = 240
+ maximumBatchSize = 1200
+)
+
+var (
+ errBatchTooLarge = errors.New("DTLS batch is too large for this DNS-SD demo")
+ errInvalidPartCount = errors.New("invalid part count")
+ errMissingPart = errors.New("missing TXT part")
+ errNoLoopbackInterface = errors.New("no active loopback interface")
+ errNoPeerAddress = errors.New("client has no IPv4 address or UDP port")
+ errTrailingBatchData = errors.New("trailing batch data")
+)
+
+type endpoint struct {
+ name string
+ instance string
+ conn *dtls.DetachedConn
+ mdns *mdns.Conn
+ udp *net.UDPConn
+ peer net.Addr
+ done <-chan struct{}
+ onConnected func()
+ report func(error)
+ mu sync.Mutex
+ sequence uint64
+ update int
+ lastSeq uint64
+ lastBatch [][]byte
+ localReady bool
+ peerReady bool
+ connected bool
+}
+
+func (e *endpoint) start(ctx context.Context, firstEvent mdns.ServiceEvent) error {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+
+ if err := e.conn.Start(ctx); err != nil {
+ return fmt.Errorf("%s start DTLS: %w", e.name, err)
+ }
+ if err := e.drainEventsLocked(); err != nil {
+ return err
+ }
+
+ return e.handleMDNSLocked(firstEvent)
+}
+
+func (e *endpoint) handleMDNS(event mdns.ServiceEvent) error {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ select {
+ case <-e.done:
+ return nil
+ default:
+ }
+
+ return e.handleMDNSLocked(event)
+}
+
+func (e *endpoint) handleMDNSLocked(event mdns.ServiceEvent) error {
+ sequence, ready, datagrams, err := decodeBatch(event.Instance.Text)
+ if err != nil {
+ return fmt.Errorf("%s decode TXT from %q: %w", e.name, event.Instance.Instance, err)
+ }
+ if sequence == 0 {
+ return nil
+ }
+ if sequence <= e.lastSeq {
+ return nil
+ }
+ if ready {
+ e.peerReady = true
+ }
+
+ if len(datagrams) != 0 {
+ fmt.Printf("%s: receive handshake TXT (%d datagrams)\n", e.name, len(datagrams))
+ }
+ for _, datagram := range datagrams {
+ if err = e.conn.HandleDatagram(datagram, e.peer); err != nil {
+ return fmt.Errorf("%s handle DTLS datagram: %w", e.name, err)
+ }
+ if err = e.drainEventsLocked(); err != nil {
+ return err
+ }
+ }
+ e.lastSeq = sequence
+ e.markConnectedLocked()
+
+ return nil
+}
+
+func (e *endpoint) handleDatagram(datagram []byte, addr net.Addr) error {
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ select {
+ case <-e.done:
+ return nil
+ default:
+ }
+
+ if err := e.conn.HandleDatagram(datagram, addr); err != nil {
+ return fmt.Errorf("%s handle DTLS datagram: %w", e.name, err)
+ }
+
+ return e.drainEventsLocked()
+}
+
+func (e *endpoint) write(data []byte) error { //nolint:contextcheck
+ e.mu.Lock()
+ defer e.mu.Unlock()
+
+ n, err := e.conn.Write(data)
+ if err != nil {
+ return err
+ }
+ if n != len(data) {
+ return io.ErrShortWrite
+ }
+
+ return e.drainEventsLocked()
+}
+
+func (e *endpoint) drainEventsLocked() error { //nolint:cyclop
+ for {
+ event := e.conn.NextEvent()
+ switch event.Kind {
+ case dtls.DetachedNoEvent:
+ return nil
+ case dtls.DetachedWriteDatagrams:
+ if err := e.writeDatagrams(event.Datagrams, event.Addr); err != nil {
+ return err
+ }
+ case dtls.DetachedApplicationData:
+ fmt.Printf("%s: received %q\n", e.name, event.Data)
+ case dtls.DetachedHandshakeDone:
+ if !e.localReady {
+ e.localReady = true
+ if err := e.publishHandshakeBatchLocked(e.lastBatch); err != nil {
+ return err
+ }
+ e.markConnectedLocked()
+ }
+ case dtls.DetachedClosed:
+ return fmt.Errorf("%s DTLS closed: %w", e.name, event.Err)
+ }
+ }
+}
+
+func (e *endpoint) writeDatagrams(datagrams [][]byte, addr net.Addr) error {
+ if e.connected {
+ if addr == nil {
+ addr = e.peer
+ }
+ for _, datagram := range datagrams {
+ if _, err := e.udp.WriteTo(datagram, addr); err != nil {
+ return fmt.Errorf("%s write UDP: %w", e.name, err)
+ }
+ }
+
+ return nil
+ }
+
+ e.lastBatch = datagrams
+
+ return e.publishHandshakeBatchLocked(datagrams)
+}
+
+func (e *endpoint) publishHandshakeBatchLocked(datagrams [][]byte) error {
+ e.sequence++
+ e.update++
+ text, err := encodeBatch(e.sequence, e.localReady, datagrams)
+ if err != nil {
+ return fmt.Errorf("%s encode handshake batch: %w", e.name, err)
+ }
+ fmt.Printf(
+ "%s: publish handshake TXT %d (%d datagrams, %d bytes, ready=%t)\n",
+ e.name, e.update, len(datagrams), datagramBytes(datagrams), e.localReady,
+ )
+ if err = e.mdns.UpdateTXT(e.instance, serviceType, text); err != nil {
+ return fmt.Errorf("%s publish handshake batch: %w", e.name, err)
+ }
+
+ return nil
+}
+
+func (e *endpoint) markConnectedLocked() {
+ if e.connected || !e.localReady || !e.peerReady {
+ return
+ }
+ e.connected = true
+ fmt.Printf("%s: both handshakes complete; switched to UDP %s\n", e.name, e.udp.LocalAddr())
+ e.onConnected()
+}
+
+func (e *endpoint) readUDP() {
+ buffer := make([]byte, maximumBatchSize)
+ for {
+ n, addr, err := e.udp.ReadFrom(buffer)
+ if err != nil {
+ select {
+ case <-e.done:
+ default:
+ e.report(fmt.Errorf("%s read UDP: %w", e.name, err))
+ }
+
+ return
+ }
+ if err = e.handleDatagram(buffer[:n], addr); err != nil {
+ e.report(err)
+
+ return
+ }
+ }
+}
+
+func (e *endpoint) watchEvents() {
+ for {
+ select {
+ case <-e.conn.EventReady():
+ case <-e.done:
+ return
+ }
+
+ e.mu.Lock()
+ select {
+ case <-e.done:
+ e.mu.Unlock()
+
+ return
+ default:
+ }
+ err := e.drainEventsLocked()
+ e.mu.Unlock()
+ if err != nil {
+ e.report(err)
+
+ return
+ }
+ }
+}
+
+func (e *endpoint) close() { //nolint:contextcheck
+ e.mu.Lock()
+ defer e.mu.Unlock()
+ _ = e.conn.Close()
+ _ = e.drainEventsLocked()
+}
+
+type roleConfig struct {
+ name string
+ instance string
+ peerInstance string
+ isClient bool
+}
+
+// RunClient discovers the server and starts an interactive DTLS session.
+//
+//nolint:cyclop,contextcheck
+func RunClient(ctx context.Context) error {
+ sessionCtx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ udpConn, err := listenUDP()
+ if err != nil {
+ return err
+ }
+ defer func() { _ = udpConn.Close() }()
+
+ id := strconv.FormatInt(time.Now().UnixNano(), 36) + "-" + strconv.Itoa(udpConn.LocalAddr().(*net.UDPAddr).Port) //nolint:forcetypeassert
+ role := roleConfig{
+ name: "client",
+ instance: clientInstancePrefix + id,
+ peerInstance: serverInstancePrefix + id,
+ isClient: true,
+ }
+ mdnsConn, err := newMDNSConn(role, udpPort(udpConn))
+ if err != nil {
+ return err
+ }
+ defer func() { _ = mdnsConn.Close() }()
+
+ completed := make(chan struct{}, 1)
+ errCh := make(chan error, 1)
+ peerEvents := make(chan mdns.ServiceEvent, 16)
+ report := func(err error) {
+ select {
+ case errCh <- err:
+ default:
+ }
+ }
+ mdnsConn.OnServiceDiscovered(func(event mdns.ServiceEvent) {
+ if event.Instance.Instance == role.peerInstance {
+ select {
+ case peerEvents <- event:
+ case <-sessionCtx.Done():
+ }
+ }
+ })
+ if err = mdnsConn.Browse(sessionCtx, serviceType); err != nil {
+ return err
+ }
+ firstEvent, peer, err := awaitPeer(sessionCtx, peerEvents, errCh)
+ if err != nil {
+ return err
+ }
+ fmt.Printf("client: UDP %s; discovered server at %s via mDNS\n", udpConn.LocalAddr(), peer)
+
+ conn, err := newDetachedConn(role, peer)
+ if err != nil {
+ return err
+ }
+ ep := &endpoint{
+ name: role.name,
+ instance: role.instance,
+ conn: conn,
+ mdns: mdnsConn,
+ udp: udpConn,
+ peer: peer,
+ done: sessionCtx.Done(),
+ onConnected: func() { completed <- struct{}{} },
+ report: report,
+ sequence: uint64(time.Now().UnixNano()),
+ }
+ defer ep.close()
+ if err = ep.start(sessionCtx, firstEvent); err != nil {
+ return err
+ }
+ go ep.watchEvents()
+ go ep.readUDP()
+ go forwardMDNSEvents(sessionCtx, ep, peerEvents, report)
+
+ select {
+ case <-completed:
+ case err = <-errCh:
+ return err
+ case <-sessionCtx.Done():
+ return sessionCtx.Err()
+ }
+
+ return runInteractive(sessionCtx, role.name, ep, errCh)
+}
+
+// RunServer accepts and serves interactive DTLS sessions from multiple clients.
+func RunServer(ctx context.Context) error {
+ return runServer(ctx)
+}
+
+type serverSession struct {
+ clientInstance string
+ serverInstance string
+ ep *endpoint
+ udp *net.UDPConn
+ cancel context.CancelFunc
+ connected bool
+}
+
+type sessionFailure struct {
+ session *serverSession
+ err error
+}
+
+//nolint:cyclop,contextcheck
+func runServer(ctx context.Context) error {
+ baseUDP, err := listenUDP()
+ if err != nil {
+ return err
+ }
+ defer func() { _ = baseUDP.Close() }()
+ baseRole := roleConfig{name: "server", instance: serverInstance}
+ mdnsConn, err := newMDNSConn(baseRole, udpPort(baseUDP))
+ if err != nil {
+ return err
+ }
+ defer func() { _ = mdnsConn.Close() }()
+
+ peerEvents := make(chan mdns.ServiceEvent, 64)
+ mdnsConn.OnServiceDiscovered(func(event mdns.ServiceEvent) {
+ if strings.HasPrefix(event.Instance.Instance, clientInstancePrefix) {
+ select {
+ case peerEvents <- event:
+ case <-ctx.Done():
+ }
+ }
+ })
+ if err = mdnsConn.Browse(ctx, serviceType); err != nil {
+ return err
+ }
+
+ failures := make(chan sessionFailure, 16)
+ connected := make(chan *serverSession, 16)
+ sessions := make(map[string]*serverSession)
+ input := make(chan string, 1)
+ inputErr := make(chan error, 1)
+ go readInput(input, inputErr)
+ fmt.Println("server: accepting clients; typed messages are broadcast to every connected client")
+ defer func() {
+ for _, session := range sessions {
+ closeServerSession(mdnsConn, session)
+ }
+ }()
+
+ for {
+ select {
+ case event := <-peerEvents:
+ instance := event.Instance.Instance
+ if session := sessions[instance]; session != nil {
+ if handleErr := session.ep.handleMDNS(event); handleErr != nil {
+ fmt.Printf("server: client %q failed: %v\n", instance, handleErr)
+ delete(sessions, instance)
+ closeServerSession(mdnsConn, session)
+ }
+
+ continue
+ }
+ session, createErr := newServerSession(ctx, mdnsConn, event, connected, failures)
+ if createErr != nil {
+ fmt.Printf("server: reject client %q: %v\n", instance, createErr)
+
+ continue
+ }
+ sessions[instance] = session
+ fmt.Printf("server: discovered client %q at %s\n", instance, session.ep.peer)
+ case session := <-connected:
+ if sessions[session.clientInstance] == session {
+ session.connected = true
+ fmt.Printf("server: client %q connected\n", session.clientInstance)
+ }
+ case failure := <-failures:
+ if sessions[failure.session.clientInstance] != failure.session {
+ continue
+ }
+ fmt.Printf("server: client %q disconnected: %v\n", failure.session.clientInstance, failure.err)
+ delete(sessions, failure.session.clientInstance)
+ closeServerSession(mdnsConn, failure.session)
+ case line := <-input:
+ if line != "" {
+ broadcast(sessions, line)
+ }
+ case inputReadErr := <-inputErr:
+ return inputReadErr
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+}
+
+func newServerSession( //nolint:cyclop,contextcheck
+ ctx context.Context,
+ mdnsConn *mdns.Conn,
+ event mdns.ServiceEvent,
+ connected chan<- *serverSession,
+ failures chan<- sessionFailure,
+) (*serverSession, error) {
+ if !event.Addr.Is4() || event.Instance.Port == 0 {
+ return nil, errNoPeerAddress
+ }
+ peer := net.UDPAddrFromAddrPort(netip.AddrPortFrom(event.Addr, event.Instance.Port))
+ udpConn, err := listenUDP()
+ if err != nil {
+ return nil, err
+ }
+ id := strings.TrimPrefix(event.Instance.Instance, clientInstancePrefix)
+ role := roleConfig{
+ name: "server[" + id + "]",
+ instance: serverInstancePrefix + id,
+ peerInstance: event.Instance.Instance,
+ }
+ if err = mdnsConn.Register(serviceInstance(role.instance, udpPort(udpConn))); err != nil {
+ _ = udpConn.Close()
+
+ return nil, err
+ }
+ conn, err := newDetachedConn(role, peer)
+ if err != nil {
+ mdnsConn.Unregister(role.instance, serviceType)
+ _ = udpConn.Close()
+
+ return nil, err
+ }
+ sessionCtx, cancel := context.WithCancel(ctx)
+ session := &serverSession{
+ clientInstance: event.Instance.Instance,
+ serverInstance: role.instance,
+ udp: udpConn,
+ cancel: cancel,
+ }
+ ep := &endpoint{
+ name: role.name,
+ instance: role.instance,
+ conn: conn,
+ mdns: mdnsConn,
+ udp: udpConn,
+ peer: peer,
+ done: sessionCtx.Done(),
+ onConnected: func() {
+ select {
+ case connected <- session:
+ case <-sessionCtx.Done():
+ }
+ },
+ sequence: uint64(time.Now().UnixNano()),
+ }
+ session.ep = ep
+ ep.report = func(reportErr error) {
+ select {
+ case failures <- sessionFailure{session: session, err: reportErr}:
+ case <-sessionCtx.Done():
+ }
+ }
+ if err = ep.start(sessionCtx, event); err != nil {
+ closeServerSession(mdnsConn, session) //nolint:contextcheck
+
+ return nil, err
+ }
+ go ep.watchEvents()
+ go ep.readUDP()
+
+ return session, nil
+}
+
+func closeServerSession(mdnsConn *mdns.Conn, session *serverSession) { //nolint:contextcheck
+ session.cancel()
+ _ = session.udp.Close()
+ session.ep.close()
+ mdnsConn.Unregister(session.serverInstance, serviceType)
+}
+
+func broadcast(sessions map[string]*serverSession, line string) {
+ sent := 0
+ for _, session := range sessions {
+ if !session.connected {
+ continue
+ }
+ if err := session.ep.write([]byte(line)); err != nil {
+ session.ep.report(err)
+
+ continue
+ }
+ sent++
+ }
+ fmt.Printf("server: sent %q to %d client(s)\n", line, sent)
+}
+
+func forwardMDNSEvents(
+ ctx context.Context,
+ ep *endpoint,
+ events <-chan mdns.ServiceEvent,
+ report func(error),
+) {
+ for {
+ select {
+ case event := <-events:
+ if err := ep.handleMDNS(event); err != nil {
+ report(err)
+
+ return
+ }
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+func listenUDP() (*net.UDPConn, error) {
+ return net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)})
+}
+
+func udpPort(conn *net.UDPConn) uint16 {
+ return uint16(conn.LocalAddr().(*net.UDPAddr).Port) //nolint:forcetypeassert,gosec
+}
+
+func newDetachedConn(role roleConfig, peer net.Addr) (*dtls.DetachedConn, error) {
+ pskCallback := func([]byte) ([]byte, error) { return []byte("pion-mdns-handshake"), nil }
+ if role.isClient {
+ return dtls.DetachedClient(peer,
+ dtls.WithPSK(pskCallback),
+ dtls.WithPSKIdentityHint([]byte("mdns-demo")),
+ dtls.WithCipherSuites(cryptosuite.TLS_PSK_WITH_AES_128_CCM_8),
+ dtls.WithFlightInterval(2*time.Second),
+ )
+ }
+
+ return dtls.DetachedServer(peer,
+ dtls.WithPSK(pskCallback),
+ dtls.WithCipherSuites(cryptosuite.TLS_PSK_WITH_AES_128_CCM_8),
+ dtls.WithFlightInterval(2*time.Second),
+ )
+}
+
+//nolint:contextcheck
+func runInteractive(
+ ctx context.Context,
+ name string,
+ endpoint *endpoint,
+ errCh <-chan error,
+) error {
+ fmt.Printf("%s: connected; type a message and press enter\n", name)
+
+ input := make(chan string, 1)
+ inputErr := make(chan error, 1)
+ go readInput(input, inputErr)
+
+ for {
+ select {
+ case line := <-input:
+ if line == "" {
+ continue
+ }
+ if err := endpoint.write([]byte(line)); err != nil {
+ return err
+ }
+ fmt.Printf("%s: sent %q\n", name, line)
+ case err := <-inputErr:
+ return err
+ case err := <-errCh:
+ return err
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+ }
+}
+
+func readInput(input chan<- string, inputErr chan<- error) {
+ scanner := bufio.NewScanner(os.Stdin)
+ scanner.Buffer(make([]byte, maximumBatchSize), maximumBatchSize)
+ for scanner.Scan() {
+ input <- scanner.Text()
+ }
+ inputErr <- scanner.Err()
+}
+
+func newMDNSConn(role roleConfig, port uint16) (*mdns.Conn, error) {
+ loopback, err := loopbackInterface()
+ if err != nil {
+ return nil, err
+ }
+
+ addr, err := net.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4)
+ if err != nil {
+ return nil, err
+ }
+ socket, err := net.ListenUDP("udp4", addr)
+ if err != nil {
+ return nil, err
+ }
+ packetConn := ipv4.NewPacketConn(socket)
+ if err = packetConn.SetMulticastLoopback(true); err != nil {
+ _ = socket.Close()
+
+ return nil, err
+ }
+
+ conn, err := mdns.NewServer(
+ packetConn,
+ nil,
+ mdns.WithName("dtls-mdns-"+role.name),
+ mdns.WithIncludeLoopback(true),
+ mdns.WithInterfaces(loopback),
+ mdns.WithLocalAddress(net.IPv4(127, 0, 0, 1)),
+ mdns.WithLocalNames("dtls-mdns-"+role.name+".local"),
+ mdns.WithService(serviceInstance(role.instance, port)),
+ )
+ if err != nil {
+ _ = socket.Close()
+
+ return nil, err
+ }
+
+ return conn, nil
+}
+
+func serviceInstance(instance string, port uint16) mdns.ServiceInstance {
+ return mdns.ServiceInstance{
+ Instance: instance,
+ Service: serviceType,
+ Port: port,
+ Text: []mdns.TXTEntry{
+ mdns.NewTXTString("v", "1"),
+ mdns.NewTXTString("seq", "0"),
+ mdns.NewTXTString("ready", "0"),
+ mdns.NewTXTString("parts", "0"),
+ },
+ }
+}
+
+func awaitPeer(
+ ctx context.Context,
+ events <-chan mdns.ServiceEvent,
+ errCh <-chan error,
+) (mdns.ServiceEvent, *net.UDPAddr, error) {
+ for {
+ select {
+ case event := <-events:
+ if event.Addr.Is4() && event.Instance.Port != 0 {
+ return event, net.UDPAddrFromAddrPort(netip.AddrPortFrom(event.Addr, event.Instance.Port)), nil
+ }
+ case err := <-errCh:
+ return mdns.ServiceEvent{}, nil, err
+ case <-ctx.Done():
+ return mdns.ServiceEvent{}, nil, ctx.Err()
+ }
+ }
+}
+
+func loopbackInterface() (net.Interface, error) {
+ interfaces, err := net.Interfaces()
+ if err != nil {
+ return net.Interface{}, err
+ }
+ for _, iface := range interfaces {
+ if iface.Flags&net.FlagLoopback != 0 && iface.Flags&net.FlagUp != 0 {
+ return iface, nil
+ }
+ }
+
+ return net.Interface{}, errNoLoopbackInterface
+}
+
+func encodeBatch(sequence uint64, ready bool, datagrams [][]byte) ([]mdns.TXTEntry, error) {
+ var payload bytes.Buffer
+ if len(datagrams) > int(^uint16(0)) {
+ return nil, errBatchTooLarge
+ }
+ _ = binary.Write(&payload, binary.BigEndian, uint16(len(datagrams))) //nolint:gosec
+ for _, datagram := range datagrams {
+ if len(datagram) > int(^uint16(0)) {
+ return nil, errBatchTooLarge
+ }
+ _ = binary.Write(&payload, binary.BigEndian, uint16(len(datagram))) //nolint:gosec
+ _, _ = payload.Write(datagram)
+ }
+ if payload.Len() > maximumBatchSize {
+ return nil, fmt.Errorf("%w: %d bytes", errBatchTooLarge, payload.Len())
+ }
+
+ raw := payload.Bytes()
+ partCount := (len(raw) + txtChunkSize - 1) / txtChunkSize
+ text := []mdns.TXTEntry{
+ mdns.NewTXTString("v", "1"),
+ mdns.NewTXTString("seq", strconv.FormatUint(sequence, 10)),
+ mdns.NewTXTString("ready", strconv.FormatBool(ready)),
+ mdns.NewTXTString("parts", strconv.Itoa(partCount)),
+ }
+ for part := range partCount {
+ start := part * txtChunkSize
+ end := min(start+txtChunkSize, len(raw))
+ text = append(text, mdns.NewTXTBinary(fmt.Sprintf("p%d", part), raw[start:end]))
+ }
+
+ return text, nil
+}
+
+func datagramBytes(datagrams [][]byte) int {
+ var total int
+ for _, datagram := range datagrams {
+ total += len(datagram)
+ }
+
+ return total
+}
+
+func decodeBatch(text []mdns.TXTEntry) (uint64, bool, [][]byte, error) { //nolint:cyclop
+ values := make(map[string][]byte, len(text))
+ for _, entry := range text {
+ values[strings.ToLower(entry.Key)] = entry.Value
+ }
+ if string(values["v"]) != "1" {
+ return 0, false, nil, nil
+ }
+
+ sequence, err := strconv.ParseUint(string(values["seq"]), 10, 64)
+ if err != nil {
+ return 0, false, nil, fmt.Errorf("invalid sequence: %w", err)
+ }
+ ready := false
+ if value, ok := values["ready"]; ok {
+ ready, err = strconv.ParseBool(string(value))
+ if err != nil {
+ return 0, false, nil, fmt.Errorf("invalid ready state: %w", err)
+ }
+ }
+ partCount, err := strconv.Atoi(string(values["parts"]))
+ if err != nil || partCount < 0 {
+ return 0, false, nil, errInvalidPartCount
+ }
+ if partCount == 0 {
+ return sequence, ready, nil, nil
+ }
+
+ payload, err := joinTXTParts(values, partCount)
+ if err != nil {
+ return 0, false, nil, err
+ }
+ datagrams, err := decodeDatagrams(payload)
+ if err != nil {
+ return 0, false, nil, err
+ }
+
+ return sequence, ready, datagrams, nil
+}
+
+func joinTXTParts(values map[string][]byte, partCount int) ([]byte, error) {
+ var payload bytes.Buffer
+ for part := range partCount {
+ chunk, ok := values[fmt.Sprintf("p%d", part)]
+ if !ok {
+ return nil, fmt.Errorf("%w: %d", errMissingPart, part)
+ }
+ _, _ = payload.Write(chunk)
+ }
+
+ return payload.Bytes(), nil
+}
+
+func decodeDatagrams(payload []byte) ([][]byte, error) {
+ reader := bytes.NewReader(payload)
+ var datagramCount uint16
+ if err := binary.Read(reader, binary.BigEndian, &datagramCount); err != nil {
+ return nil, err
+ }
+ datagrams := make([][]byte, 0, datagramCount)
+ for range datagramCount {
+ var size uint16
+ if err := binary.Read(reader, binary.BigEndian, &size); err != nil {
+ return nil, err
+ }
+ datagram := make([]byte, size)
+ if _, err := io.ReadFull(reader, datagram); err != nil {
+ return nil, err
+ }
+ datagrams = append(datagrams, datagram)
+ }
+ if reader.Len() != 0 {
+ return nil, errTrailingBatchData
+ }
+
+ return datagrams, nil
+}
diff --git a/examples/mdns/server/main.go b/examples/mdns/server/main.go
new file mode 100644
index 000000000..1e69a990c
--- /dev/null
+++ b/examples/mdns/server/main.go
@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: 2026 The Pion community
+// SPDX-License-Identifier: MIT
+
+// Command server accepts a DTLS handshake from the mDNS demo client.
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/pion/dtls/v4/examples/mdns/internal/mdnsdemo"
+)
+
+func main() {
+ if err := mdnsdemo.RunServer(context.Background()); err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/examples/resumption-dtls13/README.md b/examples/resumption-dtls13/README.md
new file mode 100644
index 000000000..2a406ffbe
--- /dev/null
+++ b/examples/resumption-dtls13/README.md
@@ -0,0 +1,63 @@
+# DetachedConn DTLS 1.3 resumption and 0-RTT
+
+A resumption DTLS server client and server demo. The server accepts one client at a time,
+
+On first start the server generates a self-signed localhost certificate and private
+key using saving them as `server.crt` and `server.key`. Later starts reuse
+them.
+
+```sh
+go run ./ -server
+```
+
+In another terminal, start the client. It prompts for the session-file passphrase, choose one on the first run and reuse it afterward:
+
+```sh
+go run ./
+```
+
+The client verifies `server.crt` for `localhost`. It saves the ticket identity,
+resumption secret, and metadata to `session.enc` when you enter `/quit` or press
+Ctrl+C. The file uses scrypt (N=32768, r=8, p=1), AES-256-GCM, a fresh random salt
+and nonce, The passphrase encrypts the local ticket file.
+
+## How this works
+
+This example uses DTLS 1.3 PSK-based resumption:
+
+- The first connection authenticates the server using its certificate and derives
+ a resumption PSK associated with a session ticket.
+- Later connections use the cached ticket and PSK for resumption and, optionally,
+ 0-RTT early data. The encrypted client file stores both the ticket and secret.
+
+Restarting the server loses its in-memory tickets and PSKs, so an old client
+ticket falls back to a full fresh certificate-authenticated handshake.
+
+1. **First run:** both peers do full certificate handshake.
+2. **Normal resumption:** restart the client with the same running server and
+ session file. At the early-data prompt, press Enter to skip 0-rtt.
+3. **0-RTT:** restart the client again and enter text at the early-data prompt.
+ The client sends it on `DetachedEarlyDataReady`. The server prints the early
+ text sent with the first epoch.
+4. **Server restart:** stop and restart the server.
+ Because its in-memory tickets are gone. The next client connection falls back to a
+ full handshake. If early text was requested, the client retries that echo text
+ after the handshake when early data is rejected or unavailable.
+
+> [!WARNING]
+> Applications must implement [`Claim`](https://github.com/pion/dtls/blob/16cc07c2682746644daa5c4297cab0468853cdfb/session.go#L79-L87)
+> to enable 0-RTT. Pion requires this interface but cannot verify that the
+> user's implementation provides the required single-use guarantee.
+>
+> `Claim` must atomically return `true` only once per ticket. Claims must be shared
+> across all servers accepting those tickets and retained until `expiresAt`, even
+> if the session is updated or deleted. Otherwise, the same ticket's early data
+> could be accepted more than once. Claiming a ticket must preserve it for ordinary
+> resumption.
+>
+> This local demo skips the server's cookie exchange to allow 0-RTT.
+> Production deployments might need real address validation and DoS
+> protection. An underlying transport such as ICE may provide address validation,
+> For most applications, prefer session resumption without 0-RTT unless your application can safely
+> handle replayed requests (Implements Claim safely) and benefits from lower latency, such as an IoT
+> application on a high-latency network.
diff --git a/examples/resumption-dtls13/main.go b/examples/resumption-dtls13/main.go
new file mode 100644
index 000000000..1ceb28045
--- /dev/null
+++ b/examples/resumption-dtls13/main.go
@@ -0,0 +1,469 @@
+// SPDX-FileCopyrightText: 2026 The Pion community
+// SPDX-License-Identifier: MIT
+
+// Package main demonstrates DTLS resumption and optional client 0-RTT.
+package main
+
+import (
+ "bufio"
+ "context"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "net"
+ "os"
+ "os/signal"
+ "strings"
+ "time"
+
+ "github.com/pion/dtls/v4"
+ "github.com/pion/dtls/v4/pkg/crypto/selfsign"
+ "github.com/pion/dtls/v4/pkg/protocol"
+ "golang.org/x/term"
+)
+
+const (
+ earlyLimit = 4096
+ serverAddress = "127.0.0.1:4444"
+ certificatePath = "server.crt"
+ keyPath = "server.key"
+ ticketPath = "session.enc"
+)
+
+var (
+ errCertificate = errors.New("CA file contains no certificates")
+ errPassphrase = errors.New("session-file passphrase must not be empty")
+ errEarlySize = errors.New("early text exceeds 4096 bytes")
+)
+
+type endpoint struct {
+ conn *dtls.DetachedConn
+ udp *net.UDPConn
+ server, ready, accepted bool
+ early string
+ cancel context.CancelFunc
+ store *connectionStore
+ pending [][]byte
+}
+
+func main() {
+ server := flag.Bool("server", false, "run the server")
+ flag.Parse()
+ ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
+
+ var err error
+ if *server {
+ err = serve(ctx)
+ } else {
+ err = client(ctx)
+ }
+ stop()
+ if err != nil && !errors.Is(err, context.Canceled) {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
+
+func serve(ctx context.Context) error {
+ certificate, err := loadServerCertificate(certificatePath, keyPath)
+ if err != nil {
+ return err
+ }
+ addr, err := net.ResolveUDPAddr("udp", serverAddress)
+ if err != nil {
+ return err
+ }
+ udp, err := net.ListenUDP("udp", addr)
+ if err != nil {
+ return err
+ }
+ defer udp.Close() //nolint:errcheck
+ store := newStore()
+ fmt.Println("Listening on", udp.LocalAddr(), "(handles one client at a time)")
+ for ctx.Err() == nil {
+ data, peer, err := readDatagram(udp)
+ if isTimeout(err) {
+ continue
+ }
+ if err != nil {
+ return err
+ }
+ peerStore := &connectionStore{sessionStore: store}
+ conn, err := dtls.DetachedServer(peer,
+ dtls.WithCertificates(certificate), dtls.WithSessionStore(peerStore, dtls.WithMaxEarlyDataSize(earlyLimit)),
+ dtls.WithMinVersion(protocol.Version1_3), dtls.WithMaxVersion(protocol.Version1_3),
+ dtls.WithInsecureSkipVerifyHello(true)) // HRR cookies prevent 0-RTT. Only do this if you want to do 0-RTT and have another way to validate the paths.
+ if err != nil {
+ return err
+ }
+ ep := &endpoint{conn: conn, udp: udp, server: true, store: peerStore}
+ if err = ep.start(ctx, data, peer); err == nil {
+ err = ep.run(ctx, peer, nil)
+ }
+ ep.close() //nolint:contextcheck
+ reportPeerError(err)
+ }
+
+ return ctx.Err()
+}
+
+func client(ctx context.Context) (result error) {
+ password, err := readPassphrase()
+ if err != nil {
+ return err
+ }
+ store := newStore()
+ saved, err := store.load(ticketPath, password)
+ if err != nil {
+ return err
+ }
+ scanner := bufio.NewScanner(os.Stdin)
+ early := ""
+ if saved {
+ fmt.Print("Saved session found. Optional 0-RTT text (Enter = normal resumption): ")
+ if scanner.Scan() {
+ early = scanner.Text()
+ }
+ if err = scanner.Err(); err != nil {
+ return err
+ }
+ }
+ if len(early) > earlyLimit {
+ return errEarlySize
+ }
+ conn, udp, peer, err := dial(store, early != "")
+ if err != nil {
+ return err
+ }
+ defer udp.Close() //nolint:errcheck
+ ep := &endpoint{conn: conn, udp: udp, early: early}
+ defer func() { //nolint:contextcheck
+ ep.close()
+ saveErr := store.save(ticketPath, password)
+ result = errors.Join(result, saveErr)
+ if saveErr == nil {
+ fmt.Println("Session saved:", ticketPath)
+ }
+ }()
+ if err = ep.start(ctx, nil, peer); err != nil {
+ return err
+ }
+ lines := make(chan string)
+ go readLines(ctx, scanner, lines)
+ fmt.Println("Type text after the handshake; /quit or ctrl+C to close and save the session")
+
+ return clientResult(ep.run(ctx, peer, lines))
+}
+
+func dial(store *sessionStore, early bool) (*dtls.DetachedConn, *net.UDPConn, net.Addr, error) {
+ pem, err := os.ReadFile(certificatePath)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ roots := x509.NewCertPool()
+ if !roots.AppendCertsFromPEM(pem) {
+ return nil, nil, nil, errCertificate
+ }
+ peer, err := net.ResolveUDPAddr("udp", serverAddress)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ udp, err := net.ListenUDP("udp", nil)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ limit := uint32(0)
+ if early {
+ limit = earlyLimit
+ }
+ conn, err := dtls.DetachedClient(peer, dtls.WithRootCAs(roots), dtls.WithServerName("localhost"),
+ dtls.WithSessionStore(store, dtls.WithMaxEarlyDataSize(limit)),
+ dtls.WithMinVersion(protocol.Version1_3), dtls.WithMaxVersion(protocol.Version1_3))
+ if err != nil {
+ _ = udp.Close()
+ }
+
+ return conn, udp, peer, err
+}
+
+func (e *endpoint) start(ctx context.Context, first []byte, peer net.Addr) error {
+ handshakeCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
+ e.cancel = cancel
+ if err := e.conn.Start(handshakeCtx); err != nil {
+ cancel()
+
+ return err
+ }
+ if first != nil {
+ return e.conn.HandleDatagram(first, peer)
+ }
+
+ return nil
+}
+
+func readLines(ctx context.Context, scanner *bufio.Scanner, lines chan<- string) {
+ defer close(lines)
+ for scanner.Scan() {
+ select {
+ case lines <- scanner.Text():
+ case <-ctx.Done():
+ return
+ }
+ }
+}
+
+func readDatagram(udp *net.UDPConn) ([]byte, net.Addr, error) {
+ if err := udp.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
+ return nil, nil, err
+ }
+ buffer := make([]byte, 65535)
+ n, peer, err := udp.ReadFrom(buffer)
+
+ return buffer[:n], peer, err
+}
+
+func isTimeout(err error) bool {
+ var netErr net.Error
+
+ return errors.As(err, &netErr) && netErr.Timeout()
+}
+
+func (e *endpoint) run(ctx context.Context, peer net.Addr, lines <-chan string) error {
+ for ctx.Err() == nil {
+ if err := e.drain(); err != nil { //nolint:contextcheck
+ return err
+ }
+ select {
+ case text, ok := <-lines:
+ if !ok || strings.TrimSpace(text) == "/quit" {
+ return nil
+ }
+ if !e.ready {
+ fmt.Println("Handshake pending; try again.")
+
+ continue
+ }
+ if _, err := e.conn.Write([]byte(text)); err != nil { //nolint:contextcheck
+ return err
+ }
+
+ continue
+ default:
+ }
+ if err := e.receive(peer); err != nil {
+ return err
+ }
+ }
+
+ return ctx.Err()
+}
+
+func (e *endpoint) drain() error {
+ for event := e.conn.NextEvent(); event.Kind != dtls.DetachedNoEvent; event = e.conn.NextEvent() {
+ if err := e.handleEvent(event); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (e *endpoint) handleEvent(event dtls.DetachedEvent) error {
+ switch event.Kind {
+ case dtls.DetachedWriteDatagrams:
+ return e.sendDatagrams(event)
+ case dtls.DetachedEarlyDataReady:
+ if _, err := e.conn.WriteEarlyData([]byte(e.early)); err != nil {
+ return err
+ }
+
+ case dtls.DetachedEarlyDataAccepted:
+ e.accepted = true
+ fmt.Println("0-RTT accepted")
+ case dtls.DetachedEarlyDataRejected:
+ fmt.Println("0-RTT rejected. will be sent after the handshake")
+ case dtls.DetachedHandshakeDone:
+ return e.handshakeDone()
+ case dtls.DetachedApplicationData:
+ return e.applicationData(event.Data)
+ case dtls.DetachedClosed:
+ return closedError(event.Err)
+ default:
+ }
+
+ return nil
+}
+
+func (e *endpoint) handshakeDone() error {
+ e.ready = true
+ e.cancel()
+ if e.server {
+ e.accepted = e.store.accepted.Load()
+ }
+ state, _ := e.conn.ConnectionState()
+ mode := "full handshake"
+ if len(state.IdentityHint) != 0 {
+ mode = "session resumption"
+ }
+ if e.accepted {
+ mode += " with early data"
+ }
+ fmt.Println(mode)
+ if e.server {
+ return e.flushEarlyEcho()
+ }
+ if !e.server && e.early != "" && !e.accepted {
+ _, err := e.conn.Write([]byte(e.early))
+
+ return err
+ }
+
+ return nil
+}
+
+func (e *endpoint) applicationData(data []byte) error {
+ if !e.ready {
+ e.accepted = true
+ fmt.Printf("0-RTT received: %q\n", data)
+ // Normal Write is available after handshake completion.
+ e.pending = append(e.pending, data)
+
+ return nil
+ }
+ fmt.Printf("Received: %q\n", data)
+ if e.server {
+ _, err := e.conn.Write(data)
+
+ return err
+ }
+
+ return nil
+}
+
+func (e *endpoint) close() {
+ if e.cancel != nil {
+ defer e.cancel()
+ }
+ _ = e.conn.Close()
+ _ = e.drain()
+}
+
+func reportPeerError(err error) {
+ if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, dtls.ErrConnClosed) {
+ fmt.Println("Peer:", err)
+ }
+}
+
+func (e *endpoint) receive(peer net.Addr) error {
+ data, from, err := readDatagram(e.udp)
+ if isTimeout(err) {
+ return nil
+ }
+ if err != nil {
+ return err
+ }
+ if from.String() != peer.String() {
+ return nil
+ }
+
+ return e.conn.HandleDatagram(data, from)
+}
+
+func (e *endpoint) sendDatagrams(event dtls.DetachedEvent) error {
+ for _, data := range event.Datagrams {
+ if _, err := e.udp.WriteTo(data, event.Addr); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (e *endpoint) flushEarlyEcho() error {
+ for _, data := range e.pending {
+ if _, err := e.conn.Write(data); err != nil {
+ return err
+ }
+ }
+ e.pending = nil
+
+ return nil
+}
+
+func closedError(err error) error {
+ if err == nil || errors.Is(err, dtls.ErrConnClosed) {
+ return io.EOF
+ }
+
+ return err
+}
+
+func clientResult(err error) error {
+ if errors.Is(err, context.Canceled) || errors.Is(err, io.EOF) || errors.Is(err, dtls.ErrConnClosed) {
+ return nil
+ }
+
+ return err
+}
+
+// only generate an identity when both files are missing.
+func loadServerCertificate(certPath, keyPath string) (tls.Certificate, error) {
+ certificate, loadErr := tls.LoadX509KeyPair(certPath, keyPath)
+ if loadErr == nil {
+ return certificate, nil
+ }
+ for _, path := range []string{certPath, keyPath} {
+ if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) {
+ return tls.Certificate{}, loadErr
+ }
+ }
+ certificate, err := selfsign.GenerateSelfSignedWithDNS("localhost")
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+ key, err := x509.MarshalPKCS8PrivateKey(certificate.PrivateKey)
+ if err != nil {
+ return tls.Certificate{}, err
+ }
+ if err = writeIdentityFile(keyPath, &pem.Block{Type: "PRIVATE KEY", Bytes: key}); err != nil {
+ return tls.Certificate{}, err
+ }
+ if err = writeIdentityFile(certPath, &pem.Block{Type: "CERTIFICATE", Bytes: certificate.Certificate[0]}); err != nil {
+ return tls.Certificate{}, errors.Join(err, os.Remove(keyPath))
+ }
+ fmt.Println("Generated localhost identity:", certPath, keyPath)
+
+ return certificate, nil
+}
+
+func writeIdentityFile(path string, block *pem.Block) error {
+ file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) //nolint:gosec
+ if err != nil {
+ return err
+ }
+ writeErr := pem.Encode(file, block)
+ if err = errors.Join(writeErr, file.Close()); err != nil {
+ return errors.Join(err, os.Remove(path))
+ }
+
+ return nil
+}
+
+func readPassphrase() (string, error) {
+ fmt.Fprint(os.Stderr, "Session-file passphrase (hidden): ")
+ password, err := term.ReadPassword(int(os.Stdin.Fd())) //nolint:gosec
+ fmt.Fprintln(os.Stderr)
+ if err != nil {
+ return "", err
+ }
+ defer clear(password)
+ if len(password) == 0 {
+ return "", errPassphrase
+ }
+
+ return string(password), nil
+}
diff --git a/examples/resumption-dtls13/store.go b/examples/resumption-dtls13/store.go
new file mode 100644
index 000000000..5c93420ca
--- /dev/null
+++ b/examples/resumption-dtls13/store.go
@@ -0,0 +1,175 @@
+// SPDX-FileCopyrightText: 2026 The Pion community
+// SPDX-License-Identifier: MIT
+
+package main
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "encoding/json"
+ "errors"
+ "os"
+ "path/filepath"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/pion/dtls/v4"
+ "golang.org/x/crypto/scrypt"
+)
+
+var errTicketFile = errors.New("invalid ticket file or incorrect passphrase")
+
+type sessionStore struct {
+ sync.Mutex
+ sessions map[string]dtls.Session
+ claims map[string]time.Time
+}
+
+func newStore() *sessionStore {
+ return &sessionStore{sessions: make(map[string]dtls.Session), claims: make(map[string]time.Time)}
+}
+
+func (s *sessionStore) Set(key []byte, session dtls.Session) error {
+ s.Lock()
+ defer s.Unlock()
+ s.sessions[string(key)] = session
+
+ return nil
+}
+
+func (s *sessionStore) Get(key []byte) (dtls.Session, error) {
+ s.Lock()
+ defer s.Unlock()
+ session := s.sessions[string(key)]
+ if session.Ticket != nil && time.Since(session.Ticket.CreatedAt) >= time.Duration(session.Ticket.Lifetime)*time.Second {
+ delete(s.sessions, string(key))
+
+ return dtls.Session{}, nil
+ }
+
+ return session, nil
+}
+
+func (s *sessionStore) Del(key []byte) error {
+ s.Lock()
+ defer s.Unlock()
+ delete(s.sessions, string(key))
+
+ return nil
+}
+
+func (s *sessionStore) Claim(ticket []byte, expiresAt time.Time) (bool, error) {
+ s.Lock()
+ defer s.Unlock()
+ now := time.Now()
+ for key, expiry := range s.claims {
+ if !now.Before(expiry) {
+ delete(s.claims, key)
+ }
+ }
+ if _, used := s.claims[string(ticket)]; used || !now.Before(expiresAt) {
+ return false, nil
+ }
+ s.claims[string(ticket)] = expiresAt
+
+ return true, nil
+}
+
+// random 16-byte salt, GCM nonce, authenticated encrypted JSON.
+func ticketCipher(password string, salt []byte) (cipher.AEAD, error) {
+ key, err := scrypt.Key([]byte(password), salt, 1<<15, 8, 1, 32)
+ if err != nil {
+ return nil, err
+ }
+ defer clear(key)
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ return cipher.NewGCM(block)
+}
+
+func (s *sessionStore) load(path, password string) (bool, error) {
+ data, err := os.ReadFile(path) //nolint:gosec
+ if errors.Is(err, os.ErrNotExist) {
+ return false, nil
+ }
+ if err != nil {
+ return false, err
+ }
+ if len(data) < 16+12+16 {
+ return false, errTicketFile
+ }
+ aead, err := ticketCipher(password, data[:16])
+ if err != nil {
+ return false, err
+ }
+ plaintext, err := aead.Open(nil, data[16:28], data[28:], nil)
+ if err != nil {
+ return false, errTicketFile
+ }
+ defer clear(plaintext)
+ if err = json.Unmarshal(plaintext, &s.sessions); err != nil {
+ return false, err
+ }
+ if s.sessions == nil {
+ s.sessions = make(map[string]dtls.Session)
+ }
+ for key := range s.sessions {
+ // drops expired tickets before offering 0-RTT.
+ _, _ = s.Get([]byte(key))
+ }
+
+ return len(s.sessions) > 0, nil
+}
+
+func (s *sessionStore) save(path, password string) error {
+ s.Lock()
+ plaintext, err := json.Marshal(s.sessions)
+ s.Unlock()
+ if err != nil {
+ return err
+ }
+ defer clear(plaintext)
+ salt := make([]byte, 16)
+ if _, err = rand.Read(salt); err != nil {
+ return err
+ }
+ aead, err := ticketCipher(password, salt)
+ if err != nil {
+ return err
+ }
+ nonce := make([]byte, aead.NonceSize())
+ if _, err = rand.Read(nonce); err != nil {
+ return err
+ }
+ data := aead.Seal(nil, nonce, plaintext, nil)
+ data = append(append(salt, nonce...), data...) //nolint:makezero
+ file, err := os.CreateTemp(filepath.Dir(path), ".dtls-ticket-*")
+ if err != nil {
+ return err
+ }
+ defer os.Remove(file.Name()) //nolint:errcheck
+ _, writeErr := file.Write(data)
+ closeErr := file.Close()
+ if err = errors.Join(writeErr, closeErr); err != nil {
+ return err
+ }
+
+ return os.Rename(file.Name(), path) //nolint:gosec
+}
+
+type connectionStore struct {
+ *sessionStore
+ accepted atomic.Bool
+}
+
+func (s *connectionStore) Claim(ticket []byte, expiresAt time.Time) (bool, error) {
+ accepted, err := s.sessionStore.Claim(ticket, expiresAt)
+ s.accepted.Store(accepted)
+
+ return accepted, err
+}
diff --git a/examples/util/util.go b/examples/util/util.go
index 0bd07aacd..e2c4eabf9 100644
--- a/examples/util/util.go
+++ b/examples/util/util.go
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT
// Package util provides auxiliary utilities used in examples
-package util //nolint: revive
+package util
import (
"bufio"
diff --git a/flight.go b/flight.go
deleted file mode 100644
index 0544177d9..000000000
--- a/flight.go
+++ /dev/null
@@ -1,104 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-/*
- DTLS messages are grouped into a series of message flights, according
- to the diagrams below. Although each flight of messages may consist
- of a number of messages, they should be viewed as monolithic for the
- purpose of timeout and retransmission.
- https://tools.ietf.org/html/rfc4347#section-4.2.4
-
- Message flights for full handshake:
-
- Client Server
- ------ ------
- Waiting Flight 0
-
- ClientHello --------> Flight 1
-
- <------- HelloVerifyRequest Flight 2
-
- ClientHello --------> Flight 3
-
- ServerHello \
- Certificate* \
- ServerKeyExchange* Flight 4
- CertificateRequest* /
- <-------- ServerHelloDone /
-
- Certificate* \
- ClientKeyExchange \
- CertificateVerify* Flight 5
- [ChangeCipherSpec] /
- Finished --------> /
-
- [ChangeCipherSpec] \ Flight 6
- <-------- Finished /
-
- Message flights for session-resuming handshake (no cookie exchange):
-
- Client Server
- ------ ------
- Waiting Flight 0
-
- ClientHello --------> Flight 1
-
- ServerHello \
- [ChangeCipherSpec] Flight 4b
- <-------- Finished /
-
- [ChangeCipherSpec] \ Flight 5b
- Finished --------> /
-
- [ChangeCipherSpec] \ Flight 6
- <-------- Finished /
-*/
-
-type flightVal uint8
-
-const (
- flight0 flightVal = iota + 1
- flight1
- flight2
- flight3
- flight4
- flight4b
- flight5
- flight5b
- flight6
-)
-
-func (f flightVal) String() string { //nolint:cyclop
- switch f {
- case flight0:
- return "Flight 0"
- case flight1:
- return "Flight 1"
- case flight2:
- return "Flight 2"
- case flight3:
- return "Flight 3"
- case flight4:
- return "Flight 4"
- case flight4b:
- return "Flight 4b"
- case flight5:
- return "Flight 5"
- case flight5b:
- return "Flight 5b"
- case flight6:
- return "Flight 6"
- default:
- return "Invalid Flight"
- }
-}
-
-func (f flightVal) isLastSendFlight() bool {
- return f == flight6 || f == flight5b
-}
-
-func (f flightVal) isLastRecvFlight() bool {
- return f == flight5 || f == flight4b
-}
diff --git a/flight0handler.go b/flight0handler.go
deleted file mode 100644
index 99efea679..000000000
--- a/flight0handler.go
+++ /dev/null
@@ -1,197 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
- "crypto/rand"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
-)
-
-// renegotiationInfoSCSV is TLS_EMPTY_RENEGOTIATION_INFO_SCSV defined in RFC 5746.
-// https://datatracker.ietf.org/doc/html/rfc5746#section-3.3.
-const renegotiationInfoSCSV uint16 = 0x00ff
-
-//nolint:cyclop,gocognit
-func flight0Parse(
- _ context.Context,
- _ flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) (flightVal, *alert.Alert, error) {
- seq, msgs, ok := cache.fullPullMap(0, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
- )
- if !ok {
- // No valid message received. Keep reading
- return 0, nil, nil
- }
-
- // Connection Identifiers must be negotiated afresh on session resumption.
- // https://datatracker.ietf.org/doc/html/rfc9146#name-the-connection_id-extension
- state.SetLocalConnectionID(nil)
- state.RemoteConnectionID = nil
-
- state.HandshakeRecvSequence = seq
-
- var clientHello *handshake.MessageClientHello
-
- // Validate type
- if clientHello, ok = msgs[handshake.TypeClientHello].(*handshake.MessageClientHello); !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
-
- if !clientHello.Version.Equal(protocol.Version1_2) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion},
- dtlserrors.ErrUnsupportedProtocolVersion
- }
-
- state.RemoteRandom = clientHello.Random
-
- cipherSuites := []CipherSuite{}
- for _, id := range clientHello.CipherSuiteIDs {
- if id == renegotiationInfoSCSV {
- state.RemoteSupportsRenegotiation = true
-
- continue
- }
- if c := cipherSuiteForID(CipherSuiteID(id), cfg.customCipherSuites); c != nil {
- cipherSuites = append(cipherSuites, c)
- }
- }
-
- cipherSuites = filterCipherSuitesForVersion(cipherSuites, protocol.Version1_2)
-
- if state.CipherSuite, ok = findMatchingCipherSuite(cipherSuites, cfg.localCipherSuites); !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrCipherSuiteNoIntersection //nolint:lll
- }
-
- for _, val := range clientHello.Extensions {
- switch ext := val.(type) {
- case *extension.SupportedEllipticCurves:
- if len(ext.EllipticCurves) == 0 {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrNoSupportedEllipticCurves //nolint:lll
- }
- state.NamedCurve = ext.EllipticCurves[0]
- case *extension.UseSRTP:
- profile, ok := findMatchingSRTPProfile(cfg.localSRTPProtectionProfiles, ext.ProtectionProfiles)
- if !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrServerNoMatchingSRTPProfile //nolint:lll
- }
- state.SetSRTPProtectionProfile(profile)
- state.RemoteSRTPMasterKeyIdentifier = ext.MasterKeyIdentifier
- case *extension.UseExtendedMasterSecret:
- if cfg.extendedMasterSecret != DisableExtendedMasterSecret {
- state.ExtendedMasterSecret = true
- }
- case *extension.ServerName:
- state.ServerName = ext.ServerName // remote server name
- case *extension.RenegotiationInfo:
- state.RemoteSupportsRenegotiation = true
- case *extension.ALPN:
- state.PeerSupportedProtocols = ext.ProtocolNameList
- case *extension.ConnectionID:
- // Only set connection ID to be sent if server supports connection
- // IDs.
- if cfg.connectionIDGenerator != nil {
- state.RemoteConnectionID = ext.CID
- }
- case *extension.SignatureAlgorithmsCert:
- // Store the client's certificate signature schemes for later validation
- state.RemoteCertSignatureSchemes = ext.SignatureHashAlgorithms
- }
- }
-
- // If the client doesn't support connection IDs, the server should not
- // expect one to be sent.
- if state.RemoteConnectionID == nil {
- state.SetLocalConnectionID(nil)
- }
-
- if cfg.extendedMasterSecret == RequireExtendedMasterSecret && !state.ExtendedMasterSecret {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrServerRequiredButNoClientEMS //nolint:lll
- }
-
- if state.LocalKeypair == nil {
- var err error
- state.LocalKeypair, err = elliptic.GenerateKeypair(state.NamedCurve)
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, err
- }
- }
-
- nextFlight := flight2
-
- if cfg.insecureSkipHelloVerify {
- nextFlight = flight4
- }
-
- return handleHelloResume(clientHello.SessionID, state, cfg, nextFlight)
-}
-
-func handleHelloResume(
- sessionID []byte,
- state *dtlsstate.State,
- cfg *handshakeConfig,
- next flightVal,
-) (flightVal, *alert.Alert, error) {
- if len(sessionID) > 0 && cfg.sessionStore != nil {
- if s, err := cfg.sessionStore.Get(sessionID); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- } else if s.ID != nil {
- cfg.log.Tracef("[handshake] resume session: %x", sessionID)
-
- state.SessionID = sessionID
- state.MasterSecret = s.Secret
-
- if err := state.InitCipherSuite(); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
-
- clientRandom := state.LocalRandom.MarshalFixed()
- cfg.writeKeyLog(keyLogLabelTLS12, clientRandom[:], state.MasterSecret)
-
- return flight4b, nil, nil
- }
- }
-
- return next, nil, nil
-}
-
-func flight0Generate(
- _ flightConn,
- state *dtlsstate.State,
- _ *handshakeCache,
- cfg *handshakeConfig,
-) ([]*packet, *alert.Alert, error) {
- // Initialize
- if !cfg.insecureSkipHelloVerify {
- state.Cookie = make([]byte, cookieLength)
- if _, err := rand.Read(state.Cookie); err != nil {
- return nil, nil, err
- }
- }
-
- var zeroEpoch uint16
- state.LocalEpoch.Store(zeroEpoch)
- state.RemoteEpoch.Store(zeroEpoch)
- if len(cfg.ellipticCurves) < 1 {
- return nil, nil, dtlserrors.ErrEmptyEllipticCurves
- }
- state.NamedCurve = cfg.ellipticCurves[0]
-
- if err := state.LocalRandom.Populate(); err != nil {
- return nil, nil, err
- }
-
- return nil, nil, nil
-}
diff --git a/flight1handler.go b/flight1handler.go
deleted file mode 100644
index 0e41c4714..000000000
--- a/flight1handler.go
+++ /dev/null
@@ -1,195 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
-)
-
-func flight1Parse(
- ctx context.Context,
- conn flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) (flightVal, *alert.Alert, error) {
- // HelloVerifyRequest can be skipped by the server,
- // so allow ServerHello during flight1 also
- seq, msgs, ok := cache.fullPullMap(state.HandshakeRecvSequence, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeHelloVerifyRequest, cfg.initialEpoch, false, true},
- handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, true},
- )
- if !ok {
- // No valid message received. Keep reading
- return 0, nil, nil
- }
-
- if _, ok := msgs[handshake.TypeServerHello]; ok {
- // Flight1 and flight2 were skipped.
- // Parse as flight3.
- return flight3Parse(ctx, conn, state, cache, cfg)
- }
-
- if h, ok := msgs[handshake.TypeHelloVerifyRequest].(*handshake.MessageHelloVerifyRequest); ok {
- // DTLS 1.2 clients must not assume that the server will use the protocol version
- // specified in HelloVerifyRequest message. RFC 6347 Section 4.2.1
- if !h.Version.Equal(protocol.Version1_0) && !h.Version.Equal(protocol.Version1_2) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion},
- dtlserrors.ErrUnsupportedProtocolVersion
- }
- state.Cookie = append([]byte{}, h.Cookie...)
- state.HandshakeRecvSequence = seq
-
- return flight3, nil, nil
- }
-
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
-}
-
-//nolint:cyclop
-func flight1Generate(
- conn flightConn,
- state *dtlsstate.State,
- _ *handshakeCache,
- cfg *handshakeConfig,
-) ([]*packet, *alert.Alert, error) {
- var zeroEpoch uint16
- state.LocalEpoch.Store(zeroEpoch)
- state.RemoteEpoch.Store(zeroEpoch)
- if len(cfg.ellipticCurves) < 1 {
- return nil, nil, dtlserrors.ErrEmptyEllipticCurves
- }
- state.NamedCurve = cfg.ellipticCurves[0]
- state.Cookie = nil
-
- if err := state.LocalRandom.Populate(); err != nil {
- return nil, nil, err
- }
-
- if cfg.helloRandomBytesGenerator != nil {
- state.LocalRandom.RandomBytes = cfg.helloRandomBytesGenerator()
- }
-
- extensions := []extension.Extension{
- &extension.SupportedSignatureAlgorithms{
- SignatureHashAlgorithms: cfg.localSignatureSchemes,
- },
- &extension.RenegotiationInfo{
- RenegotiatedConnection: 0,
- },
- }
-
- if len(cfg.localCertSignatureSchemes) > 0 {
- extensions = append(extensions, &extension.SignatureAlgorithmsCert{
- SignatureHashAlgorithms: cfg.localCertSignatureSchemes,
- })
- }
-
- var setEllipticCurveCryptographyClientHelloExtensions bool
- for _, c := range cfg.localCipherSuites {
- if c.ECC() {
- setEllipticCurveCryptographyClientHelloExtensions = true
-
- break
- }
- }
-
- if setEllipticCurveCryptographyClientHelloExtensions {
- extensions = append(extensions, []extension.Extension{
- &extension.SupportedEllipticCurves{
- EllipticCurves: cfg.ellipticCurves,
- },
- &extension.SupportedPointFormats{
- PointFormats: []elliptic.CurvePointFormat{elliptic.CurvePointFormatUncompressed},
- },
- }...)
- }
-
- if len(cfg.localSRTPProtectionProfiles) > 0 {
- extensions = append(extensions, &extension.UseSRTP{
- ProtectionProfiles: cfg.localSRTPProtectionProfiles,
- MasterKeyIdentifier: cfg.localSRTPMasterKeyIdentifier,
- })
- }
-
- if cfg.extendedMasterSecret == RequestExtendedMasterSecret ||
- cfg.extendedMasterSecret == RequireExtendedMasterSecret {
- extensions = append(extensions, &extension.UseExtendedMasterSecret{
- Supported: true,
- })
- }
-
- if len(cfg.serverName) > 0 {
- extensions = append(extensions, &extension.ServerName{ServerName: cfg.serverName})
- }
-
- if len(cfg.supportedProtocols) > 0 {
- extensions = append(extensions, &extension.ALPN{ProtocolNameList: cfg.supportedProtocols})
- }
-
- if cfg.sessionStore != nil {
- cfg.log.Tracef("[handshake] try to resume session")
- if s, err := cfg.sessionStore.Get(conn.sessionKey()); err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- } else if s.ID != nil {
- cfg.log.Tracef("[handshake] get saved session: %x", s.ID)
-
- state.SessionID = s.ID
- state.MasterSecret = s.Secret
- }
- }
-
- // If we have a connection ID generator, use it. The CID may be zero length,
- // in which case we are just requesting that the server send us a CID to
- // use.
- if cfg.connectionIDGenerator != nil {
- state.SetLocalConnectionID(cfg.connectionIDGenerator())
- // The presence of a generator indicates support for connection IDs. We
- // use the presence of a non-nil local CID in flight 3 to determine
- // whether we send a CID in the second ClientHello, so we convert any
- // nil CID returned by a generator to []byte{}.
- if state.GetLocalConnectionID() == nil {
- state.SetLocalConnectionID([]byte{})
- }
- extensions = append(extensions, &extension.ConnectionID{CID: state.GetLocalConnectionID()})
- }
-
- clientHello := &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- SessionID: state.SessionID,
- Cookie: state.Cookie,
- Random: state.LocalRandom,
- CipherSuiteIDs: cipherSuiteIDs(cfg.localCipherSuites),
- CompressionMethods: defaultCompressionMethods(),
- Extensions: extensions,
- }
-
- var content handshake.Handshake
-
- if cfg.clientHelloMessageHook != nil {
- content = handshake.Handshake{Message: cfg.clientHelloMessageHook(*clientHello)}
- } else {
- content = handshake.Handshake{Message: clientHello}
- }
-
- return []*packet{
- {
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &content,
- },
- },
- }, nil, nil
-}
diff --git a/flight2handler.go b/flight2handler.go
deleted file mode 100644
index c92d70724..000000000
--- a/flight2handler.go
+++ /dev/null
@@ -1,80 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "bytes"
- "context"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
-)
-
-func flight2Parse(
- ctx context.Context,
- c flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) (flightVal, *alert.Alert, error) {
- seq, msgs, ok := cache.fullPullMap(state.HandshakeRecvSequence, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
- )
- if !ok {
- // Client may retransmit the first ClientHello when HelloVerifyRequest is dropped.
- // Parse as flight 0 in this case.
- return flight0Parse(ctx, c, state, cache, cfg)
- }
- state.HandshakeRecvSequence = seq
-
- var clientHello *handshake.MessageClientHello
-
- // Validate type
- if clientHello, ok = msgs[handshake.TypeClientHello].(*handshake.MessageClientHello); !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
-
- if !clientHello.Version.Equal(protocol.Version1_2) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion},
- dtlserrors.ErrUnsupportedProtocolVersion
- }
-
- if len(clientHello.Cookie) == 0 {
- return 0, nil, nil
- }
- if !bytes.Equal(state.Cookie, clientHello.Cookie) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.AccessDenied}, dtlserrors.ErrCookieMismatch
- }
-
- return flight4, nil, nil
-}
-
-func flight2Generate(
- _ flightConn,
- state *dtlsstate.State,
- _ *handshakeCache,
- _ *handshakeConfig,
-) ([]*packet, *alert.Alert, error) {
- state.HandshakeSendSequence = 0
-
- return []*packet{
- {
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageHelloVerifyRequest{
- Version: protocol.Version1_2,
- Cookie: state.Cookie,
- },
- },
- },
- },
- }, nil, nil
-}
diff --git a/flight3handler.go b/flight3handler.go
deleted file mode 100644
index a91302c7b..000000000
--- a/flight3handler.go
+++ /dev/null
@@ -1,370 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "bytes"
- "context"
-
- "github.com/pion/dtls/v3/internal/ciphersuite/types"
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
- "github.com/pion/dtls/v3/pkg/crypto/prf"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
-)
-
-//nolint:gocognit,gocyclo,maintidx,cyclop
-func flight3Parse(
- ctx context.Context,
- conn flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) (flightVal, *alert.Alert, error) {
- // Clients may receive multiple HelloVerifyRequest messages with different cookies.
- // Clients SHOULD handle this by sending a new ClientHello with a cookie in response
- // to the new HelloVerifyRequest. RFC 6347 Section 4.2.1
- seq, msgs, ok := cache.fullPullMap(state.HandshakeRecvSequence, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeHelloVerifyRequest, cfg.initialEpoch, false, true},
- )
- if ok {
- if h, msgOk := msgs[handshake.TypeHelloVerifyRequest].(*handshake.MessageHelloVerifyRequest); msgOk {
- // DTLS 1.2 clients must not assume that the server will use the protocol version
- // specified in HelloVerifyRequest message. RFC 6347 Section 4.2.1
- if !h.Version.Equal(protocol.Version1_0) && !h.Version.Equal(protocol.Version1_2) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion}, dtlserrors.ErrUnsupportedProtocolVersion //nolint:lll
- }
- state.Cookie = append([]byte{}, h.Cookie...)
- state.HandshakeRecvSequence = seq
-
- return flight3, nil, nil
- }
- }
-
- _, msgs, ok = cache.fullPullMap(state.HandshakeRecvSequence, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
- )
- if !ok {
- // Don't have enough messages. Keep reading
- return 0, nil, nil
- }
-
- if serverHelloMsg, msgOk := msgs[handshake.TypeServerHello].(*handshake.MessageServerHello); msgOk { //nolint:nestif
- if !serverHelloMsg.Version.Equal(protocol.Version1_2) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion},
- dtlserrors.ErrUnsupportedProtocolVersion
- }
- for _, v := range serverHelloMsg.Extensions {
- switch ext := v.(type) {
- case *extension.UseSRTP:
- profile, found := findMatchingSRTPProfile(ext.ProtectionProfiles, cfg.localSRTPProtectionProfiles)
- if !found {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, dtlserrors.ErrClientNoMatchingSRTPProfile //nolint:lll
- }
- state.SetSRTPProtectionProfile(profile)
- state.RemoteSRTPMasterKeyIdentifier = ext.MasterKeyIdentifier
- case *extension.UseExtendedMasterSecret:
- if cfg.extendedMasterSecret != DisableExtendedMasterSecret {
- state.ExtendedMasterSecret = true
- }
- case *extension.ALPN:
- if len(ext.ProtocolNameList) > 1 { // This should be exactly 1, the zero case is handle when unmarshalling
- return 0, &alert.Alert{
- Level: alert.Fatal,
- Description: alert.InternalError,
- }, extension.ErrALPNInvalidFormat // Meh, internal error?
- }
- state.NegotiatedProtocol = ext.ProtocolNameList[0]
- case *extension.ConnectionID:
- // Only set connection ID to be sent if client supports connection
- // IDs.
- if cfg.connectionIDGenerator != nil {
- state.RemoteConnectionID = ext.CID
- }
- }
- }
- // If the server doesn't support connection IDs, the client should not
- // expect one to be sent.
- if state.RemoteConnectionID == nil {
- state.SetLocalConnectionID(nil)
- }
-
- if cfg.extendedMasterSecret == RequireExtendedMasterSecret && !state.ExtendedMasterSecret {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrClientRequiredButNoServerEMS //nolint:lll
- }
- if len(cfg.localSRTPProtectionProfiles) > 0 && state.GetSRTPProtectionProfile() == 0 {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrRequestedButNoSRTPExtension //nolint:lll
- }
-
- remoteCipherSuite := cipherSuiteForID(CipherSuiteID(*serverHelloMsg.CipherSuiteID), cfg.customCipherSuites)
- if remoteCipherSuite == nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrCipherSuiteNoIntersection //nolint:lll
- }
- if !cipherSuiteIDSupportsVersion(remoteCipherSuite.ID(), protocol.Version1_2) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrInvalidCipherSuite
- }
-
- selectedCipherSuite, found := findMatchingCipherSuite([]CipherSuite{remoteCipherSuite}, cfg.localCipherSuites)
- if !found {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrInvalidCipherSuite
- }
-
- state.CipherSuite = selectedCipherSuite
- state.RemoteRandom = serverHelloMsg.Random
- cfg.log.Tracef("[handshake] use cipher suite: %s", selectedCipherSuite.String())
-
- if len(serverHelloMsg.SessionID) > 0 && bytes.Equal(state.SessionID, serverHelloMsg.SessionID) {
- return handleResumption(ctx, conn, state, cache, cfg)
- }
-
- if len(state.SessionID) > 0 {
- cfg.log.Tracef("[handshake] clean old session : %s", state.SessionID)
- if err := cfg.sessionStore.Del(state.SessionID); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- }
-
- if cfg.sessionStore == nil {
- state.SessionID = []byte{}
- } else {
- state.SessionID = serverHelloMsg.SessionID
- }
-
- state.MasterSecret = []byte{}
- }
-
- if cfg.localPSKCallback != nil {
- seq, msgs, ok = cache.fullPullMap(state.HandshakeRecvSequence+1, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, true},
- handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
- )
- } else {
- seq, msgs, ok = cache.fullPullMap(state.HandshakeRecvSequence+1, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, true},
- handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, true},
- handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
- )
- }
- if !ok {
- // Don't have enough messages. Keep reading
- return 0, nil, nil
- }
- state.HandshakeRecvSequence = seq
-
- if h, ok := msgs[handshake.TypeCertificate].(*handshake.MessageCertificate); ok {
- state.PeerCertificates = h.Certificate
- } else if state.CipherSuite.AuthenticationType() == CipherSuiteAuthenticationTypeCertificate {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.NoCertificate}, dtlserrors.ErrInvalidCertificate
- }
-
- if h, ok := msgs[handshake.TypeServerKeyExchange].(*handshake.MessageServerKeyExchange); ok {
- alertPtr, err := handleServerKeyExchange(conn, state, cfg, h)
- if err != nil {
- return 0, alertPtr, err
- }
- }
-
- if creq, ok := msgs[handshake.TypeCertificateRequest].(*handshake.MessageCertificateRequest); ok {
- state.RemoteCertRequestAlgs = creq.SignatureHashAlgorithms
- state.RemoteRequestedCertificate = true
- }
-
- return flight5, nil, nil
-}
-
-func handleResumption(
- ctx context.Context,
- c flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) (flightVal, *alert.Alert, error) {
- if err := state.InitCipherSuite(); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
-
- // Now, encrypted packets can be handled
- if err := c.handleQueuedPackets(ctx); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
-
- _, msgs, ok := cache.fullPullMap(state.HandshakeRecvSequence+1, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
- )
- if !ok {
- // No valid message received. Keep reading
- return 0, nil, nil
- }
-
- var finished *handshake.MessageFinished
- if finished, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
- plainText := cache.pullAndMerge(
- handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
- )
-
- expectedVerifyData, err := prf.VerifyDataServer(state.MasterSecret, plainText, state.CipherSuite.HashFunc())
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- if !bytes.Equal(expectedVerifyData, finished.VerifyData) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, dtlserrors.ErrVerifyDataMismatch
- }
-
- clientRandom := state.LocalRandom.MarshalFixed()
- cfg.writeKeyLog(keyLogLabelTLS12, clientRandom[:], state.MasterSecret)
-
- return flight5b, nil, nil
-}
-
-//nolint:cyclop
-func handleServerKeyExchange(
- _ flightConn,
- state *dtlsstate.State,
- cfg *handshakeConfig,
- keyExchangeMessage *handshake.MessageServerKeyExchange,
-) (*alert.Alert, error) {
- var err error
- if state.CipherSuite == nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrInvalidCipherSuite
- }
- if cfg.localPSKCallback != nil { //nolint:nestif
- var psk []byte
- if psk, err = cfg.localPSKCallback(keyExchangeMessage.IdentityHint); err != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- state.IdentityHint = keyExchangeMessage.IdentityHint
- switch state.CipherSuite.KeyExchangeAlgorithm() {
- case types.KeyExchangeAlgorithmPsk:
- state.PreMasterSecret = prf.PSKPreMasterSecret(psk)
- case (types.KeyExchangeAlgorithmEcdhe | types.KeyExchangeAlgorithmPsk):
- if state.LocalKeypair, err = elliptic.GenerateKeypair(keyExchangeMessage.NamedCurve); err != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- state.PreMasterSecret, err = prf.EcdhePSKPreMasterSecret(
- psk,
- keyExchangeMessage.PublicKey,
- state.LocalKeypair.PrivateKey,
- state.LocalKeypair.Curve,
- )
- if err != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- default:
- return &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrInvalidCipherSuite
- }
- } else {
- if state.LocalKeypair, err = elliptic.GenerateKeypair(keyExchangeMessage.NamedCurve); err != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
-
- if state.PreMasterSecret, err = prf.PreMasterSecret(
- keyExchangeMessage.PublicKey,
- state.LocalKeypair.PrivateKey,
- state.LocalKeypair.Curve,
- ); err != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- }
-
- return nil, nil //nolint:nilnil
-}
-
-func flight3Generate(
- _ flightConn,
- state *dtlsstate.State,
- _ *handshakeCache,
- cfg *handshakeConfig,
-) ([]*packet, *alert.Alert, error) {
- extensions := []extension.Extension{
- &extension.SupportedSignatureAlgorithms{
- SignatureHashAlgorithms: cfg.localSignatureSchemes,
- },
- &extension.RenegotiationInfo{
- RenegotiatedConnection: 0,
- },
- }
-
- if len(cfg.localCertSignatureSchemes) > 0 {
- extensions = append(extensions, &extension.SignatureAlgorithmsCert{
- SignatureHashAlgorithms: cfg.localCertSignatureSchemes,
- })
- }
-
- if state.NamedCurve != 0 {
- extensions = append(extensions, []extension.Extension{
- &extension.SupportedEllipticCurves{
- EllipticCurves: cfg.ellipticCurves,
- },
- &extension.SupportedPointFormats{
- PointFormats: []elliptic.CurvePointFormat{elliptic.CurvePointFormatUncompressed},
- },
- }...)
- }
-
- if len(cfg.localSRTPProtectionProfiles) > 0 {
- extensions = append(extensions, &extension.UseSRTP{
- ProtectionProfiles: cfg.localSRTPProtectionProfiles,
- MasterKeyIdentifier: cfg.localSRTPMasterKeyIdentifier,
- })
- }
-
- if cfg.extendedMasterSecret == RequestExtendedMasterSecret ||
- cfg.extendedMasterSecret == RequireExtendedMasterSecret {
- extensions = append(extensions, &extension.UseExtendedMasterSecret{
- Supported: true,
- })
- }
-
- if len(cfg.serverName) > 0 {
- extensions = append(extensions, &extension.ServerName{ServerName: cfg.serverName})
- }
-
- if len(cfg.supportedProtocols) > 0 {
- extensions = append(extensions, &extension.ALPN{ProtocolNameList: cfg.supportedProtocols})
- }
-
- // If we sent a connection ID on the first ClientHello, send it on the
- // second.
- if state.GetLocalConnectionID() != nil {
- extensions = append(extensions, &extension.ConnectionID{CID: state.GetLocalConnectionID()})
- }
-
- clientHello := &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- SessionID: state.SessionID,
- Cookie: state.Cookie,
- Random: state.LocalRandom,
- CipherSuiteIDs: cipherSuiteIDs(cfg.localCipherSuites),
- CompressionMethods: defaultCompressionMethods(),
- Extensions: extensions,
- }
-
- var content handshake.Handshake
-
- if cfg.clientHelloMessageHook != nil {
- content = handshake.Handshake{Message: cfg.clientHelloMessageHook(*clientHello)}
- } else {
- content = handshake.Handshake{Message: clientHello}
- }
-
- return []*packet{
- {
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &content,
- },
- },
- }, nil, nil
-}
diff --git a/flight3handler_test.go b/flight3handler_test.go
deleted file mode 100644
index 4ce9f85cf..000000000
--- a/flight3handler_test.go
+++ /dev/null
@@ -1,98 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
- "math/rand"
- "testing"
- "time"
-
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
- dtlsnet "github.com/pion/dtls/v3/pkg/net"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
- "github.com/pion/transport/v4/dpipe"
- "github.com/pion/transport/v4/test"
- "github.com/stretchr/testify/assert"
-)
-
-// Assert that SupportedEllipticCurves is only sent when a ECC CipherSuite is available.
-func TestSupportedEllipticCurves(t *testing.T) {
- // Limit runtime in case of deadlocks
- lim := test.TimeOut(time.Second * 20)
- defer lim.Stop()
-
- // Check for leaking routines
- report := test.CheckRoutines(t)
- defer report()
-
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
-
- expectedCurves := defaultCurves
- var actualCurves []elliptic.Curve
-
- rand.Shuffle(len(expectedCurves), func(i, j int) {
- expectedCurves[i], expectedCurves[j] = expectedCurves[j], expectedCurves[i]
- })
-
- clientErr := make(chan error, 1)
- ca, cb := dpipe.Pipe()
- caAnalyzer := &connWithCallback{Conn: ca}
- caAnalyzer.onWrite = func(in []byte) {
- messages, err := recordlayer.UnpackDatagram(in)
- assert.NoError(t, err)
-
- for i := range messages {
- h := &handshake.Handshake{}
- _ = h.Unmarshal(messages[i][recordlayer.FixedHeaderSize:])
-
- if h.Header.Type == handshake.TypeClientHello { //nolint:nestif
- clientHello := &handshake.MessageClientHello{}
- msg, err := h.Message.Marshal()
-
- assert.NoError(t, err)
- assert.NoError(t, clientHello.Unmarshal(msg))
-
- for _, e := range clientHello.Extensions {
- if e.TypeValue() == extension.SupportedEllipticCurvesTypeValue {
- if c, ok := e.(*extension.SupportedEllipticCurves); ok {
- actualCurves = c.EllipticCurves
- }
- }
- }
- }
- }
- }
-
- go func() {
- if client, err := testClient(
- ctx,
- dtlsnet.PacketConnFromConn(caAnalyzer),
- caAnalyzer.RemoteAddr(),
- []ClientOption{
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- WithEllipticCurves(expectedCurves...),
- },
- false,
- ); err != nil {
- clientErr <- err
- } else {
- clientErr <- client.Close() // nolint:errcheck,contextcheck
- }
- }()
-
- server, err := testServer(ctx, dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), []ServerOption{
- WithCipherSuites(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256),
- }, true)
- assert.NoError(t, err)
- assert.NoError(t, server.Close())
- assert.NoError(t, <-clientErr)
-
- for i := range expectedCurves {
- assert.Equal(t, expectedCurves[i], actualCurves[i], "curves in SupportedEllipticCurves mismatch")
- }
-}
diff --git a/flight4bhandler.go b/flight4bhandler.go
deleted file mode 100644
index 43bcf4d82..000000000
--- a/flight4bhandler.go
+++ /dev/null
@@ -1,165 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "bytes"
- "context"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/prf"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
-)
-
-func flight4bParse(
- _ context.Context,
- _ flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) (flightVal, *alert.Alert, error) {
- _, msgs, ok := cache.fullPullMap(state.HandshakeRecvSequence, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
- )
- if !ok {
- // No valid message received. Keep reading
- return 0, nil, nil
- }
-
- var finished *handshake.MessageFinished
- if finished, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
-
- plainText := cache.pullAndMerge(
- handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
- )
-
- expectedVerifyData, err := prf.VerifyDataClient(state.MasterSecret, plainText, state.CipherSuite.HashFunc())
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- if !bytes.Equal(expectedVerifyData, finished.VerifyData) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, dtlserrors.ErrVerifyDataMismatch
- }
-
- // Other party may re-transmit the last flight. Keep state to be flight4b.
- return flight4b, nil, nil
-}
-
-//nolint:cyclop
-func flight4bGenerate(
- _ flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) ([]*packet, *alert.Alert, error) {
- var pkts []*packet
-
- extensions := []extension.Extension{&extension.RenegotiationInfo{
- RenegotiatedConnection: 0,
- }}
- if (cfg.extendedMasterSecret == RequestExtendedMasterSecret ||
- cfg.extendedMasterSecret == RequireExtendedMasterSecret) && state.ExtendedMasterSecret {
- extensions = append(extensions, &extension.UseExtendedMasterSecret{
- Supported: true,
- })
- }
- if state.GetSRTPProtectionProfile() != 0 {
- extensions = append(extensions, &extension.UseSRTP{
- ProtectionProfiles: []SRTPProtectionProfile{state.GetSRTPProtectionProfile()},
- MasterKeyIdentifier: cfg.localSRTPMasterKeyIdentifier,
- })
- }
-
- selectedProto, err := extension.ALPNProtocolSelection(cfg.supportedProtocols, state.PeerSupportedProtocols)
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.NoApplicationProtocol}, err
- }
- if selectedProto != "" {
- extensions = append(extensions, &extension.ALPN{
- ProtocolNameList: []string{selectedProto},
- })
- state.NegotiatedProtocol = selectedProto
- }
-
- cipherSuiteID := uint16(state.CipherSuite.ID())
- var serverHello handshake.Handshake
-
- serverHelloMessage := &handshake.MessageServerHello{
- Version: protocol.Version1_2,
- Random: state.LocalRandom,
- SessionID: state.SessionID,
- CipherSuiteID: &cipherSuiteID,
- CompressionMethod: defaultCompressionMethods()[0],
- Extensions: extensions,
- }
-
- if cfg.serverHelloMessageHook != nil {
- serverHello = handshake.Handshake{Message: cfg.serverHelloMessageHook(*serverHelloMessage)}
- } else {
- serverHello = handshake.Handshake{Message: serverHelloMessage}
- }
-
- serverHello.Header.MessageSequence = uint16(state.HandshakeSendSequence) //nolint:gosec // G115
-
- if len(state.LocalVerifyData) == 0 {
- plainText := cache.pullAndMerge(
- handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
- )
- raw, err := serverHello.Marshal()
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- plainText = append(plainText, raw...)
-
- state.LocalVerifyData, err = prf.VerifyDataServer(state.MasterSecret, plainText, state.CipherSuite.HashFunc())
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- }
-
- pkts = append(pkts,
- &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &serverHello,
- },
- },
- &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &protocol.ChangeCipherSpec{},
- },
- },
- &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- Epoch: 1,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageFinished{
- VerifyData: state.LocalVerifyData,
- },
- },
- },
- shouldEncrypt: true,
- resetLocalSequenceNumber: true,
- },
- )
-
- return pkts, nil, nil
-}
diff --git a/flight4handler.go b/flight4handler.go
deleted file mode 100644
index 49b00bc82..000000000
--- a/flight4handler.go
+++ /dev/null
@@ -1,502 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
- "crypto"
- "crypto/rand"
- "crypto/x509"
-
- "github.com/pion/dtls/v3/internal/ciphersuite"
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
- "github.com/pion/dtls/v3/pkg/crypto/prf"
- "github.com/pion/dtls/v3/pkg/crypto/signaturehash"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
-)
-
-//nolint:gocognit,gocyclo,lll,cyclop,maintidx
-func flight4Parse(
- ctx context.Context,
- conn flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) (flightVal, *alert.Alert, error) {
- seq, msgs, ok := cache.fullPullMap(state.HandshakeRecvSequence, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, true, true},
- handshakeCachePullRule{handshake.TypeClientKeyExchange, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeCertificateVerify, cfg.initialEpoch, true, true},
- )
- if !ok {
- // No valid message received. Keep reading
- return 0, nil, nil
- }
-
- // Validate type
- var clientKeyExchange *handshake.MessageClientKeyExchange
- if clientKeyExchange, ok = msgs[handshake.TypeClientKeyExchange].(*handshake.MessageClientKeyExchange); !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
-
- if h, hasCert := msgs[handshake.TypeCertificate].(*handshake.MessageCertificate); hasCert {
- state.PeerCertificates = h.Certificate
- // If the client offer its certificate, just disable session resumption.
- // Otherwise, we have to store the certificate identitfication and expire time.
- // And we have to check whether this certificate expired, revoked or changed.
- //
- // https://curl.se/docs/CVE-2016-5419.html
- state.SessionID = nil
- }
-
- //nolint:nestif
- if verify, hasVerify := msgs[handshake.TypeCertificateVerify].(*handshake.MessageCertificateVerify); hasVerify {
- if state.PeerCertificates == nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.NoCertificate}, dtlserrors.ErrCertificateVerifyNoCertificate
- }
-
- plainText := cache.pullAndMerge(
- handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeClientKeyExchange, cfg.initialEpoch, true, false},
- )
-
- // Verify that the pair of hash algorithm and signiture is listed.
- var validSignatureScheme bool
- for _, ss := range cfg.localSignatureSchemes {
- if ss.Hash == verify.HashAlgorithm && ss.Signature == verify.SignatureAlgorithm {
- validSignatureScheme = true
-
- break
- }
- }
- if !validSignatureScheme {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrNoAvailableSignatureSchemes
- }
-
- if err := verifyCertificateVerify(
- plainText,
- verify.HashAlgorithm,
- verify.SignatureAlgorithm,
- verify.Signature,
- state.PeerCertificates,
- ); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
- }
- var chains [][]*x509.Certificate
- var err error
- var verified bool
- if cfg.clientAuth >= VerifyClientCertIfGiven {
- // Use cert-specific algorithms if present, otherwise fall back to signature_algorithms per RFC 8446
- certAlgs := cfg.localCertSignatureSchemes
- if len(certAlgs) == 0 {
- certAlgs = cfg.localSignatureSchemes
- }
- if chains, err = verifyClientCert(state.PeerCertificates, cfg.clientCAs, certAlgs); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
- }
- verified = true
- }
- if cfg.verifyPeerCertificate != nil {
- if err := cfg.verifyPeerCertificate(state.PeerCertificates, chains); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
- }
- }
- state.PeerCertificatesVerified = verified
- } else if state.PeerCertificates != nil {
- // A certificate was received, but we haven't seen a CertificateVerify
- // keep reading until we receive one
- return 0, nil, nil
- }
-
- if !state.CipherSuite.IsInitialized() { //nolint:nestif
- serverRandom := state.LocalRandom.MarshalFixed()
- clientRandom := state.RemoteRandom.MarshalFixed()
-
- var err error
- var preMasterSecret []byte
- if state.CipherSuite.AuthenticationType() == CipherSuiteAuthenticationTypePreSharedKey {
- var psk []byte
- if psk, err = cfg.localPSKCallback(clientKeyExchange.IdentityHint); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- state.IdentityHint = clientKeyExchange.IdentityHint
- switch state.CipherSuite.KeyExchangeAlgorithm() {
- case CipherSuiteKeyExchangeAlgorithmPsk:
- preMasterSecret = prf.PSKPreMasterSecret(psk)
- case (CipherSuiteKeyExchangeAlgorithmPsk | CipherSuiteKeyExchangeAlgorithmEcdhe):
- if preMasterSecret, err = prf.EcdhePSKPreMasterSecret(
- psk,
- clientKeyExchange.PublicKey,
- state.LocalKeypair.PrivateKey,
- state.LocalKeypair.Curve,
- ); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- default:
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, dtlserrors.ErrInvalidCipherSuite
- }
- } else {
- preMasterSecret, err = prf.PreMasterSecret(
- clientKeyExchange.PublicKey,
- state.LocalKeypair.PrivateKey,
- state.LocalKeypair.Curve,
- )
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, err
- }
- }
-
- if state.ExtendedMasterSecret {
- var sessionHash []byte
- sessionHash, err = cache.sessionHash(state.CipherSuite.HashFunc(), cfg.initialEpoch)
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
-
- state.MasterSecret, err = prf.ExtendedMasterSecret(preMasterSecret, sessionHash, state.CipherSuite.HashFunc())
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- } else {
- state.MasterSecret, err = prf.MasterSecret(
- preMasterSecret,
- clientRandom[:],
- serverRandom[:],
- state.CipherSuite.HashFunc(),
- )
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- }
-
- if err := state.CipherSuite.Init(state.MasterSecret, clientRandom[:], serverRandom[:], false); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- cfg.writeKeyLog(keyLogLabelTLS12, clientRandom[:], state.MasterSecret)
- }
-
- if len(state.SessionID) > 0 {
- s := Session{
- ID: state.SessionID,
- Secret: state.MasterSecret,
- }
- cfg.log.Tracef("[handshake] save new session: %x", s.ID)
- if err := cfg.sessionStore.Set(state.SessionID, s); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- }
-
- // Now, encrypted packets can be handled
- if err := conn.handleQueuedPackets(ctx); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
-
- seq, msgs, ok = cache.fullPullMap(seq, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
- )
- if !ok {
- // No valid message received. Keep reading
- return 0, nil, nil
- }
- state.HandshakeRecvSequence = seq
-
- if _, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
-
- if state.CipherSuite.AuthenticationType() == CipherSuiteAuthenticationTypeAnonymous { //nolint:nestif
- if cfg.verifyConnection != nil {
- stateSnapshot, err := generateState(state)
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- if err := cfg.verifyConnection(stateSnapshot); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
- }
- }
-
- return flight6, nil, nil
- }
-
- switch cfg.clientAuth {
- case RequireAnyClientCert:
- if state.PeerCertificates == nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.NoCertificate}, dtlserrors.ErrClientCertificateRequired
- }
- case VerifyClientCertIfGiven:
- if state.PeerCertificates != nil && !state.PeerCertificatesVerified {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, dtlserrors.ErrClientCertificateNotVerified
- }
- case RequireAndVerifyClientCert:
- if state.PeerCertificates == nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.NoCertificate}, dtlserrors.ErrClientCertificateRequired
- }
- if !state.PeerCertificatesVerified {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, dtlserrors.ErrClientCertificateNotVerified
- }
- case NoClientCert, RequestClientCert:
- // go to flight6
- }
- if cfg.verifyConnection != nil {
- stateSnapshot, err := generateState(state)
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- if err := cfg.verifyConnection(stateSnapshot); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
- }
- }
-
- return flight6, nil, nil
-}
-
-//nolint:gocognit,cyclop,maintidx
-func flight4Generate(
- _ flightConn,
- state *dtlsstate.State,
- _ *handshakeCache,
- cfg *handshakeConfig,
-) ([]*packet, *alert.Alert, error) {
- extensions := []extension.Extension{}
-
- if (cfg.extendedMasterSecret == RequestExtendedMasterSecret ||
- cfg.extendedMasterSecret == RequireExtendedMasterSecret) && state.ExtendedMasterSecret {
- extensions = append(extensions, &extension.UseExtendedMasterSecret{
- Supported: true,
- })
- }
- if state.GetSRTPProtectionProfile() != 0 {
- extensions = append(extensions, &extension.UseSRTP{
- ProtectionProfiles: []SRTPProtectionProfile{state.GetSRTPProtectionProfile()},
- MasterKeyIdentifier: cfg.localSRTPMasterKeyIdentifier,
- })
- }
- if state.RemoteSupportsRenegotiation {
- extensions = append(extensions, &extension.RenegotiationInfo{
- RenegotiatedConnection: 0,
- })
- }
- if state.CipherSuite.AuthenticationType() == CipherSuiteAuthenticationTypeCertificate {
- extensions = append(extensions, &extension.SupportedPointFormats{
- PointFormats: []elliptic.CurvePointFormat{elliptic.CurvePointFormatUncompressed},
- })
- }
-
- selectedProto, err := extension.ALPNProtocolSelection(cfg.supportedProtocols, state.PeerSupportedProtocols)
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.NoApplicationProtocol}, err
- }
- if selectedProto != "" {
- extensions = append(extensions, &extension.ALPN{
- ProtocolNameList: []string{selectedProto},
- })
- state.NegotiatedProtocol = selectedProto
- }
-
- // If we have a connection ID generator, we are willing to use connection
- // IDs. We already know whether the client supports connection IDs from
- // parsing the ClientHello, so avoid setting local connection ID if the
- // client won't send it.
- if cfg.connectionIDGenerator != nil && state.RemoteConnectionID != nil {
- state.SetLocalConnectionID(cfg.connectionIDGenerator())
- extensions = append(extensions, &extension.ConnectionID{CID: state.GetLocalConnectionID()})
- }
-
- var pkts []*packet
- cipherSuiteID := uint16(state.CipherSuite.ID())
-
- if cfg.sessionStore != nil {
- state.SessionID = make([]byte, sessionLength)
- if _, err := rand.Read(state.SessionID); err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- }
-
- serverHello := &handshake.MessageServerHello{
- Version: protocol.Version1_2,
- Random: state.LocalRandom,
- SessionID: state.SessionID,
- CipherSuiteID: &cipherSuiteID,
- CompressionMethod: defaultCompressionMethods()[0],
- Extensions: extensions,
- }
-
- var content handshake.Handshake
-
- if cfg.serverHelloMessageHook != nil {
- content = handshake.Handshake{Message: cfg.serverHelloMessageHook(*serverHello)}
- } else {
- content = handshake.Handshake{Message: serverHello}
- }
-
- pkts = append(pkts, &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &content,
- },
- })
-
- switch {
- case state.CipherSuite.AuthenticationType() == CipherSuiteAuthenticationTypeCertificate:
- certificate, err := cfg.getCertificate(&ClientHelloInfo{
- ServerName: state.ServerName,
- CipherSuites: []ciphersuite.ID{state.CipherSuite.ID()},
- RandomBytes: state.RemoteRandom.RandomBytes,
- })
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, err
- }
-
- pkts = append(pkts, &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageCertificate{
- Certificate: certificate.Certificate,
- },
- },
- },
- })
-
- serverRandom := state.LocalRandom.MarshalFixed()
- clientRandom := state.RemoteRandom.MarshalFixed()
-
- signer, ok := certificate.PrivateKey.(crypto.Signer)
- if !ok {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, dtlserrors.ErrInvalidPrivateKey
- }
-
- // Find compatible signature scheme
- signatureHashAlgo, err := signaturehash.SelectSignatureScheme(cfg.localSignatureSchemes, signer)
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, err
- }
-
- signature, err := generateKeySignature(
- clientRandom[:],
- serverRandom[:],
- state.LocalKeypair.PublicKey,
- state.NamedCurve,
- signer,
- signatureHashAlgo.Hash,
- signatureHashAlgo.Signature,
- )
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- state.LocalKeySignature = signature
-
- pkts = append(pkts, &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageServerKeyExchange{
- EllipticCurveType: elliptic.CurveTypeNamedCurve,
- NamedCurve: state.NamedCurve,
- PublicKey: state.LocalKeypair.PublicKey,
- HashAlgorithm: signatureHashAlgo.Hash,
- SignatureAlgorithm: signatureHashAlgo.Signature,
- Signature: state.LocalKeySignature,
- },
- },
- },
- })
-
- if cfg.clientAuth > NoClientCert {
- // An empty list of certificateAuthorities signals to
- // the client that it may send any certificate in response
- // to our request. When we know the CAs we trust, then
- // we can send them down, so that the client can choose
- // an appropriate certificate to give to us.
- var certificateAuthorities [][]byte
- if cfg.clientCAs != nil {
- // nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR
- // because cert does not come from SystemCertPool and it's ok if certificate
- // authorities is empty.
- certificateAuthorities = cfg.clientCAs.Subjects()
- }
-
- certReq := &handshake.MessageCertificateRequest{
- CertificateTypes: []clientcertificate.Type{clientcertificate.RSASign, clientcertificate.ECDSASign},
- SignatureHashAlgorithms: cfg.localSignatureSchemes,
- CertificateAuthoritiesNames: certificateAuthorities,
- }
-
- var content handshake.Handshake
-
- if cfg.certificateRequestMessageHook != nil {
- content = handshake.Handshake{Message: cfg.certificateRequestMessageHook(*certReq)}
- } else {
- content = handshake.Handshake{Message: certReq}
- }
-
- pkts = append(pkts, &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &content,
- },
- })
- }
- case cfg.localPSKIdentityHint != nil ||
- state.CipherSuite.KeyExchangeAlgorithm().Has(CipherSuiteKeyExchangeAlgorithmEcdhe):
- // To help the client in selecting which identity to use, the server
- // can provide a "PSK identity hint" in the ServerKeyExchange message.
- // If no hint is provided and cipher suite doesn't use elliptic curve,
- // the ServerKeyExchange message is omitted.
- //
- // https://tools.ietf.org/html/rfc4279#section-2
- srvExchange := &handshake.MessageServerKeyExchange{
- IdentityHint: cfg.localPSKIdentityHint,
- }
- if state.CipherSuite.KeyExchangeAlgorithm().Has(CipherSuiteKeyExchangeAlgorithmEcdhe) {
- srvExchange.EllipticCurveType = elliptic.CurveTypeNamedCurve
- srvExchange.NamedCurve = state.NamedCurve
- srvExchange.PublicKey = state.LocalKeypair.PublicKey
- }
- pkts = append(pkts, &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: srvExchange,
- },
- },
- })
- }
-
- pkts = append(pkts, &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageServerHelloDone{},
- },
- },
- })
-
- return pkts, nil, nil
-}
diff --git a/flight5bhandler.go b/flight5bhandler.go
deleted file mode 100644
index e9cd0d92a..000000000
--- a/flight5bhandler.go
+++ /dev/null
@@ -1,90 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
-
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/prf"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
-)
-
-func flight5bParse(
- _ context.Context,
- _ flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) (flightVal, *alert.Alert, error) {
- _, msgs, ok := cache.fullPullMap(state.HandshakeRecvSequence-1, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
- )
- if !ok {
- // No valid message received. Keep reading
- return 0, nil, nil
- }
-
- if _, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
-
- // Other party may re-transmit the last flight. Keep state to be flight5b.
- return flight5b, nil, nil
-}
-
-func flight5bGenerate(
- _ flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) ([]*packet, *alert.Alert, error) { //nolint:gocognit
- var pkts []*packet
-
- pkts = append(pkts,
- &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &protocol.ChangeCipherSpec{},
- },
- })
-
- if len(state.LocalVerifyData) == 0 {
- plainText := cache.pullAndMerge(
- handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
- )
-
- var err error
- state.LocalVerifyData, err = prf.VerifyDataClient(state.MasterSecret, plainText, state.CipherSuite.HashFunc())
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- }
-
- pkts = append(pkts,
- &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- Epoch: 1,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageFinished{
- VerifyData: state.LocalVerifyData,
- },
- },
- },
- shouldEncrypt: true,
- resetLocalSequenceNumber: true,
- })
-
- return pkts, nil, nil
-}
diff --git a/flight5handler.go b/flight5handler.go
deleted file mode 100644
index 181f25856..000000000
--- a/flight5handler.go
+++ /dev/null
@@ -1,412 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "bytes"
- "context"
- "crypto"
- "crypto/x509"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/prf"
- "github.com/pion/dtls/v3/pkg/crypto/signaturehash"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
-)
-
-func flight5Parse(
- _ context.Context,
- conn flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) (flightVal, *alert.Alert, error) {
- _, msgs, ok := cache.fullPullMap(state.HandshakeRecvSequence, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
- )
- if !ok {
- // No valid message received. Keep reading
- return 0, nil, nil
- }
-
- var finished *handshake.MessageFinished
- if finished, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
- plainText := cache.pullAndMerge(
- handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeClientKeyExchange, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeCertificateVerify, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
- )
-
- expectedVerifyData, err := prf.VerifyDataServer(state.MasterSecret, plainText, state.CipherSuite.HashFunc())
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- if !bytes.Equal(expectedVerifyData, finished.VerifyData) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, dtlserrors.ErrVerifyDataMismatch
- }
-
- if len(state.SessionID) > 0 {
- s := Session{
- ID: state.SessionID,
- Secret: state.MasterSecret,
- }
- cfg.log.Tracef("[handshake] save new session: %x", s.ID)
- if err := cfg.sessionStore.Set(conn.sessionKey(), s); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- }
-
- return flight5, nil, nil
-}
-
-//nolint:gocognit,cyclop,maintidx
-func flight5Generate(
- conn flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) ([]*packet, *alert.Alert, error) {
- var signer crypto.Signer
- var pkts []*packet
- if state.RemoteRequestedCertificate { //nolint:nestif
- _, msgs, ok := cache.fullPullMap(state.HandshakeRecvSequence-2, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, false})
- if !ok {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, dtlserrors.ErrClientCertificateRequired //nolint:lll
- }
- reqInfo := CertificateRequestInfo{}
- if r, ok2 := msgs[handshake.TypeCertificateRequest].(*handshake.MessageCertificateRequest); ok2 {
- reqInfo.AcceptableCAs = r.CertificateAuthoritiesNames
- } else {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, dtlserrors.ErrClientCertificateRequired //nolint:lll
- }
- certificate, err := cfg.getClientCertificate(&reqInfo)
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, err
- }
- if certificate == nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, dtlserrors.ErrNotAcceptableCertificateChain //nolint:lll
- }
- if certificate.Certificate != nil {
- signer, ok = certificate.PrivateKey.(crypto.Signer)
- if !ok {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, dtlserrors.ErrInvalidPrivateKey
- }
- }
- pkts = append(pkts,
- &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageCertificate{
- Certificate: certificate.Certificate,
- },
- },
- },
- })
- }
-
- clientKeyExchange := &handshake.MessageClientKeyExchange{}
- if cfg.localPSKCallback == nil {
- clientKeyExchange.PublicKey = state.LocalKeypair.PublicKey
- } else {
- clientKeyExchange.IdentityHint = cfg.localPSKIdentityHint
- }
- if state != nil && state.LocalKeypair != nil && len(state.LocalKeypair.PublicKey) > 0 {
- clientKeyExchange.PublicKey = state.LocalKeypair.PublicKey
- }
-
- pkts = append(pkts,
- &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: clientKeyExchange,
- },
- },
- })
-
- serverKeyExchangeData := cache.pullAndMerge(
- handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
- )
-
- serverKeyExchange := &handshake.MessageServerKeyExchange{}
-
- // handshakeMessageServerKeyExchange is optional for PSK
- if len(serverKeyExchangeData) == 0 {
- alertPtr, err := handleServerKeyExchange(conn, state, cfg, &handshake.MessageServerKeyExchange{})
- if err != nil {
- return nil, alertPtr, err
- }
- } else {
- rawHandshake := &handshake.Handshake{
- KeyExchangeAlgorithm: state.CipherSuite.KeyExchangeAlgorithm(),
- }
- err := rawHandshake.Unmarshal(serverKeyExchangeData)
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.UnexpectedMessage}, err
- }
-
- switch h := rawHandshake.Message.(type) {
- case *handshake.MessageServerKeyExchange:
- serverKeyExchange = h
- default:
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.UnexpectedMessage}, dtlserrors.ErrInvalidContentType
- }
- }
-
- // Append not-yet-sent packets
- merged := []byte{}
- seqPred := uint16(state.HandshakeSendSequence) //nolint:gosec // G115
- for _, p := range pkts {
- h, ok := p.record.Content.(*handshake.Handshake)
- if !ok {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, dtlserrors.ErrInvalidContentType
- }
- h.Header.MessageSequence = seqPred
- seqPred++
- raw, err := h.Marshal()
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- merged = append(merged, raw...)
- }
-
- if alertPtr, err := initializeCipherSuite(state, cache, cfg, serverKeyExchange, merged); err != nil {
- return nil, alertPtr, err
- }
-
- // If the client has sent a certificate with signing ability, a digitally-signed
- // CertificateVerify message is sent to explicitly verify possession of the
- // private key in the certificate.
- if state.RemoteRequestedCertificate && signer != nil {
- plainText := append(cache.pullAndMerge(
- handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeClientKeyExchange, cfg.initialEpoch, true, false},
- ), merged...)
-
- // Find compatible signature scheme
-
- signatureHashAlgo, err := signaturehash.SelectSignatureScheme(state.RemoteCertRequestAlgs, signer)
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, err
- }
-
- certVerify, err := generateCertificateVerify(plainText, signer, signatureHashAlgo.Hash, signatureHashAlgo.Signature)
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- state.LocalCertificatesVerify = certVerify
-
- pkt := &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageCertificateVerify{
- HashAlgorithm: signatureHashAlgo.Hash,
- SignatureAlgorithm: signatureHashAlgo.Signature,
- Signature: state.LocalCertificatesVerify,
- },
- },
- },
- }
- pkts = append(pkts, pkt)
-
- h, ok := pkt.record.Content.(*handshake.Handshake)
- if !ok {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, dtlserrors.ErrInvalidContentType
- }
- h.Header.MessageSequence = seqPred
- // seqPred++ // this is the last use of seqPred
- raw, err := h.Marshal()
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- merged = append(merged, raw...)
- }
-
- pkts = append(pkts,
- &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &protocol.ChangeCipherSpec{},
- },
- })
-
- if len(state.LocalVerifyData) == 0 {
- plainText := cache.pullAndMerge(
- handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeClientKeyExchange, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeCertificateVerify, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
- )
-
- var err error
- state.LocalVerifyData, err = prf.VerifyDataClient(
- state.MasterSecret,
- append(plainText, merged...),
- state.CipherSuite.HashFunc(),
- )
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- }
-
- pkts = append(pkts,
- &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- Epoch: 1,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageFinished{
- VerifyData: state.LocalVerifyData,
- },
- },
- },
- shouldWrapCID: len(state.RemoteConnectionID) > 0,
- shouldEncrypt: true,
- resetLocalSequenceNumber: true,
- })
-
- return pkts, nil, nil
-}
-
-//nolint:gocognit,cyclop
-func initializeCipherSuite(
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
- handshakeKeyExchange *handshake.MessageServerKeyExchange,
- sendingPlainText []byte,
-) (*alert.Alert, error) {
- if state.CipherSuite.IsInitialized() {
- return nil, nil //nolint
- }
-
- clientRandom := state.LocalRandom.MarshalFixed()
- serverRandom := state.RemoteRandom.MarshalFixed()
-
- var err error
-
- if state.ExtendedMasterSecret {
- var sessionHash []byte
- sessionHash, err = cache.sessionHash(state.CipherSuite.HashFunc(), cfg.initialEpoch, sendingPlainText)
- if err != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
-
- state.MasterSecret, err = prf.ExtendedMasterSecret(state.PreMasterSecret, sessionHash, state.CipherSuite.HashFunc())
- if err != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, err
- }
- } else {
- state.MasterSecret, err = prf.MasterSecret(
- state.PreMasterSecret,
- clientRandom[:],
- serverRandom[:],
- state.CipherSuite.HashFunc(),
- )
- if err != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- }
-
- if state.CipherSuite.AuthenticationType() == CipherSuiteAuthenticationTypeCertificate { //nolint:nestif
- // Verify that the pair of hash algorithm and signiture is listed.
- var validSignatureScheme bool
- for _, ss := range cfg.localSignatureSchemes {
- if ss.Hash == handshakeKeyExchange.HashAlgorithm && ss.Signature == handshakeKeyExchange.SignatureAlgorithm {
- validSignatureScheme = true
-
- break
- }
- }
- if !validSignatureScheme {
- return &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrNoAvailableSignatureSchemes //nolint:lll
- }
-
- expectedMsg := valueKeyMessage(
- clientRandom[:],
- serverRandom[:],
- handshakeKeyExchange.PublicKey,
- handshakeKeyExchange.NamedCurve,
- )
- if err = verifyKeySignature(
- expectedMsg,
- handshakeKeyExchange.Signature,
- handshakeKeyExchange.HashAlgorithm,
- handshakeKeyExchange.SignatureAlgorithm,
- state.PeerCertificates,
- ); err != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
- }
- var chains [][]*x509.Certificate
- if !cfg.insecureSkipVerify {
- certAlgs := cfg.localCertSignatureSchemes
- if len(certAlgs) == 0 {
- certAlgs = cfg.localSignatureSchemes
- }
- if chains, err = verifyServerCert(state.PeerCertificates, cfg.rootCAs, cfg.serverName, certAlgs); err != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
- }
- }
- if cfg.verifyPeerCertificate != nil {
- if err = cfg.verifyPeerCertificate(state.PeerCertificates, chains); err != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
- }
- }
- }
- if cfg.verifyConnection != nil {
- stateSnapshot, errC := generateState(state)
- if errC != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, errC
- }
- if errC = cfg.verifyConnection(stateSnapshot); errC != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, errC
- }
- }
-
- if err = state.CipherSuite.Init(state.MasterSecret, clientRandom[:], serverRandom[:], true); err != nil {
- return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
-
- cfg.writeKeyLog(keyLogLabelTLS12, clientRandom[:], state.MasterSecret)
-
- return nil, nil //nolint
-}
diff --git a/flight6handler.go b/flight6handler.go
deleted file mode 100644
index 5f0ee6388..000000000
--- a/flight6handler.go
+++ /dev/null
@@ -1,99 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
-
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/prf"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
-)
-
-func flight6Parse(
- _ context.Context,
- _ flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) (flightVal, *alert.Alert, error) {
- _, msgs, ok := cache.fullPullMap(state.HandshakeRecvSequence-1, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
- )
- if !ok {
- // No valid message received. Keep reading
- return 0, nil, nil
- }
-
- if _, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
-
- // Other party may re-transmit the last flight. Keep state to be flight6.
- return flight6, nil, nil
-}
-
-func flight6Generate(
- _ flightConn,
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
-) ([]*packet, *alert.Alert, error) {
- var pkts []*packet
-
- pkts = append(pkts,
- &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &protocol.ChangeCipherSpec{},
- },
- })
-
- if len(state.LocalVerifyData) == 0 {
- plainText := cache.pullAndMerge(
- handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeClientKeyExchange, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeCertificateVerify, cfg.initialEpoch, true, false},
- handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
- )
-
- var err error
- state.LocalVerifyData, err = prf.VerifyDataServer(state.MasterSecret, plainText, state.CipherSuite.HashFunc())
- if err != nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- }
-
- pkts = append(pkts,
- &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- Epoch: 1,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageFinished{
- VerifyData: state.LocalVerifyData,
- },
- },
- },
- shouldWrapCID: len(state.RemoteConnectionID) > 0,
- shouldEncrypt: true,
- resetLocalSequenceNumber: true,
- },
- )
-
- return pkts, nil, nil
-}
diff --git a/flight_13.go b/flight_13.go
deleted file mode 100644
index 396a28f10..000000000
--- a/flight_13.go
+++ /dev/null
@@ -1,194 +0,0 @@
-// SPDX-FileCopyrightText: 2023 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-type flightVal13 uint8
-
-/*
-// [RFC9147 Section-5.7]
-
-Full DTLS Handshake (with Cookie Exchange):
-
-Client Server
-
- +----------+
- Waiting | Flight 0 |
- +----------+
-
- +----------+
- ClientHello | Flight 1 |
- --------> +----------+
-
- +----------+
- <-------- HelloRetryRequest | Flight 2 |
- + cookie +----------+
-
-
- +----------+
-ClientHello | Flight 3 |
- + cookie --------> +----------+
-
-
-
- ServerHello
- {EncryptedExtensions} +----------+
- {CertificateRequest*} | Flight 4 |
- {Certificate*} +----------+
- {CertificateVerify*}
- {Finished}
- <-------- [Application Data*]
-
-
-
- {Certificate*} +----------+
- {CertificateVerify*} | Flight 5 |
- {Finished} --------> +----------+
- [Application Data]
- +----------+
- <-------- [ACK] | Flight 6 |
- [Application Data*] +----------+
-
- [Application Data] <-------> [Application Data]
-
-
-
-
-Resumption and PSK Handshake (without Cookie Exchange):
-
-Client Server
-
- ClientHello +-----------+
- + pre_shared_key | Flight 3a |
- + psk_key_exchange_modes +-----------+
- + key_share* -------->
-
-
- ServerHello
- + pre_shared_key +-----------+
- + key_share* | Flight 4a |
- {EncryptedExtensions} +-----------+
- <-------- {Finished}
- [Application Data*]
- +-----------+
- {Finished} --------> | Flight 5a |
- [Application Data*] +-----------+
-
- +-----------+
- <-------- [ACK] | Flight 6a |
- [Application Data*] +-----------+
-
- [Application Data] <-------> [Application Data]
-
-
-Zero-RTT Handshake:
-
-Client Server
-
- ClientHello
- + early_data
- + psk_key_exchange_modes +-----------+
- + key_share* | Flight 3b |
- + pre_shared_key +-----------+
- (Application Data*) -------->
-
- ServerHello
- + pre_shared_key
- + key_share* +-----------+
- {EncryptedExtensions} | Flight 4b |
- {Finished} +-----------+
- <-------- [Application Data*]
-
-
- +-----------+
- {Finished} --------> | Flight 5b |
- [Application Data*] +-----------+
-
- +-----------+
- <-------- [ACK] | Flight 6b |
- [Application Data*] +-----------+
-
- [Application Data] <-------> [Application Data]
-
-
-NewSessionTicket Message:
-
-Client Server
-
- +-----------+
- <-------- [NewSessionTicket] | Flight 4c |
- +-----------+
-
- +-----------+
-[ACK] --------> | Flight 5c |
- +-----------+
-*/
-
-const (
- flight13_0 flightVal13 = iota + 1
- flight13_1
- flight13_2
- flight13_3
- flight13_3a
- flight13_3b
- flight13_4
- flight13_4a
- flight13_4b
- flight13_4c
- flight13_5
- flight13_5a
- flight13_5b
- flight13_5c
- flight13_6
- flight13_6a
- flight13_6b
-)
-
-func (f flightVal13) String() string { //nolint:cyclop
- switch f {
- case flight13_0:
- return "Flight13 0"
- case flight13_1:
- return "Flight13 1"
- case flight13_2:
- return "Flight13 2"
- case flight13_3:
- return "Flight13 3"
- case flight13_3a:
- return "Flight13 3a"
- case flight13_3b:
- return "Flight13 3b"
- case flight13_4:
- return "Flight13 4"
- case flight13_4a:
- return "Flight13 4a"
- case flight13_4b:
- return "Flight13 4b"
- case flight13_4c:
- return "Flight13 4c"
- case flight13_5:
- return "Flight13 5"
- case flight13_5a:
- return "Flight13 5a"
- case flight13_5b:
- return "Flight13 5b"
- case flight13_5c:
- return "Flight13 5c"
- case flight13_6:
- return "Flight13 6"
- case flight13_6a:
- return "Flight13 6a"
- case flight13_6b:
- return "Flight13 6b"
- default:
- return "Invalid Flight"
- }
-}
-
-func (f flightVal13) isLastSendFlight() bool { // nolint: unused
- return f == flight13_6 || f == flight13_6a || f == flight13_6b || f == flight13_5c
-}
-
-func (f flightVal13) isLastRecvFlight() bool { // nolint: unused
- return f == flight13_5 || f == flight13_5a || f == flight13_5b || f == flight13_4c
-}
diff --git a/flight_test.go b/flight_test.go
new file mode 100644
index 000000000..1225c005b
--- /dev/null
+++ b/flight_test.go
@@ -0,0 +1,3167 @@
+// SPDX-FileCopyrightText: 2026 The Pion community
+// SPDX-License-Identifier: MIT
+
+package dtls
+
+import (
+ "bytes"
+ "context"
+ "crypto"
+ "crypto/hmac"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha256"
+ "crypto/tls"
+ "crypto/x509"
+ "errors"
+ "hash"
+ "slices"
+ "testing"
+ "time"
+
+ "github.com/pion/dtls/v4/internal/ciphersuite"
+ dtlsconfig "github.com/pion/dtls/v4/internal/config"
+ dtlserrors "github.com/pion/dtls/v4/internal/errors"
+ dtlsflight "github.com/pion/dtls/v4/internal/flight"
+ dtlsflight13 "github.com/pion/dtls/v4/internal/flight/flight13"
+ dtlsfragmentbuffer "github.com/pion/dtls/v4/internal/fragmentbuffer"
+ dtlshandshake "github.com/pion/dtls/v4/internal/handshake"
+ dtlscrypto "github.com/pion/dtls/v4/internal/handshakecrypto"
+ "github.com/pion/dtls/v4/internal/negotiation"
+ dtlsstate "github.com/pion/dtls/v4/internal/state"
+ "github.com/pion/dtls/v4/internal/util"
+ cryptosuite "github.com/pion/dtls/v4/pkg/crypto/ciphersuite"
+ "github.com/pion/dtls/v4/pkg/crypto/elliptic"
+ dtlshash "github.com/pion/dtls/v4/pkg/crypto/hash"
+ "github.com/pion/dtls/v4/pkg/crypto/keyschedule"
+ "github.com/pion/dtls/v4/pkg/crypto/prf"
+ "github.com/pion/dtls/v4/pkg/crypto/selfsign"
+ "github.com/pion/dtls/v4/pkg/crypto/signature"
+ "github.com/pion/dtls/v4/pkg/crypto/signaturehash"
+ "github.com/pion/dtls/v4/pkg/protocol"
+ "github.com/pion/dtls/v4/pkg/protocol/alert"
+ "github.com/pion/dtls/v4/pkg/protocol/extension"
+ extension13 "github.com/pion/dtls/v4/pkg/protocol/extension/dtls13"
+ "github.com/pion/dtls/v4/pkg/protocol/handshake"
+ "github.com/pion/logging"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func testTrafficProtectionInitialized(state *dtlsstate.State13, epoch uint64) bool {
+ if state == nil || state.TrafficKeys == nil {
+ return false
+ }
+ write, hasWrite := state.TrafficKeys.Write(epoch)
+ read, hasRead := state.TrafficKeys.Read(epoch)
+
+ return hasWrite && write.Protection != nil && hasRead && read.Protection != nil
+}
+
+const tlsHandshakeHeaderLength13 = 4
+
+var (
+ //nolint:gochecknoglobals
+ testCurves13 = []elliptic.Curve{elliptic.X25519, elliptic.P256, elliptic.P384}
+ errFlight13ConnectionCallbackRejected = errors.New("connection callback rejected")
+ errFlight13ServerIdentityCallbackReject = errors.New("identity callback rejected")
+)
+
+type handshakeTestContext13 struct {
+ state *dtlsstate.State13
+ cache *dtlsflight.Cache
+ cfg *dtlsconfig.HandshakeConfig
+ transcript *dtlshandshake.Transcript
+}
+
+func newTestState13(tb testing.TB, isClient bool) *dtlsstate.State13 {
+ tb.Helper()
+
+ state := dtlsstate.NewState13(isClient)
+ _, snapshot, err := negotiation.FinalizeClientHello(&handshake.MessageClientHello{
+ CipherSuiteIDs: []uint16{uint16(cryptosuite.TLS_AES_128_GCM_SHA256)},
+ Extensions: []extension.Value{&extension13.OfferedVersions{Versions: []protocol.Version{protocol.Version1_3}}, &extension.SignatureAlgorithms{Schemes: []uint16{0x0403}}, &extension.SupportedGroups{Groups: slices.Clone(testCurves13)}, &extension13.ClientKeyShare{}},
+ }, nil)
+ require.NoError(tb, err)
+ require.NoError(tb, state.LocalClientHelloSnapshots.Record(snapshot))
+ require.NoError(tb, state.RemoteClientHelloSnapshots.Record(snapshot))
+
+ return &state
+}
+
+func omitInitialKeyShares() func(handshake.MessageClientHello) handshake.Message {
+ initial := true
+
+ return func(clientHello handshake.MessageClientHello) handshake.Message {
+ if initial {
+ initial = false
+ for i, value := range clientHello.Extensions {
+ if value.ExtensionType() == extension.TypeKeyShare {
+ extensions := clientHello.Extensions
+ extensions[i] = &extension13.ClientKeyShare{}
+ clientHello.Extensions = extensions
+ }
+ }
+ }
+
+ return &clientHello
+ }
+}
+
+func flight13ParseForTest(testingT require.TestingT, flight dtlsflight13.Flight, ctx context.Context, flightCtx *handshakeTestContext13) (dtlsflight13.Flight, *alert.Alert, error) {
+ return flight13ParseForTestWithConn(testingT, flight, ctx, nil, flightCtx)
+}
+
+func flight13ParseForTestWithConn(testingT require.TestingT, flight dtlsflight13.Flight, ctx context.Context, conn dtlsflight.Conn, flightCtx *handshakeTestContext13) (dtlsflight13.Flight, *alert.Alert, error) {
+ if helper, ok := testingT.(interface{ Helper() }); ok {
+ helper.Helper()
+ }
+
+ nextFlight, dtlsAlert, err, ok := dtlsflight13.Parse(
+ ctx,
+ flight,
+ conn,
+ dtlsflight13.ParseDependencies{
+ State: flightCtx.state,
+ Cache: flightCtx.cache,
+ Config: flightCtx.cfg,
+ Hooks: dtlsflight13.ParseHooks{
+ InboundHandshake: func(cipherSuite dtlsconfig.CipherSuite, items []dtlsflight.DecodedHandshakeCacheItem) error {
+ return dtlshandshake.AppendVerifiedInboundHandshakeCacheItems(flightCtx.transcript, cipherSuite, items)
+ },
+ ProtectedHandshake: func(cipherSuite dtlsconfig.CipherSuite, items []dtlsflight.DecodedHandshakeCacheItem) error {
+ return dtlshandshake.VerifyAndAppendProtectedHandshakeCacheItems(flightCtx.transcript, flightCtx.state, flightCtx.cfg, cipherSuite, items)
+ },
+ HandshakeTrafficSecretDeriver: func(state *dtlsstate.State13) error {
+ return dtlshandshake.DeriveAndStoreHandshakeTrafficSecrets(state, flightCtx.transcript, nil)
+ },
+ HandshakeRecordProtectionInitializer: dtlshandshake.InitHandshakeRecordProtection,
+ },
+ },
+ )
+ require.True(testingT, ok)
+
+ return nextFlight, dtlsAlert, err
+}
+
+type flight13QueuedPacketConn struct {
+ handleQueuedPackets func(context.Context) error
+}
+
+func (c *flight13QueuedPacketConn) HandleQueuedPackets(ctx context.Context) error {
+ if c.handleQueuedPackets == nil {
+ return nil
+ }
+
+ return c.handleQueuedPackets(ctx)
+}
+
+func (c *flight13QueuedPacketConn) SessionKey() []byte {
+ return nil
+}
+
+func flight13GenerateForTest(testingT require.TestingT, flight dtlsflight13.Flight, flightCtx *handshakeTestContext13) ([]*dtlsflight.Outbound, *alert.Alert, error) {
+ if helper, ok := testingT.(interface{ Helper() }); ok {
+ helper.Helper()
+ }
+
+ gen, _, ok := dtlsflight13.GetGenerator(flight)
+ require.True(testingT, ok)
+
+ transcript := flightCtx.transcript
+ if transcript == nil {
+ transcript = dtlshandshake.NewTranscript()
+ }
+
+ return gen(nil, flightCtx.state, flightCtx.cache, flightCtx.cfg, transcript.FinalizeClientHello)
+}
+
+func retryRequestForTest(tb testing.TB, state *dtlsstate.State13, cfg *dtlsconfig.HandshakeConfig, initial negotiation.ClientHelloSnapshot, group elliptic.Curve) negotiation.RetryRequest {
+ tb.Helper()
+ id := uint16(cfg.LocalCipherSuites[0].ID())
+ extensions := []extension.Value{&extension13.SelectedVersion{Version: protocol.Version1_3}}
+ if group != 0 {
+ extensions = append(extensions, &extension13.RetryKeyShare{SelectedGroup: group})
+ }
+ if state.Cookie != nil {
+ extensions = append(extensions, &extension13.Cookie{Cookie: state.Cookie})
+ }
+ request, err := negotiation.ValidateHelloRetryRequest(initial, &handshake.MessageServerHello{CipherSuiteID: &id, Extensions: extensions})
+ require.NoError(tb, err)
+
+ return request
+}
+
+func canonicalPacketHandshake13(t *testing.T, p *dtlsflight.Outbound) []byte {
+ t.Helper()
+
+ content, ok := p.Content.(*handshake.Handshake)
+ require.True(t, ok)
+ raw, err := content.Marshal()
+ require.NoError(t, err)
+ canonical, err := canonicalHandshake13(raw)
+ require.NoError(t, err)
+
+ return canonical
+}
+
+func canonicalTranscriptHandshake13(typ handshake.Type, body []byte) []byte {
+ out := make([]byte, tlsHandshakeHeaderLength13+len(body))
+ out[0] = byte(typ)
+ util.PutBigEndianUint24(out[1:], uint32(len(body))) //nolint:gosec // G115
+ copy(out[tlsHandshakeHeaderLength13:], body)
+
+ return out
+}
+
+func canonicalHandshake13(raw []byte) ([]byte, error) {
+ if len(raw) < handshake.HeaderLength {
+ return nil, dtlserrors.ErrBufferTooSmall
+ }
+
+ var header handshake.Header
+ if err := header.Unmarshal(raw); err != nil {
+ return nil, err
+ }
+ if header.FragmentOffset != 0 || header.FragmentLength != header.Length || len(raw) != handshake.HeaderLength+int(header.Length) {
+ return nil, dtlserrors.ErrInvalidHandshakeTranscriptMessage
+ }
+
+ out := make([]byte, tlsHandshakeHeaderLength13+int(header.Length))
+ copy(out[:tlsHandshakeHeaderLength13], raw[:tlsHandshakeHeaderLength13])
+ copy(out[tlsHandshakeHeaderLength13:], raw[handshake.HeaderLength:])
+
+ return out, nil
+}
+
+func hashTranscript13(messages ...[]byte) []byte {
+ hash := sha256.New()
+ for _, message := range messages {
+ _, _ = hash.Write(message)
+ }
+
+ return hash.Sum(nil)
+}
+
+func deriveHandshakeTrafficSecrets13(hashFunc func() hash.Hash, psk, keyAgreementSecret, transcriptHash []byte) (dtlsstate.TrafficSecrets, error) {
+ hashSize := hashFunc().Size()
+ if len(psk) == 0 {
+ psk = make([]byte, hashSize)
+ }
+ earlySecret, err := keyschedule.HkdfExtract(hashFunc, nil, psk)
+ if err != nil {
+ return dtlsstate.TrafficSecrets{}, err
+ }
+
+ derivedSecret, err := keyschedule.DeriveSecret(hashFunc, earlySecret, "derived", nil)
+ if err != nil {
+ return dtlsstate.TrafficSecrets{}, err
+ }
+
+ handshakeSecret, err := keyschedule.HkdfExtract(hashFunc, derivedSecret, keyAgreementSecret)
+ if err != nil {
+ return dtlsstate.TrafficSecrets{}, err
+ }
+
+ clientSecret, err := keyschedule.HkdfExpandLabel(hashFunc, handshakeSecret, "c hs traffic", transcriptHash, hashSize)
+ if err != nil {
+ return dtlsstate.TrafficSecrets{}, err
+ }
+ serverSecret, err := keyschedule.HkdfExpandLabel(hashFunc, handshakeSecret, "s hs traffic", transcriptHash, hashSize)
+ if err != nil {
+ return dtlsstate.TrafficSecrets{}, err
+ }
+
+ return dtlsstate.TrafficSecrets{Client: clientSecret, Server: serverSecret}, nil
+}
+
+func finishedVerifyData13(
+ t *testing.T,
+ hashFunc func() hash.Hash,
+ baseKey, transcriptHash []byte,
+) []byte {
+ t.Helper()
+
+ finishedKey, err := keyschedule.HkdfExpandLabel(hashFunc, baseKey, "finished", nil, hashFunc().Size())
+ require.NoError(t, err)
+
+ mac := hmac.New(hashFunc, finishedKey)
+ _, err = mac.Write(transcriptHash)
+ require.NoError(t, err)
+
+ return mac.Sum(nil)
+}
+
+func marshalFinished13(t *testing.T, seq uint16, verifyData []byte) []byte {
+ t.Helper()
+
+ raw, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: seq}, Message: &handshake.MessageFinished{VerifyData: verifyData}}).Marshal()
+ require.NoError(t, err)
+
+ return raw
+}
+
+func marshalServerFinished13(
+ t *testing.T,
+ state *dtlsstate.State13,
+ seq uint16,
+ transcriptMessages ...[]byte,
+) []byte {
+ t.Helper()
+
+ verifyData := finishedVerifyData13(t, state.CipherSuite.HashFunc(), state.KeySchedule.HandshakeTraffic.Server, hashTranscript13(transcriptMessages...))
+
+ return marshalFinished13(t, seq, verifyData)
+}
+
+type flight13ProtectedServerFlightFixture struct {
+ cfg *dtlsconfig.HandshakeConfig
+ state *dtlsstate.State13
+ transcript *dtlshandshake.Transcript
+ clientHelloCanonical []byte
+ serverHelloCanonical []byte
+ encryptedExtensionsCanonical []byte
+ rawServerHello []byte
+ rawEncryptedExtensions []byte
+ rawFinished []byte
+ handshakeSecrets dtlsstate.TrafficSecrets
+}
+
+func newFlight13ProtectedServerFlightFixture(t *testing.T, psk bool) flight13ProtectedServerFlightFixture {
+ t.Helper()
+
+ cfg := testHandshakeConfig13(t)
+ if psk {
+ configureTestPSK13(cfg)
+ }
+ state := newTestState13(t, true)
+ transcript := dtlshandshake.NewTranscript()
+ clientHello, _, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+ appended, err := dtlshandshake.AppendClientHelloInitialFlights(transcript, clientHello)
+ require.NoError(t, err)
+ require.True(t, appended)
+ clientHelloCanonical := canonicalPacketHandshake13(t, clientHello[0])
+
+ return newFlight13ProtectedServerFlightFixtureFromClientHello(t, cfg, state, transcript, clientHelloCanonical)
+}
+
+func newFlight13ProtectedServerFlightFixtureFromClientHello(t *testing.T, cfg *dtlsconfig.HandshakeConfig, state *dtlsstate.State13, transcript *dtlshandshake.Transcript, clientHelloCanonical []byte) flight13ProtectedServerFlightFixture {
+ t.Helper()
+
+ group := cfg.EllipticCurves[0]
+ serverKeypair, err := elliptic.GenerateKeypair(group)
+ require.NoError(t, err)
+ rawServerHello := marshalServerHello(t, cfg, handshake.Random{
+ RandomBytes: [handshake.RandomBytesLength]byte{0x01},
+ }, []extension.Value{
+ &extension13.SelectedVersion{Version: protocol.Version1_3},
+ &extension13.ServerKeyShare{Share: extension13.KeyShareEntry{Group: group, KeyExchange: serverKeypair.PublicKey}},
+ })
+ serverHelloCanonical, err := canonicalHandshake13(rawServerHello)
+ require.NoError(t, err)
+
+ rawEncryptedExtensions, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: 1}, Message: &handshake.MessageEncryptedExtensions{}}).Marshal()
+ require.NoError(t, err)
+ encryptedExtensionsCanonical, err := canonicalHandshake13(rawEncryptedExtensions)
+ require.NoError(t, err)
+
+ clientKeypair := state.LocalKeypairs[group]
+ require.NotNil(t, clientKeypair)
+ keyAgreementSecret, err := prf.PreMasterSecret(clientKeypair.PublicKey, serverKeypair.PrivateKey, group)
+ require.NoError(t, err)
+ handshakeSecrets, err := deriveHandshakeTrafficSecrets13(cfg.LocalCipherSuites[0].HashFunc(), firstPSKSecret13(state), keyAgreementSecret, hashTranscript13(clientHelloCanonical, serverHelloCanonical))
+ require.NoError(t, err)
+ state.CipherSuite = cfg.LocalCipherSuites[0]
+ state.KeySchedule.HandshakeTraffic = handshakeSecrets
+ rawFinished := marshalServerFinished13(t, state, 2, clientHelloCanonical, serverHelloCanonical, encryptedExtensionsCanonical)
+ state.CipherSuite = nil
+ state.KeySchedule.HandshakeTraffic = dtlsstate.TrafficSecrets{}
+
+ return flight13ProtectedServerFlightFixture{
+ cfg: cfg,
+ state: state,
+ transcript: transcript,
+ clientHelloCanonical: clientHelloCanonical,
+ serverHelloCanonical: serverHelloCanonical,
+ encryptedExtensionsCanonical: encryptedExtensionsCanonical,
+ rawServerHello: rawServerHello,
+ rawEncryptedExtensions: rawEncryptedExtensions,
+ rawFinished: rawFinished,
+ handshakeSecrets: handshakeSecrets,
+ }
+}
+
+func (f flight13ProtectedServerFlightFixture) cacheWithFinished(rawFinished []byte) *dtlsflight.Cache {
+ cache := dtlsflight.NewCache()
+ cache.Push(f.rawServerHello, f.cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+ cache.Push(f.rawEncryptedExtensions, dtlsflight13.EpochHandshake, 1, handshake.TypeEncryptedExtensions, false)
+ cache.Push(rawFinished, dtlsflight13.EpochHandshake, 2, handshake.TypeFinished, false)
+
+ return cache
+}
+
+type flight13ProtectedServerCertificateFlight struct {
+ cache *dtlsflight.Cache
+ certificateCanonical []byte
+ certificateVerifyCanonical []byte
+ finishedCanonical []byte
+}
+
+func (f flight13ProtectedServerFlightFixture) cacheWithCertificate(t *testing.T, certificate tls.Certificate) flight13ProtectedServerCertificateFlight {
+ t.Helper()
+
+ rawCertificate := marshalCertificate13(t, 2, certificate.Certificate)
+ certificateCanonical, err := canonicalHandshake13(rawCertificate)
+ require.NoError(t, err)
+
+ signer, ok := certificate.PrivateKey.(crypto.Signer)
+ require.True(t, ok)
+ rawCertificateVerify := marshalServerCertificateVerify13(t, 3, signer, f.clientHelloCanonical, f.serverHelloCanonical, f.encryptedExtensionsCanonical, certificateCanonical)
+ certificateVerifyCanonical, err := canonicalHandshake13(rawCertificateVerify)
+ require.NoError(t, err)
+
+ f.state.CipherSuite = f.cfg.LocalCipherSuites[0]
+ f.state.KeySchedule.HandshakeTraffic = f.handshakeSecrets
+ rawFinished := marshalServerFinished13(t, f.state, 4, f.clientHelloCanonical, f.serverHelloCanonical, f.encryptedExtensionsCanonical, certificateCanonical, certificateVerifyCanonical)
+ f.state.CipherSuite = nil
+ f.state.KeySchedule.HandshakeTraffic = dtlsstate.TrafficSecrets{}
+ finishedCanonical, err := canonicalHandshake13(rawFinished)
+ require.NoError(t, err)
+
+ cache := dtlsflight.NewCache()
+ cache.Push(f.rawServerHello, f.cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+ cache.Push(f.rawEncryptedExtensions, dtlsflight13.EpochHandshake, 1, handshake.TypeEncryptedExtensions, false)
+ cache.Push(rawCertificate, dtlsflight13.EpochHandshake, 2, handshake.TypeCertificate, false)
+ cache.Push(rawCertificateVerify, dtlsflight13.EpochHandshake, 3, handshake.TypeCertificateVerify, false)
+ cache.Push(rawFinished, dtlsflight13.EpochHandshake, 4, handshake.TypeFinished, false)
+
+ return flight13ProtectedServerCertificateFlight{cache: cache, certificateCanonical: certificateCanonical, certificateVerifyCanonical: certificateVerifyCanonical, finishedCanonical: finishedCanonical}
+}
+
+func marshalCertificate13(t *testing.T, seq uint16, rawCertificates [][]byte) []byte {
+ t.Helper()
+
+ entries := make([]handshake.CertificateEntry13, 0, len(rawCertificates))
+ for _, rawCertificate := range rawCertificates {
+ entries = append(entries, handshake.CertificateEntry13{
+ CertificateData: rawCertificate,
+ })
+ }
+
+ raw, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: seq}, Message: &handshake.MessageCertificate13{CertificateList: entries}}).Marshal()
+ require.NoError(t, err)
+
+ return raw
+}
+
+func marshalServerCertificateVerify13(t *testing.T, seq uint16, signer crypto.Signer, transcriptMessages ...[]byte) []byte {
+ t.Helper()
+
+ signatureBytes, err := dtlscrypto.GenerateCertificateVerify(serverCertificateVerifyInput13(t, transcriptMessages...), signer, dtlshash.SHA256, signature.ECDSA)
+ require.NoError(t, err)
+
+ raw, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: seq}, Message: &handshake.MessageCertificateVerify{HashAlgorithm: dtlshash.SHA256, SignatureAlgorithm: signature.ECDSA, Signature: signatureBytes}}).Marshal()
+ require.NoError(t, err)
+
+ return raw
+}
+
+func serverCertificateVerifyInput13(t *testing.T, transcriptMessages ...[]byte) []byte {
+ t.Helper()
+
+ transcriptHash := hashTranscript13(transcriptMessages...)
+ out := make([]byte, 64, 64+len("TLS 1.3, server CertificateVerify\x00")+len(transcriptHash))
+ for i := range out {
+ out[i] = 0x20
+ }
+ out = append(out, []byte("TLS 1.3, server CertificateVerify\x00")...)
+ out = append(out, transcriptHash...)
+
+ return out
+}
+
+func flight13RootCAsForCertificate(t *testing.T, certificate tls.Certificate) *x509.CertPool {
+ t.Helper()
+
+ leaf := certificate.Leaf
+ if leaf == nil {
+ var err error
+ leaf, err = x509.ParseCertificate(certificate.Certificate[0])
+ require.NoError(t, err)
+ }
+ pool := x509.NewCertPool()
+ pool.AddCert(leaf)
+
+ return pool
+}
+
+func testHandshakeConfig13(t *testing.T) *dtlsconfig.HandshakeConfig {
+ t.Helper()
+
+ cipherSuite := ciphersuite.ForID(cryptosuite.TLS_AES_128_GCM_SHA256)
+ require.NotNil(t, cipherSuite)
+
+ loggerFactory := logging.NewDefaultLoggerFactory()
+
+ return &dtlsconfig.HandshakeConfig{
+ LocalCipherSuites: []dtlsconfig.CipherSuite{cipherSuite},
+ EllipticCurves: testCurves13,
+ InitialRetransmitInterval: time.Second,
+ ExtendedMasterSecret: dtlsconfig.RequestExtendedMasterSecret,
+ Log: loggerFactory.NewLogger("dtls"),
+ MinVersion: protocol.Version1_3,
+ MaxVersion: protocol.Version1_3,
+ LocalSignatureSchemes: signaturehash.Algorithms(),
+ LocalCertSignatureSchemes: nil,
+ LocalSRTPProtectionProfiles: nil,
+ }
+}
+
+func TestFlight13_5GenerateSelectsClientCertificateBySignatureScheme(t *testing.T) {
+ rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
+ require.NoError(t, err)
+ rsaCertificate, err := selfsign.SelfSign(rsaKey)
+ require.NoError(t, err)
+ ecdsaCertificate, err := selfsign.GenerateSelfSigned()
+ require.NoError(t, err)
+
+ ecdsaSHA256 := signaturehash.Algorithm{
+ Hash: dtlshash.SHA256,
+ Signature: signature.ECDSA,
+ }
+ request := &handshake.MessageCertificateRequest13{CertificateRequestContext: []byte("request"), Extensions: []extension.Value{&extension.SignatureAlgorithms{Schemes: dtlsflight.SignatureSchemeIDs([]signaturehash.Algorithm{ecdsaSHA256})}}}
+ cfg := testHandshakeConfig13(t)
+ cfg.LocalCertificates = []tls.Certificate{rsaCertificate, ecdsaCertificate}
+ state := newTestState13(t, true)
+ state.RemoteCertificateRequest = request
+
+ packets, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight5, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, packets, 3)
+
+ certificateHandshake, ok := packets[0].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ certificateMessage, ok := certificateHandshake.Message.(*handshake.MessageCertificate13)
+ require.True(t, ok)
+ require.Len(t, certificateMessage.CertificateList, 1)
+ assert.Equal(t, ecdsaCertificate.Certificate[0], certificateMessage.CertificateList[0].CertificateData)
+
+ verifyHandshake, ok := packets[1].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ verifyMessage, ok := verifyHandshake.Message.(*handshake.MessageCertificateVerify)
+ require.True(t, ok)
+ assert.Equal(t, ecdsaSHA256.Hash, verifyMessage.HashAlgorithm)
+ assert.Equal(t, ecdsaSHA256.Signature, verifyMessage.SignatureAlgorithm)
+ assert.Same(t, ecdsaCertificate.PrivateKey, packets[1].CertificateVerifySigner)
+}
+
+func marshalHelloRetryRequestServerHello(t *testing.T, cfg *dtlsconfig.HandshakeConfig, extensions []extension.Value) []byte {
+ t.Helper()
+
+ var hrrRandomFixed [handshake.RandomLength]byte
+ copy(hrrRandomFixed[:], handshake.HelloRetryRequestRandom())
+ var hrrRandom handshake.Random
+ hrrRandom.UnmarshalFixed(hrrRandomFixed)
+
+ return marshalServerHello(t, cfg, hrrRandom, extensions)
+}
+
+func marshalServerHello(t *testing.T, cfg *dtlsconfig.HandshakeConfig, random handshake.Random, extensions []extension.Value) []byte {
+ t.Helper()
+
+ return marshalServerHelloWithSequence(t, cfg, random, extensions, 0)
+}
+
+func marshalServerHelloWithSequence(t *testing.T, cfg *dtlsconfig.HandshakeConfig, random handshake.Random, extensions []extension.Value, seq uint16) []byte {
+ t.Helper()
+
+ cipherSuiteID := uint16(cfg.LocalCipherSuites[0].ID())
+ serverHello := &handshake.MessageServerHello{Version: protocol.Version1_2, Random: random, CipherSuiteID: &cipherSuiteID, CompressionMethod: dtlsflight.DefaultCompressionMethods()[0], Extensions: extensions}
+ if cfg.GetPSKs != nil && !dtlsflight13.IsHelloRetryRequest(serverHello) {
+ serverHello.Extensions = append(serverHello.Extensions, &extension13.SelectedPSK{Identity: 0})
+ }
+ rawServerHello, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: seq}, Message: serverHello}).Marshal()
+ require.NoError(t, err)
+
+ return rawServerHello
+}
+
+func generateFlight13_1ClientHello(t *testing.T, cfg *dtlsconfig.HandshakeConfig) *handshake.MessageClientHello {
+ t.Helper()
+
+ state := newTestState13(t, false)
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, pkts, 1)
+
+ hand, ok := pkts[0].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ raw, err := hand.Marshal()
+ require.NoError(t, err)
+
+ var parsed handshake.Handshake
+ require.NoError(t, parsed.Unmarshal(raw))
+ clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
+ require.True(t, ok)
+
+ return clientHello
+}
+
+func TestFlight13_1GenerateClientHelloIncludesRequiredExtensions(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, pkts, 1)
+
+ hand, ok := pkts[0].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ raw, err := hand.Marshal()
+ require.NoError(t, err)
+
+ var parsed handshake.Handshake
+ require.NoError(t, parsed.Unmarshal(raw))
+ clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
+ require.True(t, ok)
+
+ var supportedVersions *extension13.OfferedVersions
+ var supportedGroups *extension.SupportedGroups
+ for _, ext := range clientHello.Extensions {
+ switch typed := ext.(type) {
+ case *extension13.OfferedVersions:
+ supportedVersions = typed
+ case *extension.SupportedGroups:
+ supportedGroups = typed
+ }
+ }
+ require.NotNil(t, supportedVersions)
+ assert.Equal(t, []protocol.Version{protocol.Version1_3}, supportedVersions.Versions)
+ require.NotNil(t, supportedGroups)
+ assert.Equal(t, cfg.EllipticCurves, supportedGroups.Groups)
+}
+
+func TestFlight13_1GenerateClientHelloIncludesSignatureAlgorithms(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ cfg.LocalCertSignatureSchemes = cfg.LocalSignatureSchemes[:1]
+
+ clientHello := generateFlight13_1ClientHello(t, cfg)
+
+ var signatureAlgorithms *extension.SignatureAlgorithms
+ var signatureAlgorithmsCert *extension.CertificateSignatureAlgorithms
+ for _, ext := range clientHello.Extensions {
+ switch typed := ext.(type) {
+ case *extension.SignatureAlgorithms:
+ signatureAlgorithms = typed
+ case *extension.CertificateSignatureAlgorithms:
+ signatureAlgorithmsCert = typed
+ }
+ }
+
+ require.NotNil(t, signatureAlgorithms)
+ assert.Equal(t, dtlsflight.SignatureSchemeIDs(cfg.LocalSignatureSchemes), signatureAlgorithms.Schemes)
+ require.NotNil(t, signatureAlgorithmsCert)
+ assert.Equal(t, dtlsflight.SignatureSchemeIDs(cfg.LocalCertSignatureSchemes), signatureAlgorithmsCert.Schemes)
+}
+
+func TestFlight13_1GenerateRetainsPrivateKeysForAdvertisedShares(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, pkts, 1)
+
+ hand, ok := pkts[0].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ raw, err := hand.Marshal()
+ require.NoError(t, err)
+
+ var parsed handshake.Handshake
+ require.NoError(t, parsed.Unmarshal(raw))
+ clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
+ require.True(t, ok)
+
+ var keyShare *extension13.ClientKeyShare
+ for _, ext := range clientHello.Extensions {
+ if ks, ok := ext.(*extension13.ClientKeyShare); ok {
+ keyShare = ks
+
+ break
+ }
+ }
+ require.NotNil(t, keyShare)
+ require.Len(t, keyShare.Shares, len(cfg.EllipticCurves))
+ require.Len(t, state.LocalKeyEntries, len(keyShare.Shares))
+ require.Len(t, state.LocalKeypairs, len(keyShare.Shares))
+
+ for _, entry := range keyShare.Shares {
+ t.Run(entry.Group.String(), func(t *testing.T) {
+ localKeypair, ok := state.LocalKeypairs[entry.Group]
+ require.True(t, ok)
+ require.Equal(t, entry.KeyExchange, localKeypair.PublicKey)
+
+ peerKeypair, err := elliptic.GenerateKeypair(entry.Group)
+ require.NoError(t, err)
+
+ localSecret, err := prf.PreMasterSecret(peerKeypair.PublicKey, localKeypair.PrivateKey, entry.Group)
+ require.NoError(t, err)
+
+ peerSecret, err := prf.PreMasterSecret(localKeypair.PublicKey, peerKeypair.PrivateKey, entry.Group)
+ require.NoError(t, err)
+
+ assert.Equal(t, peerSecret, localSecret)
+ })
+ }
+}
+
+func TestFlight13_1GenerateClientHelloIncludesX25519MLKEM768KeyShare(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ cfg.EllipticCurves = []elliptic.Curve{elliptic.X25519MLKEM768}
+ state := newTestState13(t, false)
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, pkts, 1)
+
+ hand, ok := pkts[0].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ raw, err := hand.Marshal()
+ require.NoError(t, err)
+
+ var parsed handshake.Handshake
+ require.NoError(t, parsed.Unmarshal(raw))
+ clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
+ require.True(t, ok)
+
+ var keyShare *extension13.ClientKeyShare
+ for _, ext := range clientHello.Extensions {
+ if ks, ok := ext.(*extension13.ClientKeyShare); ok {
+ keyShare = ks
+
+ break
+ }
+ }
+ require.NotNil(t, keyShare)
+ require.Len(t, keyShare.Shares, 1)
+ assert.Equal(t, elliptic.X25519MLKEM768, keyShare.Shares[0].Group)
+ assert.Len(t, keyShare.Shares[0].KeyExchange, elliptic.X25519MLKEM768ClientPublicKeySize)
+
+ localKeypair := state.LocalKeypairs[elliptic.X25519MLKEM768]
+ require.NotNil(t, localKeypair)
+ serverKeypair, err := elliptic.GenerateKeypairForPeer(elliptic.X25519MLKEM768, localKeypair.PublicKey)
+ require.NoError(t, err)
+ assert.Len(t, serverKeypair.PublicKey, elliptic.X25519MLKEM768ServerPublicKeySize)
+
+ clientSecret, err := prf.PreMasterSecret(
+ serverKeypair.PublicKey,
+ localKeypair.PrivateKey,
+ elliptic.X25519MLKEM768,
+ )
+ require.NoError(t, err)
+ serverSecret, err := prf.PreMasterSecret(
+ localKeypair.PublicKey,
+ serverKeypair.PrivateKey,
+ elliptic.X25519MLKEM768,
+ )
+ require.NoError(t, err)
+
+ assert.Equal(t, serverSecret, clientSecret)
+ assert.Len(t, clientSecret, elliptic.X25519MLKEM768SharedSecretSize)
+}
+
+func TestFlight13_1ParseStoresHelloRetryRequestSelectedGroup(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ selectedGroup := elliptic.P384
+ cfg.ClientHelloMessageHook = omitInitialKeyShares()
+
+ rawServerHello := marshalHelloRetryRequestServerHello(t, cfg, []extension.Value{&extension13.SelectedVersion{Version: protocol.Version1_3}, &extension13.RetryKeyShare{SelectedGroup: selectedGroup}})
+
+ state := newTestState13(t, false)
+ _, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{
+ state: state,
+ cfg: cfg,
+ })
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ cache := dtlsflight.NewCache()
+ cache.Push(rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight1, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight3, nextFlight)
+ assert.Equal(t, selectedGroup, state.HelloRetryRequest.SelectedGroup)
+}
+
+func TestFlight13_1ParseRejectsHelloRetryRequestWithoutSupportedVersions(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ selectedGroup := elliptic.P384
+
+ rawServerHello := marshalHelloRetryRequestServerHello(t, cfg, []extension.Value{&extension13.RetryKeyShare{SelectedGroup: selectedGroup}})
+
+ state := newTestState13(t, false)
+ cache := dtlsflight.NewCache()
+ cache.Push(rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight1, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.ErrorIs(t, err, dtlserrors.ErrMissingSupportedVersionsExtension)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, alert.Fatal, dtlsAlert.Level)
+ assert.Equal(t, alert.MissingExtension, dtlsAlert.Description)
+ assert.Zero(t, nextFlight)
+ assert.False(t, state.HasRemoteKeyEntries)
+}
+
+func TestFlight13_1ParseRejectsHelloRetryRequestWithWrongSelectedVersion(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ selectedGroup := elliptic.P384
+
+ rawServerHello := marshalHelloRetryRequestServerHello(t, cfg, []extension.Value{&extension13.SelectedVersion{Version: protocol.Version1_2}, &extension13.RetryKeyShare{SelectedGroup: selectedGroup}})
+
+ state := newTestState13(t, false)
+ cache := dtlsflight.NewCache()
+ cache.Push(rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight1, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.ErrorIs(t, err, dtlserrors.ErrUnsupportedProtocolVersion)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, alert.Fatal, dtlsAlert.Level)
+ assert.Equal(t, alert.ProtocolVersion, dtlsAlert.Description)
+ assert.Zero(t, nextFlight)
+ assert.False(t, state.HasRemoteKeyEntries)
+}
+
+func TestFlight13_1ParseRejectsHelloRetryRequestWithClientHelloSupportedVersionsEncoding(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ selectedGroup := elliptic.P384
+
+ rawServerHello := marshalHelloRetryRequestServerHello(
+ t,
+ cfg,
+ []extension.Value{
+ extension.Raw{
+ Type: extension.TypeSupportedVersions,
+ Data: []byte{
+ 0x02, // ClientHello vector length
+ 0xfe, 0xfc, // DTLS v1.3
+ },
+ },
+ &extension13.RetryKeyShare{SelectedGroup: selectedGroup},
+ },
+ )
+
+ parsed := &handshake.Handshake{}
+ err := parsed.Unmarshal(rawServerHello)
+ require.ErrorIs(t, err, dtlserrors.ErrInvalidSupportedVersionsFormat)
+}
+
+func TestFlight13_3GenerateRejectsWithoutCommonVersion(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ require.NoError(t, state.LocalRandom.Populate())
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight3, &handshakeTestContext13{state: state, cfg: cfg})
+
+ require.ErrorIs(t, err, dtlserrors.ErrNoCommonProtocolVersion)
+ require.Nil(t, dtlsAlert)
+ require.Nil(t, pkts)
+}
+
+func TestFlight13_3GenerateIncludesCookieAndSupportedVersions(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ state.Cookie = []byte{0x01, 0x02, 0x03, 0x04}
+ state.RemoteVersions = []protocol.Version{protocol.Version1_3}
+ state.HelloRetryRequest = retryRequestForTest(
+ t, state, cfg, state.LocalClientHelloSnapshots.Initial(), 0,
+ )
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight3, &handshakeTestContext13{state: state, cfg: cfg})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, pkts, 1)
+
+ hand, ok := pkts[0].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ raw, err := hand.Marshal()
+ require.NoError(t, err)
+
+ var parsed handshake.Handshake
+ require.NoError(t, parsed.Unmarshal(raw))
+ clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
+ require.True(t, ok)
+
+ var supportedVersions *extension13.OfferedVersions
+ for _, ext := range clientHello.Extensions {
+ if sv, ok := ext.(*extension13.OfferedVersions); ok {
+ supportedVersions = sv
+
+ break
+ }
+ }
+ require.NotNil(t, supportedVersions)
+ assert.Equal(t, []protocol.Version{protocol.Version1_3}, supportedVersions.Versions)
+
+ var signatureAlgorithms *extension.SignatureAlgorithms
+ for _, ext := range clientHello.Extensions {
+ if sigAlgs, ok := ext.(*extension.SignatureAlgorithms); ok {
+ signatureAlgorithms = sigAlgs
+
+ break
+ }
+ }
+ require.NotNil(t, signatureAlgorithms)
+ assert.Equal(t, []uint16{0x0403}, signatureAlgorithms.Schemes)
+
+ var cookieExt *extension13.Cookie
+ for _, ext := range clientHello.Extensions {
+ if c, ok := ext.(*extension13.Cookie); ok {
+ cookieExt = c
+
+ break
+ }
+ }
+ require.NotNil(t, cookieExt)
+ assert.Equal(t, state.Cookie, cookieExt.Cookie)
+}
+
+func TestFlight13_3GeneratePrioritizesHelloRetryRequestSelectedGroup(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ selectedGroup := elliptic.P384
+
+ state := newTestState13(t, false)
+ state.RemoteVersions = []protocol.Version{protocol.Version1_3}
+ state.HelloRetryRequest = retryRequestForTest(
+ t, state, cfg, state.LocalClientHelloSnapshots.Initial(), selectedGroup,
+ )
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight3, &handshakeTestContext13{state: state, cfg: cfg})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, pkts, 1)
+
+ hand, ok := pkts[0].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ raw, err := hand.Marshal()
+ require.NoError(t, err)
+
+ var parsed handshake.Handshake
+ require.NoError(t, parsed.Unmarshal(raw))
+ clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
+ require.True(t, ok)
+
+ var keyShare *extension13.ClientKeyShare
+ for _, ext := range clientHello.Extensions {
+ if ks, ok := ext.(*extension13.ClientKeyShare); ok {
+ keyShare = ks
+
+ break
+ }
+ }
+ require.NotNil(t, keyShare)
+ require.Len(t, keyShare.Shares, 1)
+ assert.Equal(t, selectedGroup, keyShare.Shares[0].Group)
+ assert.NotEmpty(t, keyShare.Shares[0].KeyExchange)
+
+ selectedKeypair := state.LocalKeypairs[selectedGroup]
+ require.NotNil(t, selectedKeypair)
+ assert.Equal(t, keyShare.Shares[0].KeyExchange, selectedKeypair.PublicKey)
+}
+
+func TestFlight13_3ParseNegotiatesVersionCipherAndKeyShare(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ configureTestPSK13(cfg)
+ state := newTestState13(t, false)
+ transcript := dtlshandshake.NewTranscript()
+ clientHello, _, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+ appended, err := dtlshandshake.AppendClientHelloInitialFlights(transcript, clientHello)
+ require.NoError(t, err)
+ require.True(t, appended)
+ clientHelloCanonical := canonicalPacketHandshake13(t, clientHello[0])
+
+ group := cfg.EllipticCurves[0]
+ serverKeypair, err := elliptic.GenerateKeypair(group)
+ require.NoError(t, err)
+
+ random := handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01, 0x02, 0x03}}
+ rawServerHello := marshalServerHello(t, cfg, random, []extension.Value{&extension13.SelectedVersion{Version: protocol.Version1_3}, &extension13.ServerKeyShare{Share: extension13.KeyShareEntry{Group: group, KeyExchange: serverKeypair.PublicKey}}})
+ serverHelloCanonical, err := canonicalHandshake13(rawServerHello)
+ require.NoError(t, err)
+
+ cache := dtlsflight.NewCache()
+ cache.Push(rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+ rawEncryptedExtensions, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: 1}, Message: &handshake.MessageEncryptedExtensions{}}).Marshal()
+ require.NoError(t, err)
+ encryptedExtensionsCanonical, err := canonicalHandshake13(rawEncryptedExtensions)
+ require.NoError(t, err)
+ clientKeypair := state.LocalKeypairs[group]
+ require.NotNil(t, clientKeypair)
+ expected, err := prf.PreMasterSecret(clientKeypair.PublicKey, serverKeypair.PrivateKey, group)
+ require.NoError(t, err)
+ expectedSecrets, err := deriveHandshakeTrafficSecrets13(cfg.LocalCipherSuites[0].HashFunc(), firstPSKSecret13(state), expected, hashTranscript13(clientHelloCanonical, serverHelloCanonical))
+ require.NoError(t, err)
+ state.CipherSuite = cfg.LocalCipherSuites[0]
+ state.KeySchedule.HandshakeTraffic = expectedSecrets
+ rawFinished := marshalServerFinished13(t, state, 2, clientHelloCanonical, serverHelloCanonical, encryptedExtensionsCanonical)
+ state.CipherSuite = nil
+ state.KeySchedule.HandshakeTraffic = dtlsstate.TrafficSecrets{}
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg, transcript: transcript})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight(0), nextFlight)
+ assert.Equal(t, 1, state.HandshakeRecvSequence)
+ assert.Equal(t, dtlsflight13.EpochHandshake, state.RemoteEpoch())
+
+ cache.Push(rawEncryptedExtensions, dtlsflight13.EpochHandshake, 1, handshake.TypeEncryptedExtensions, false)
+ cache.Push(rawFinished, dtlsflight13.EpochHandshake, 2, handshake.TypeFinished, false)
+ nextFlight, dtlsAlert, err = flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg, transcript: transcript})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight5, nextFlight)
+
+ assert.Equal(t, protocol.Version1_3, state.LocalVersion)
+ assert.Equal(t, []protocol.Version{protocol.Version1_3}, state.RemoteVersions)
+ require.NotNil(t, state.CipherSuite)
+ assert.Equal(t, cfg.LocalCipherSuites[0].ID(), state.CipherSuite.ID())
+ assert.Equal(t, group, state.SelectedGroup)
+ assert.Equal(t, random.RandomBytes, state.RemoteRandom.RandomBytes)
+ require.True(t, state.HasRemoteKeyEntries)
+ require.Len(t, state.RemoteKeyEntries, 1)
+ assert.Equal(t, group, state.RemoteKeyEntries[0].Group)
+
+ assert.Equal(t, expected, state.KeyAgreementSecret)
+ assert.NotEmpty(t, state.KeyAgreementSecret)
+ assert.Equal(t, expectedSecrets, state.KeySchedule.HandshakeTraffic)
+ assert.NotEqual(t, state.KeySchedule.HandshakeTraffic.Client, state.KeySchedule.HandshakeTraffic.Server)
+ assert.True(t, testTrafficProtectionInitialized(state, dtlsflight13.EpochHandshake))
+}
+
+func TestFlight13_3ParseDrainsQueuedProtectedHandshakeBeforeEncryptedExtensions(t *testing.T) {
+ fixture := newFlight13ProtectedServerFlightFixture(t, true)
+ cache := dtlsflight.NewCache()
+ cache.Push(fixture.rawServerHello, fixture.cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+ drained := false
+ conn := &flight13QueuedPacketConn{
+ handleQueuedPackets: func(context.Context) error {
+ drained = true
+ assert.True(t, testTrafficProtectionInitialized(fixture.state, dtlsflight13.EpochHandshake))
+ assert.Equal(t, dtlsflight13.EpochHandshake, fixture.state.RemoteEpoch())
+ cache.Push(fixture.rawEncryptedExtensions, dtlsflight13.EpochHandshake, 1, handshake.TypeEncryptedExtensions, false)
+ cache.Push(fixture.rawFinished, dtlsflight13.EpochHandshake, 2, handshake.TypeFinished, false)
+
+ return nil
+ },
+ }
+
+ nextFlight, dtlsAlert, err := flight13ParseForTestWithConn(t, dtlsflight13.Flight3, context.Background(), conn, &handshakeTestContext13{state: fixture.state, cache: cache, cfg: fixture.cfg, transcript: fixture.transcript})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.True(t, drained)
+ assert.Equal(t, dtlsflight13.Flight5, nextFlight)
+ assert.Equal(t, 3, fixture.state.HandshakeRecvSequence)
+}
+
+func TestFlight13ClientParsesEncryptedExtensionsFromProtectedRecord(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ configureTestPSK13(cfg)
+ cache := dtlsflight.NewCache()
+ commonState := &dtlsstate.Common{IsClient: true, LocalVersion: protocol.Version1_3}
+ conn := &Conn{fragmentBuffer: dtlsfragmentbuffer.New(), handshakeCache: cache, maximumTransmissionUnit: defaultMTU, replayProtectionWindow: defaultReplayProtectionWindow, log: logging.NewDefaultLoggerFactory().NewLogger("dtls"), state: &dtlsstate.State13{Common: commonState}}
+ state, err := dtlsstate.As13(conn.state)
+ require.NoError(t, err)
+ transcript := dtlshandshake.NewTranscript()
+
+ clientHello, _, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+ appended, err := dtlshandshake.AppendClientHelloInitialFlights(transcript, clientHello)
+ require.NoError(t, err)
+ require.True(t, appended)
+ clientHelloCanonical := canonicalPacketHandshake13(t, clientHello[0])
+
+ fixture := newFlight13ProtectedServerFlightFixtureFromClientHello(t, cfg, state, transcript, clientHelloCanonical)
+ cache.Push(fixture.rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+
+ peerCipherSuite := trafficSuiteForTest(t)
+ peerWriteProtection, err := newTestRecordProtection13(peerCipherSuite, fixture.handshakeSecrets.Server)
+ require.NoError(t, err)
+
+ protectedEncryptedExtensions := sealTestProtectedHandshakeRecordWithSequence(t, peerWriteProtection, fixture.rawEncryptedExtensions, 0)
+ protectedEncryptedExtensionsRaw, err := protectedEncryptedExtensions.Marshal()
+ require.NoError(t, err)
+ protectedFinished := sealTestProtectedHandshakeRecordWithSequence(t, peerWriteProtection, fixture.rawFinished, 1)
+ protectedFinishedRaw, err := protectedFinished.Marshal()
+ require.NoError(t, err)
+ conn.encryptedPackets = []addrPkt{{data: protectedEncryptedExtensionsRaw}, {data: protectedFinishedRaw}}
+
+ nextFlight, dtlsAlert, err := flight13ParseForTestWithConn(t, dtlsflight13.Flight3, context.Background(), adaptFlightConn(conn), &handshakeTestContext13{state: state, cache: cache, cfg: cfg, transcript: transcript})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight5, nextFlight)
+ assert.Equal(t, 3, state.HandshakeRecvSequence)
+
+ items := cache.Pull(dtlsflight.HandshakeCachePullRule{Typ: handshake.TypeEncryptedExtensions, Epoch: dtlsflight13.EpochHandshake, IsClient: false})
+ if assert.Len(t, items, 1) && assert.NotNil(t, items[0]) {
+ assert.Equal(t, fixture.rawEncryptedExtensions, items[0].Data)
+ }
+}
+
+func TestFlight13ClientParseAppendsNoHRRTranscriptOrder(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ configureTestPSK13(cfg)
+ state := newTestState13(t, false)
+ transcript := dtlshandshake.NewTranscript()
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg, transcript: transcript})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ appended, err := dtlshandshake.AppendClientHelloInitialFlights(transcript, pkts)
+ require.NoError(t, err)
+ require.True(t, appended)
+ clientHelloCanonical := canonicalPacketHandshake13(t, pkts[0])
+ fixture := newFlight13ProtectedServerFlightFixtureFromClientHello(t, cfg, state, transcript, clientHelloCanonical)
+
+ cache := dtlsflight.NewCache()
+ cache.Push(fixture.rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+ cache.Push(fixture.rawEncryptedExtensions, dtlsflight13.EpochHandshake, 1, handshake.TypeEncryptedExtensions, false)
+ finishedCanonical, err := canonicalHandshake13(fixture.rawFinished)
+ require.NoError(t, err)
+ cache.Push(fixture.rawFinished, dtlsflight13.EpochHandshake, 2, handshake.TypeFinished, false)
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight1, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg, transcript: transcript})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight5, nextFlight)
+ expectedTranscript := append(append(append(append([]byte(nil), clientHelloCanonical...), fixture.serverHelloCanonical...),
+ fixture.encryptedExtensionsCanonical...), finishedCanonical...)
+ assert.Equal(t, expectedTranscript, transcript.Bytes())
+}
+
+func TestFlight13ClientParseAppendsHRRTranscriptOrder(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ configureTestPSK13(cfg)
+ cfg.ClientHelloMessageHook = omitInitialKeyShares()
+ state := newTestState13(t, false)
+ transcript := dtlshandshake.NewTranscript()
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg, transcript: transcript})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ appended, err := dtlshandshake.AppendClientHelloInitialFlights(transcript, pkts)
+ require.NoError(t, err)
+ require.True(t, appended)
+ clientHello1Canonical := canonicalPacketHandshake13(t, pkts[0])
+
+ group := cfg.EllipticCurves[0]
+ rawHelloRetryRequest := marshalHelloRetryRequestServerHello(t, cfg, []extension.Value{&extension13.SelectedVersion{Version: protocol.Version1_3}, &extension13.RetryKeyShare{SelectedGroup: group}})
+ helloRetryRequestCanonical, err := canonicalHandshake13(rawHelloRetryRequest)
+ require.NoError(t, err)
+
+ cache := dtlsflight.NewCache()
+ cache.Push(rawHelloRetryRequest, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight1, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg, transcript: transcript})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight3, nextFlight)
+
+ clientHello2, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight3, &handshakeTestContext13{state: state, cache: cache, cfg: cfg, transcript: transcript})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, clientHello2, 1)
+ clientHello2Handshake, ok := clientHello2[0].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ clientHello2Handshake.Header.MessageSequence = 1
+ require.NoError(t, dtlshandshake.AppendOutboundHandshakeFlight(transcript, true, state.CipherSuite, clientHello2))
+ clientHello2Canonical := canonicalPacketHandshake13(t, clientHello2[0])
+
+ serverKeypair, err := elliptic.GenerateKeypair(group)
+ require.NoError(t, err)
+ rawServerHello := marshalServerHelloWithSequence(
+ t,
+ cfg,
+ handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x02}},
+ []extension.Value{
+ &extension13.SelectedVersion{Version: protocol.Version1_3},
+ &extension13.ServerKeyShare{Share: extension13.KeyShareEntry{Group: group, KeyExchange: serverKeypair.PublicKey}},
+ },
+ 1,
+ )
+ serverHelloCanonical, err := canonicalHandshake13(rawServerHello)
+ require.NoError(t, err)
+ cache.Push(rawServerHello, cfg.InitialEpoch, 1, handshake.TypeServerHello, false)
+ rawEncryptedExtensions, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: 2}, Message: &handshake.MessageEncryptedExtensions{}}).Marshal()
+ require.NoError(t, err)
+ cache.Push(rawEncryptedExtensions, dtlsflight13.EpochHandshake, 2, handshake.TypeEncryptedExtensions, false)
+ encryptedExtensionsCanonical, err := canonicalHandshake13(rawEncryptedExtensions)
+ require.NoError(t, err)
+
+ clientHello1Hash := hashTranscript13(clientHello1Canonical)
+ messageHash := canonicalTranscriptHandshake13(handshake.TypeMessageHash, clientHello1Hash)
+ clientKeypair := state.LocalKeypairs[group]
+ require.NotNil(t, clientKeypair)
+ keyAgreementSecret, err := prf.PreMasterSecret(clientKeypair.PublicKey, serverKeypair.PrivateKey, group)
+ require.NoError(t, err)
+ secrets, err := deriveHandshakeTrafficSecrets13(cfg.LocalCipherSuites[0].HashFunc(), firstPSKSecret13(state), keyAgreementSecret, hashTranscript13(messageHash, helloRetryRequestCanonical, clientHello2Canonical, serverHelloCanonical))
+ require.NoError(t, err)
+ state.CipherSuite = cfg.LocalCipherSuites[0]
+ state.KeySchedule.HandshakeTraffic = secrets
+ rawFinished := marshalServerFinished13(t, state, 3, messageHash, helloRetryRequestCanonical, clientHello2Canonical, serverHelloCanonical, encryptedExtensionsCanonical)
+ finishedCanonical, err := canonicalHandshake13(rawFinished)
+ require.NoError(t, err)
+ state.CipherSuite = nil
+ state.KeySchedule.HandshakeTraffic = dtlsstate.TrafficSecrets{}
+ cache.Push(rawFinished, dtlsflight13.EpochHandshake, 3, handshake.TypeFinished, false)
+
+ nextFlight, dtlsAlert, err = flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg, transcript: transcript})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight5, nextFlight)
+
+ expectedTranscript := append(append(append(append(append([]byte(nil), messageHash...), helloRetryRequestCanonical...), clientHello2Canonical...), serverHelloCanonical...), encryptedExtensionsCanonical...)
+ expectedTranscript = append(expectedTranscript, finishedCanonical...)
+ assert.Equal(t, expectedTranscript, transcript.Bytes())
+}
+
+func TestFlight13_3ParseRejectsInvalidServerFinished(t *testing.T) {
+ tests := []struct {
+ name string
+ finished func(t *testing.T, f flight13ProtectedServerFlightFixture) []byte
+ }{
+ {
+ name: "tampered Finished",
+ finished: func(t *testing.T, f flight13ProtectedServerFlightFixture) []byte {
+ t.Helper()
+
+ raw := append([]byte(nil), f.rawFinished...)
+ raw[len(raw)-1] ^= 0xff
+
+ return raw
+ },
+ },
+ {
+ name: "wrong transcript",
+ finished: func(t *testing.T, f flight13ProtectedServerFlightFixture) []byte {
+ t.Helper()
+
+ verifyData := finishedVerifyData13(t, f.cfg.LocalCipherSuites[0].HashFunc(), f.handshakeSecrets.Server, hashTranscript13(f.clientHelloCanonical, f.serverHelloCanonical))
+
+ return marshalFinished13(t, 2, verifyData)
+ },
+ },
+ {
+ name: "wrong handshake traffic secret",
+ finished: func(t *testing.T, f flight13ProtectedServerFlightFixture) []byte {
+ t.Helper()
+
+ wrongSecret := append([]byte(nil), f.handshakeSecrets.Server...)
+ wrongSecret[0] ^= 0xff
+ verifyData := finishedVerifyData13(t, f.cfg.LocalCipherSuites[0].HashFunc(), wrongSecret, hashTranscript13(f.clientHelloCanonical, f.serverHelloCanonical, f.encryptedExtensionsCanonical))
+
+ return marshalFinished13(t, 2, verifyData)
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ fixture := newFlight13ProtectedServerFlightFixture(t, true)
+ cache := fixture.cacheWithFinished(test.finished(t, fixture))
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: fixture.state, cache: cache, cfg: fixture.cfg, transcript: fixture.transcript})
+
+ require.ErrorIs(t, err, dtlserrors.ErrVerifyDataMismatch)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, alert.Fatal, dtlsAlert.Level)
+ assert.Equal(t, alert.DecryptError, dtlsAlert.Description)
+ assert.Zero(t, nextFlight)
+ assert.Equal(t, 1, fixture.state.HandshakeRecvSequence)
+
+ expectedTranscript := append(append([]byte(nil), fixture.clientHelloCanonical...), fixture.serverHelloCanonical...)
+ assert.Equal(t, expectedTranscript, fixture.transcript.Bytes())
+ })
+ }
+}
+
+func TestFlight13_3ParseRunsVerifyConnectionWithoutServerCertificate(t *testing.T) {
+ fixture := newFlight13ProtectedServerFlightFixture(t, true)
+ var verifyConnectionCalled bool
+ fixture.cfg.VerifyConnection = adaptVerifyConnection(func(state *State) error {
+ verifyConnectionCalled = true
+ assert.Nil(t, state.PeerCertificates)
+ assert.Equal(t, fixture.cfg.LocalCipherSuites[0].ID(), state.CipherSuiteID)
+
+ return nil
+ })
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: fixture.state, cache: fixture.cacheWithFinished(fixture.rawFinished), cfg: fixture.cfg, transcript: fixture.transcript})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight5, nextFlight)
+ assert.True(t, verifyConnectionCalled)
+ assert.Nil(t, fixture.state.PeerCertificates)
+}
+
+func TestFlight13_3ParseRejectsVerifyConnectionErrorWithoutServerCertificate(t *testing.T) {
+ fixture := newFlight13ProtectedServerFlightFixture(t, true)
+ callbackErr := errFlight13ConnectionCallbackRejected
+ fixture.cfg.VerifyConnection = adaptVerifyConnection(func(*State) error {
+ return callbackErr
+ })
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: fixture.state, cache: fixture.cacheWithFinished(fixture.rawFinished), cfg: fixture.cfg, transcript: fixture.transcript})
+
+ require.ErrorIs(t, err, dtlserrors.ErrCertificateVerificationFailed)
+ require.ErrorIs(t, err, callbackErr)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, alert.Fatal, dtlsAlert.Level)
+ assert.Equal(t, alert.BadCertificate, dtlsAlert.Description)
+ assert.Zero(t, nextFlight)
+ assert.Nil(t, fixture.state.PeerCertificates)
+
+ expectedTranscript := append(append([]byte(nil), fixture.clientHelloCanonical...), fixture.serverHelloCanonical...)
+ assert.Equal(t, expectedTranscript, fixture.transcript.Bytes())
+}
+
+func TestFlight13_3ParseValidatesServerCertificate(t *testing.T) {
+ certificate, err := selfsign.GenerateSelfSignedWithDNS("server.test")
+ require.NoError(t, err)
+
+ fixture := newFlight13ProtectedServerFlightFixture(t, false)
+ fixture.cfg.RootCAs = flight13RootCAsForCertificate(t, certificate)
+ fixture.cfg.ServerName = "server.test"
+
+ var verifyPeerCalled bool
+ fixture.cfg.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+ verifyPeerCalled = true
+ require.Equal(t, certificate.Certificate, rawCerts)
+ require.Len(t, verifiedChains, 1)
+ require.NotEmpty(t, verifiedChains[0])
+ assert.Equal(t, certificate.Leaf.Raw, verifiedChains[0][0].Raw)
+
+ return nil
+ }
+
+ var verifyConnectionCalled bool
+ fixture.cfg.VerifyConnection = adaptVerifyConnection(func(state *State) error {
+ verifyConnectionCalled = true
+ require.Equal(t, certificate.Certificate, state.PeerCertificates)
+ assert.Equal(t, fixture.cfg.LocalCipherSuites[0].ID(), state.CipherSuiteID)
+ state.PeerCertificates[0][0] ^= 0xff
+
+ return nil
+ })
+
+ certificateFlight := fixture.cacheWithCertificate(t, certificate)
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: fixture.state, cache: certificateFlight.cache, cfg: fixture.cfg, transcript: fixture.transcript})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight5, nextFlight)
+ assert.True(t, verifyPeerCalled)
+ assert.True(t, verifyConnectionCalled)
+ assert.Equal(t, certificate.Certificate, fixture.state.PeerCertificates)
+
+ expectedTranscript := append([]byte(nil), fixture.clientHelloCanonical...)
+ for _, message := range [][]byte{fixture.serverHelloCanonical, fixture.encryptedExtensionsCanonical, certificateFlight.certificateCanonical, certificateFlight.certificateVerifyCanonical, certificateFlight.finishedCanonical} {
+ expectedTranscript = append(expectedTranscript, message...)
+ }
+ assert.Equal(t, expectedTranscript, fixture.transcript.Bytes())
+}
+
+func TestFlight13_3ParseRejectsWrongServerName(t *testing.T) {
+ certificate, err := selfsign.GenerateSelfSignedWithDNS("server.test")
+ require.NoError(t, err)
+
+ fixture := newFlight13ProtectedServerFlightFixture(t, false)
+ fixture.cfg.RootCAs = flight13RootCAsForCertificate(t, certificate)
+ fixture.cfg.ServerName = "wrong.test"
+ certificateFlight := fixture.cacheWithCertificate(t, certificate)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: fixture.state, cache: certificateFlight.cache, cfg: fixture.cfg, transcript: fixture.transcript})
+
+ require.ErrorIs(t, err, dtlserrors.ErrCertificateVerificationFailed)
+ var hostnameErr x509.HostnameError
+ assert.True(t, errors.As(err, &hostnameErr))
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, alert.Fatal, dtlsAlert.Level)
+ assert.Equal(t, alert.BadCertificate, dtlsAlert.Description)
+ assert.Zero(t, nextFlight)
+ assert.Nil(t, fixture.state.PeerCertificates)
+ assert.Equal(t, 1, fixture.state.HandshakeRecvSequence)
+
+ expectedTranscript := append(append([]byte(nil), fixture.clientHelloCanonical...), fixture.serverHelloCanonical...)
+ assert.Equal(t, expectedTranscript, fixture.transcript.Bytes())
+}
+
+func TestFlight13_3ParseInsecureSkipVerifyStillRunsCertificateCallback(t *testing.T) {
+ certificate, err := selfsign.GenerateSelfSignedWithDNS("server.test")
+ require.NoError(t, err)
+
+ fixture := newFlight13ProtectedServerFlightFixture(t, false)
+ fixture.cfg.InsecureSkipVerify = true
+ fixture.cfg.ServerName = "wrong.test"
+
+ var verifyPeerCalled bool
+ fixture.cfg.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+ verifyPeerCalled = true
+ assert.Equal(t, certificate.Certificate, rawCerts)
+ assert.Nil(t, verifiedChains)
+
+ return nil
+ }
+ var verifyConnectionCalled bool
+ fixture.cfg.VerifyConnection = adaptVerifyConnection(func(state *State) error {
+ verifyConnectionCalled = true
+ assert.Equal(t, certificate.Certificate, state.PeerCertificates)
+
+ return nil
+ })
+
+ certificateFlight := fixture.cacheWithCertificate(t, certificate)
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: fixture.state, cache: certificateFlight.cache, cfg: fixture.cfg, transcript: fixture.transcript})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight5, nextFlight)
+ assert.True(t, verifyPeerCalled)
+ assert.True(t, verifyConnectionCalled)
+ assert.Equal(t, certificate.Certificate, fixture.state.PeerCertificates)
+}
+
+func TestFlight13_3ParseRejectsServerIdentityCallbackErrors(t *testing.T) {
+ certificate, err := selfsign.GenerateSelfSignedWithDNS("server.test")
+ require.NoError(t, err)
+ callbackErr := errFlight13ServerIdentityCallbackReject
+
+ tests := []struct {
+ name string
+ configure func(*dtlsconfig.HandshakeConfig)
+ }{
+ {
+ name: "VerifyPeerCertificate",
+ configure: func(cfg *dtlsconfig.HandshakeConfig) {
+ cfg.VerifyPeerCertificate = func([][]byte, [][]*x509.Certificate) error {
+ return callbackErr
+ }
+ },
+ },
+ {
+ name: "VerifyConnection",
+ configure: func(cfg *dtlsconfig.HandshakeConfig) {
+ cfg.VerifyConnection = adaptVerifyConnection(func(*State) error {
+ return callbackErr
+ })
+ },
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ fixture := newFlight13ProtectedServerFlightFixture(t, false)
+ fixture.cfg.RootCAs = flight13RootCAsForCertificate(t, certificate)
+ fixture.cfg.ServerName = "server.test"
+ test.configure(fixture.cfg)
+ certificateFlight := fixture.cacheWithCertificate(t, certificate)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: fixture.state, cache: certificateFlight.cache, cfg: fixture.cfg, transcript: fixture.transcript})
+
+ require.ErrorIs(t, err, dtlserrors.ErrCertificateVerificationFailed)
+ require.ErrorIs(t, err, callbackErr)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, alert.Fatal, dtlsAlert.Level)
+ assert.Equal(t, alert.BadCertificate, dtlsAlert.Description)
+ assert.Zero(t, nextFlight)
+ assert.Nil(t, fixture.state.PeerCertificates)
+
+ expectedTranscript := append(append([]byte(nil), fixture.clientHelloCanonical...), fixture.serverHelloCanonical...)
+ assert.Equal(t, expectedTranscript, fixture.transcript.Bytes())
+ })
+ }
+}
+
+func TestFlight13_3ParseKeepsReadingWithoutServerHello(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: state, cache: dtlsflight.NewCache(), cfg: cfg})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Zero(t, nextFlight)
+}
+
+func TestFlight13_3ParseRejectsSecondHelloRetryRequest(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ _, _, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+
+ rawServerHello := marshalHelloRetryRequestServerHello(t, cfg, []extension.Value{&extension13.SelectedVersion{Version: protocol.Version1_3}})
+
+ cache := dtlsflight.NewCache()
+ cache.Push(rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+ rawEncryptedExtensions, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: 1}, Message: &handshake.MessageEncryptedExtensions{}}).Marshal()
+ require.NoError(t, err)
+ cache.Push(rawEncryptedExtensions, dtlsflight13.EpochHandshake, 1, handshake.TypeEncryptedExtensions, false)
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.ErrorIs(t, err, dtlserrors.ErrUnexpectedSecondHelloRetryRequest)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, alert.Fatal, dtlsAlert.Level)
+ assert.Equal(t, alert.UnexpectedMessage, dtlsAlert.Description)
+ assert.Zero(t, nextFlight)
+}
+
+func TestFlight13_3ParseRejectsWrongLegacyVersion(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ _, _, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+
+ group := cfg.EllipticCurves[0]
+ serverKeypair, err := elliptic.GenerateKeypair(group)
+ require.NoError(t, err)
+
+ cipherSuiteID := uint16(cfg.LocalCipherSuites[0].ID())
+ serverHello := &handshake.MessageServerHello{
+ Version: protocol.Version1_0,
+ Random: handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}},
+ CipherSuiteID: &cipherSuiteID,
+ CompressionMethod: dtlsflight.DefaultCompressionMethods()[0],
+ Extensions: []extension.Value{&extension13.SelectedVersion{Version: protocol.Version1_3}, &extension13.ServerKeyShare{Share: extension13.KeyShareEntry{Group: group, KeyExchange: serverKeypair.PublicKey}}},
+ }
+ rawServerHello, err := (&handshake.Handshake{Message: serverHello}).Marshal()
+ require.NoError(t, err)
+
+ cache := dtlsflight.NewCache()
+ cache.Push(rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+ rawEncryptedExtensions, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: 1}, Message: &handshake.MessageEncryptedExtensions{}}).Marshal()
+ require.NoError(t, err)
+ cache.Push(rawEncryptedExtensions, dtlsflight13.EpochHandshake, 1, handshake.TypeEncryptedExtensions, false)
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.ErrorIs(t, err, dtlserrors.ErrUnsupportedProtocolVersion)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, alert.ProtocolVersion, dtlsAlert.Description)
+ assert.Zero(t, nextFlight)
+}
+
+func TestFlight13_3ParseRejectsMissingSupportedVersions(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ _, _, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+
+ random := handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}}
+ rawServerHello := marshalServerHello(t, cfg, random, nil)
+
+ cache := dtlsflight.NewCache()
+ cache.Push(rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+ rawEncryptedExtensions, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: 1}, Message: &handshake.MessageEncryptedExtensions{}}).Marshal()
+ require.NoError(t, err)
+ cache.Push(rawEncryptedExtensions, dtlsflight13.EpochHandshake, 1, handshake.TypeEncryptedExtensions, false)
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.ErrorIs(t, err, dtlserrors.ErrUnsupportedProtocolVersion)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, alert.ProtocolVersion, dtlsAlert.Description)
+ assert.Zero(t, nextFlight)
+}
+
+func TestFlight13_3ParseRejectsMissingKeyShare(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ _, _, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+
+ random := handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}}
+ rawServerHello := marshalServerHello(t, cfg, random, []extension.Value{&extension13.SelectedVersion{Version: protocol.Version1_3}})
+
+ cache := dtlsflight.NewCache()
+ cache.Push(rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+ rawEncryptedExtensions, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: 1}, Message: &handshake.MessageEncryptedExtensions{}}).Marshal()
+ require.NoError(t, err)
+ cache.Push(rawEncryptedExtensions, dtlsflight13.EpochHandshake, 1, handshake.TypeEncryptedExtensions, false)
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.ErrorIs(t, err, dtlserrors.ErrServerKeyShareMissing)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, alert.IllegalParameter, dtlsAlert.Description)
+ assert.Zero(t, nextFlight)
+}
+
+func TestFlight13_3ParseRejectsUnofferedKeyShareGroup(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ _, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{
+ state: state,
+ cfg: cfg,
+ })
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+
+ group := elliptic.X25519MLKEM768
+ serverKeypair, err := elliptic.GenerateKeypair(group)
+ require.NoError(t, err)
+
+ random := handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}}
+ rawServerHello := marshalServerHello(t, cfg, random, []extension.Value{&extension13.SelectedVersion{Version: protocol.Version1_3}, &extension13.ServerKeyShare{Share: extension13.KeyShareEntry{Group: group, KeyExchange: serverKeypair.PublicKey}}})
+
+ cache := dtlsflight.NewCache()
+ cache.Push(rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+ rawEncryptedExtensions, err := (&handshake.Handshake{Header: handshake.Header{MessageSequence: 1}, Message: &handshake.MessageEncryptedExtensions{}}).Marshal()
+ require.NoError(t, err)
+ cache.Push(rawEncryptedExtensions, dtlsflight13.EpochHandshake, 1, handshake.TypeEncryptedExtensions, false)
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.ErrorIs(t, err, dtlserrors.ErrServerKeyShareUnknownGroup)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, alert.IllegalParameter, dtlsAlert.Description)
+ assert.Zero(t, nextFlight)
+}
+
+func TestFlight13_0ParseSelectsNegotiatedGroupWithoutGeneratingKeypair(t *testing.T) {
+ tests := []struct {
+ name string
+ serverCurves []elliptic.Curve
+ clientGroup elliptic.Curve
+ staleServerKeypair bool
+ }{
+ {"preserves stale server keypair", []elliptic.Curve{elliptic.P384, elliptic.P256}, elliptic.P384, true},
+ {"selects X25519 MLKEM 768", []elliptic.Curve{elliptic.X25519MLKEM768}, elliptic.X25519MLKEM768, false},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ cfg.EllipticCurves = test.serverCurves
+ clientKeypair, err := elliptic.GenerateKeypair(test.clientGroup)
+ require.NoError(t, err)
+
+ state := newTestState13(t, false)
+ var staleServerKeypair *elliptic.Keypair
+ if test.staleServerKeypair {
+ staleServerKeypair, err = elliptic.GenerateKeypair(elliptic.X25519)
+ require.NoError(t, err)
+ state.SelectedGroup = elliptic.X25519
+ state.LocalKeypair = staleServerKeypair
+ }
+ cache := dtlsflight.NewCache()
+ pushFlight13_0ClientHello(t, cache, cfg, []extension.Value{
+ &extension.SignatureAlgorithms{Schemes: dtlsflight.SignatureSchemeIDs(cfg.LocalSignatureSchemes)},
+ &extension.SupportedGroups{Groups: []elliptic.Curve{test.clientGroup}},
+ &extension13.ClientKeyShare{Shares: []extension13.KeyShareEntry{{
+ Group: test.clientGroup, KeyExchange: clientKeypair.PublicKey,
+ }}},
+ &extension13.OfferedVersions{Versions: []protocol.Version{protocol.Version1_3}},
+ })
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight0, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight2, nextFlight)
+ assert.Equal(t, test.clientGroup, state.SelectedGroup)
+ assert.Same(t, staleServerKeypair, state.LocalKeypair)
+ assert.Empty(t, state.KeyAgreementSecret)
+ })
+ }
+}
+
+func TestFlight13_0ParseSelectsServerPreferredGroupFromClientShares(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ cfg.EllipticCurves = []elliptic.Curve{elliptic.X25519MLKEM768, elliptic.X25519}
+
+ mlkemKeypair, err := elliptic.GenerateKeypair(elliptic.X25519MLKEM768)
+ require.NoError(t, err)
+ x25519Keypair, err := elliptic.GenerateKeypair(elliptic.X25519)
+ require.NoError(t, err)
+
+ state := newTestState13(t, false)
+ cache := dtlsflight.NewCache()
+ pushFlight13_0ClientHello(t, cache, cfg, []extension.Value{
+ &extension.SignatureAlgorithms{Schemes: dtlsflight.SignatureSchemeIDs(cfg.LocalSignatureSchemes)},
+ &extension.SupportedGroups{
+ Groups: []elliptic.Curve{elliptic.X25519, elliptic.X25519MLKEM768},
+ },
+ &extension13.ClientKeyShare{
+ Shares: []extension13.KeyShareEntry{{Group: elliptic.X25519, KeyExchange: x25519Keypair.PublicKey}, {Group: elliptic.X25519MLKEM768, KeyExchange: mlkemKeypair.PublicKey}},
+ },
+ &extension13.OfferedVersions{Versions: []protocol.Version{protocol.Version1_3}},
+ })
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight0, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight2, nextFlight)
+ assert.Equal(t, elliptic.X25519MLKEM768, state.SelectedGroup)
+ assert.Nil(t, state.LocalKeypair)
+ assert.Empty(t, state.KeyAgreementSecret)
+}
+
+func TestFlight13_0ParseRequestsPreferredGroupWhenShareMissing(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ cfg.EllipticCurves = []elliptic.Curve{elliptic.X25519MLKEM768, elliptic.X25519}
+
+ x25519Keypair, err := elliptic.GenerateKeypair(elliptic.X25519)
+ require.NoError(t, err)
+
+ state := newTestState13(t, false)
+ cache := dtlsflight.NewCache()
+ pushFlight13_0ClientHello(t, cache, cfg, []extension.Value{
+ &extension.SignatureAlgorithms{Schemes: dtlsflight.SignatureSchemeIDs(cfg.LocalSignatureSchemes)},
+ &extension.SupportedGroups{Groups: cfg.EllipticCurves},
+ &extension13.ClientKeyShare{
+ Shares: []extension13.KeyShareEntry{
+ {Group: elliptic.X25519, KeyExchange: x25519Keypair.PublicKey},
+ },
+ },
+ &extension13.OfferedVersions{Versions: []protocol.Version{protocol.Version1_3}},
+ })
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight0, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight2, nextFlight)
+ assert.Equal(t, elliptic.X25519MLKEM768, state.SelectedGroup)
+
+ serverHello := serverHelloFromFlight13_2(t, state, cfg)
+ keyShare, ok := findRetryKeyShare(serverHello.Extensions)
+ require.True(t, ok)
+ assert.Equal(t, elliptic.X25519MLKEM768, keyShare.SelectedGroup)
+}
+
+func TestFlight13_0ParseRejectsClientHelloWithSelectedSupportedVersion(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+
+ extensions := []extension.Value{
+ &extension13.SelectedVersion{Version: protocol.Version1_3},
+ }
+ clientHello := &handshake.MessageClientHello{Version: protocol.Version1_2, Random: handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}}, CipherSuiteIDs: []uint16{uint16(cfg.LocalCipherSuites[0].ID())}, CompressionMethods: dtlsflight.DefaultCompressionMethods(), Extensions: extensions}
+ rawClientHello, err := (&handshake.Handshake{Message: clientHello}).Marshal()
+ require.NoError(t, err)
+
+ parsed := &handshake.Handshake{}
+ err = parsed.Unmarshal(rawClientHello)
+ require.ErrorIs(t, err, dtlserrors.ErrInvalidSupportedVersionsFormat)
+}
+
+func pushFlight13_0ClientHello(t *testing.T, cache *dtlsflight.Cache, cfg *dtlsconfig.HandshakeConfig, exts []extension.Value) []byte {
+ t.Helper()
+
+ clientHello := &handshake.MessageClientHello{Version: protocol.Version1_2, Random: handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}}, CipherSuiteIDs: []uint16{uint16(cfg.LocalCipherSuites[0].ID())}, CompressionMethods: dtlsflight.DefaultCompressionMethods(), Extensions: exts}
+ rawClientHello, err := (&handshake.Handshake{Message: clientHello}).Marshal()
+ require.NoError(t, err)
+
+ cache.Push(rawClientHello, cfg.InitialEpoch, 0, handshake.TypeClientHello, true)
+
+ return rawClientHello
+}
+
+func requiredClientHello13Extensions(t *testing.T, cfg *dtlsconfig.HandshakeConfig) []extension.Value {
+ t.Helper()
+
+ clientKeypair, err := elliptic.GenerateKeypair(cfg.EllipticCurves[0])
+ require.NoError(t, err)
+
+ return []extension.Value{
+ &extension.SignatureAlgorithms{Schemes: dtlsflight.SignatureSchemeIDs(cfg.LocalSignatureSchemes)},
+ &extension.SupportedGroups{Groups: cfg.EllipticCurves},
+ &extension13.ClientKeyShare{
+ Shares: []extension13.KeyShareEntry{
+ {Group: clientKeypair.Curve, KeyExchange: clientKeypair.PublicKey},
+ },
+ },
+ &extension13.OfferedVersions{Versions: []protocol.Version{protocol.Version1_3}},
+ }
+}
+
+func TestFlight13_0ParseRequiresCertificateAuthClientHelloExtensions(t *testing.T) {
+ t.Run("AcceptsSignatureAlgorithmsAndSupportedGroups", func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ cache := dtlsflight.NewCache()
+ pushFlight13_0ClientHello(t, cache, cfg, requiredClientHello13Extensions(t, cfg))
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight0, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight2, nextFlight)
+ assert.Equal(t, cfg.LocalSignatureSchemes, state.RemoteSignatureSchemes)
+ assert.Equal(t, cfg.EllipticCurves, state.RemoteGroups)
+ })
+
+ t.Run("RejectsPreSharedKeyWithoutModes", func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ cache := dtlsflight.NewCache()
+ binder := make([]byte, 32)
+ pushFlight13_0ClientHello(t, cache, cfg, []extension.Value{&extension13.OfferedVersions{Versions: []protocol.Version{protocol.Version1_3}}, &extension13.OfferedPSKs{Identities: []extension13.PSKIdentity{{Identity: []byte("psk"), ObfuscatedTicketAge: 0}}, Binders: []extension13.PSKBinder{binder}}})
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight0, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.ErrorIs(t, err, dtlserrors.ErrMissingPSKKeyExchangeModesExtension)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.MissingExtension}, dtlsAlert)
+ assert.Zero(t, nextFlight)
+ })
+
+ t.Run("RejectsMissingSignatureAlgorithms", func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ cache := dtlsflight.NewCache()
+ exts := requiredClientHello13Extensions(t, cfg)[1:]
+ pushFlight13_0ClientHello(t, cache, cfg, exts)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight0, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.ErrorIs(t, err, dtlserrors.ErrMissingClientHelloExtension)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.MissingExtension}, dtlsAlert)
+ assert.Zero(t, nextFlight)
+ })
+
+ t.Run("RejectsSignatureAlgorithmsCertAsSubstitute", func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ cache := dtlsflight.NewCache()
+ exts := requiredClientHello13Extensions(t, cfg)[1:]
+ exts = append([]extension.Value{&extension.CertificateSignatureAlgorithms{Schemes: dtlsflight.SignatureSchemeIDs(cfg.LocalSignatureSchemes)}}, exts...)
+ pushFlight13_0ClientHello(t, cache, cfg, exts)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight0, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.ErrorIs(t, err, dtlserrors.ErrMissingClientHelloExtension)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.MissingExtension}, dtlsAlert)
+ assert.Zero(t, nextFlight)
+ })
+
+ t.Run("RejectsMissingSupportedGroups", func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ cache := dtlsflight.NewCache()
+ required := requiredClientHello13Extensions(t, cfg)
+ exts := []extension.Value{required[0], required[2], required[3]}
+ pushFlight13_0ClientHello(t, cache, cfg, exts)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight0, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+
+ require.ErrorIs(t, err, dtlserrors.ErrKeyShareWithoutSupportedGroups)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.MissingExtension}, dtlsAlert)
+ assert.Zero(t, nextFlight)
+ })
+}
+
+func TestFlight13ServerParseAppendsNoHRRTranscriptOrder(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ cfg.InsecureSkipHelloVerify = true
+ state := newTestState13(t, false)
+ cache := dtlsflight.NewCache()
+ rawClientHello := pushFlight13_0ClientHello(t, cache, cfg, requiredClientHello13Extensions(t, cfg))
+ clientHelloCanonical, err := canonicalHandshake13(rawClientHello)
+ require.NoError(t, err)
+ transcript := dtlshandshake.NewTranscript()
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight0, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg, transcript: transcript})
+
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight4, nextFlight)
+ assert.Equal(t, clientHelloCanonical, transcript.Bytes())
+}
+
+func TestFlight13ServerParseAppendsHRRTranscriptOrder(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ cookie := []byte{0xde, 0xad, 0xbe, 0xef}
+ state := newTestState13(t, false)
+ state.Cookie = cookie
+ cache := dtlsflight.NewCache()
+ rawClientHello1 := pushFlight13_0ClientHello(t, cache, cfg, requiredClientHello13Extensions(t, cfg))
+ clientHello1Canonical, err := canonicalHandshake13(rawClientHello1)
+ require.NoError(t, err)
+ transcript := dtlshandshake.NewTranscript()
+ flightCtx := &handshakeTestContext13{
+ state: state,
+ cache: cache,
+ cfg: cfg,
+ transcript: transcript,
+ }
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight0, context.Background(), flightCtx)
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight2, nextFlight)
+
+ helloRetryRequest, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight2, flightCtx)
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, helloRetryRequest, 1)
+ require.NoError(t, dtlshandshake.AppendOutboundHandshakeFlight(transcript, false, state.CipherSuite, helloRetryRequest))
+ helloRetryRequestCanonical := canonicalPacketHandshake13(t, helloRetryRequest[0])
+
+ retry, err := negotiation.BuildClientHelloRetry(state.RemoteClientHelloSnapshots.Initial(), state.HelloRetryRequest, nil)
+ require.NoError(t, err)
+ rawClientHello2, err := (&handshake.Handshake{
+ Header: handshake.Header{MessageSequence: 1}, Message: retry,
+ }).Marshal()
+ require.NoError(t, err)
+ cache.Push(rawClientHello2, 0, 1, handshake.TypeClientHello, true)
+ clientHello2Canonical, err := canonicalHandshake13(rawClientHello2)
+ require.NoError(t, err)
+
+ nextFlight, dtlsAlert, err = flight13ParseForTest(t, dtlsflight13.Flight2, context.Background(), flightCtx)
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight4, nextFlight)
+
+ clientHello1Hash := hashTranscript13(clientHello1Canonical)
+ messageHash := canonicalTranscriptHandshake13(handshake.TypeMessageHash, clientHello1Hash)
+ expectedTranscript := append(append(append([]byte(nil), messageHash...), helloRetryRequestCanonical...), clientHello2Canonical...)
+ assert.Equal(t, expectedTranscript, transcript.Bytes())
+}
+
+func serverHelloFromFlight13_2(t *testing.T, state *dtlsstate.State13, cfg *dtlsconfig.HandshakeConfig) *handshake.MessageServerHello {
+ t.Helper()
+
+ if state.CipherSuite == nil {
+ state.CipherSuite = cfg.LocalCipherSuites[0]
+ }
+ if state.SelectedGroup == 0 && state.Cookie == nil {
+ state.Cookie = []byte{0x01}
+ }
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight2, flight13_2Context(state, dtlsflight.NewCache(), cfg))
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, pkts, 1)
+
+ require.NotNil(t, pkts[0].Content)
+
+ content, ok := pkts[0].Content.(*handshake.Handshake)
+ require.True(t, ok)
+
+ serverHello, ok := content.Message.(*handshake.MessageServerHello)
+ require.True(t, ok)
+
+ return serverHello
+}
+
+func findSupportedVersions(exts []extension.Value) (*extension13.SelectedVersion, bool) {
+ for _, ext := range exts {
+ if typed, ok := ext.(*extension13.SelectedVersion); ok {
+ return typed, true
+ }
+ }
+
+ return nil, false
+}
+
+func findRetryKeyShare(exts []extension.Value) (*extension13.RetryKeyShare, bool) {
+ for _, ext := range exts {
+ if typed, ok := ext.(*extension13.RetryKeyShare); ok {
+ return typed, true
+ }
+ }
+
+ return nil, false
+}
+
+func findServerKeyShare(exts []extension.Value) (*extension13.ServerKeyShare, bool) {
+ for _, ext := range exts {
+ if typed, ok := ext.(*extension13.ServerKeyShare); ok {
+ return typed, true
+ }
+ }
+
+ return nil, false
+}
+
+func findCookie(exts []extension.Value) (*extension13.Cookie, bool) {
+ for _, ext := range exts {
+ if typed, ok := ext.(*extension13.Cookie); ok {
+ return typed, true
+ }
+ }
+
+ return nil, false
+}
+
+func TestFlight13_2Generate(t *testing.T) {
+ t.Run("ServerHelloIsHelloRetryRequest", func(t *testing.T) {
+ state := newTestState13(t, false)
+ cfg := testHandshakeConfig13(t)
+
+ serverHello := serverHelloFromFlight13_2(t, state, cfg)
+
+ assert.Equal(t, protocol.Version1_2, serverHello.Version)
+ assert.Equal(t, [32]byte(handshake.HelloRetryRequestRandom()), serverHello.Random.MarshalFixed())
+ })
+
+ t.Run("ResetsHandshakeSendSequence", func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ state.CipherSuite = cfg.LocalCipherSuites[0]
+ state.HandshakeSendSequence = 7
+ state.Cookie = []byte{0x01}
+
+ _, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight2, flight13_2Context(state, dtlsflight.NewCache(), cfg))
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+
+ assert.Equal(t, 0, state.HandshakeSendSequence)
+ })
+
+ t.Run("RejectsWithoutCipherSuite", func(t *testing.T) {
+ state := newTestState13(t, false)
+ cfg := testHandshakeConfig13(t)
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight2, flight13_2Context(state, dtlsflight.NewCache(), cfg))
+ require.ErrorIs(t, err, dtlserrors.ErrCipherSuiteUnset)
+ require.Nil(t, dtlsAlert)
+ require.Nil(t, pkts)
+ })
+
+ t.Run("AlwaysIncludesSupportedVersions", func(t *testing.T) {
+ state := newTestState13(t, false)
+ cfg := testHandshakeConfig13(t)
+
+ serverHello := serverHelloFromFlight13_2(t, state, cfg)
+
+ supportedVersions, ok := findSupportedVersions(serverHello.Extensions)
+ require.True(t, ok, "SupportedVersions extension must always be present")
+ assert.Equal(t, protocol.Version1_3, supportedVersions.Version)
+ })
+
+ t.Run("IncludesCipherSuiteAndCompressionMethod", func(t *testing.T) {
+ state := newTestState13(t, false)
+ cfg := testHandshakeConfig13(t)
+
+ serverHello := serverHelloFromFlight13_2(t, state, cfg)
+
+ require.NotNil(t, serverHello.CipherSuiteID)
+ assert.Equal(t, uint16(cfg.LocalCipherSuites[0].ID()), *serverHello.CipherSuiteID)
+ require.NotNil(t, serverHello.CompressionMethod)
+ assert.Equal(t, dtlsflight.DefaultCompressionMethods()[0], serverHello.CompressionMethod)
+
+ raw, err := (&handshake.Handshake{Message: serverHello}).Marshal()
+ require.NoError(t, err)
+
+ var parsed handshake.Handshake
+ require.NoError(t, parsed.Unmarshal(raw))
+ parsedServerHello, ok := parsed.Message.(*handshake.MessageServerHello)
+ require.True(t, ok)
+ require.NotNil(t, parsedServerHello.CipherSuiteID)
+ assert.Equal(t, *serverHello.CipherSuiteID, *parsedServerHello.CipherSuiteID)
+ })
+
+ t.Run("RejectsHelloRetryRequestWithNoEffect", func(t *testing.T) {
+ state := newTestState13(t, false)
+ cfg := testHandshakeConfig13(t)
+ state.CipherSuite = cfg.LocalCipherSuites[0]
+
+ packets, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight2, flight13_2Context(state, dtlsflight.NewCache(), cfg))
+ require.ErrorIs(t, err, dtlserrors.ErrInvalidHelloRetryRequest)
+ require.Nil(t, dtlsAlert)
+ require.Nil(t, packets)
+ })
+
+ t.Run("IncludesKeyShareWhenRemoteKeyEntriesPresent", func(t *testing.T) {
+ state := newTestState13(t, false)
+ state.SelectedGroup = elliptic.X25519
+ cfg := testHandshakeConfig13(t)
+
+ serverHello := serverHelloFromFlight13_2(t, state, cfg)
+
+ keyShare, ok := findRetryKeyShare(serverHello.Extensions)
+ require.True(t, ok, "KeyShare must be present when remote key entries were offered")
+ assert.Equal(t, elliptic.X25519, keyShare.SelectedGroup)
+ })
+
+ t.Run("OmitsKeyShareWhenSelectedGroupWasAlreadyOffered", func(t *testing.T) {
+ state := newTestState13(t, false)
+ state.SelectedGroup = elliptic.X25519
+ state.RemoteKeyEntries = []extension13.KeyShareEntry{{
+ Group: elliptic.X25519,
+ KeyExchange: []byte{0x01},
+ }}
+ state.HasRemoteKeyEntries = true
+ state.Cookie = []byte{0x01}
+ cfg := testHandshakeConfig13(t)
+
+ serverHello := serverHelloFromFlight13_2(t, state, cfg)
+
+ _, hasKeyShare := findRetryKeyShare(serverHello.Extensions)
+ assert.False(t, hasKeyShare, "KeyShare must be omitted when the selected group was already offered")
+ })
+
+ t.Run("IncludesCookieWhenSet", func(t *testing.T) {
+ cookie := []byte{0x01, 0x02, 0x03, 0x04}
+ state := newTestState13(t, false)
+ state.Cookie = cookie
+ cfg := testHandshakeConfig13(t)
+
+ serverHello := serverHelloFromFlight13_2(t, state, cfg)
+
+ cookieExt, ok := findCookie(serverHello.Extensions)
+ require.True(t, ok, "Cookie must be present when set on state")
+ assert.Equal(t, cookie, cookieExt.Cookie)
+ })
+
+ t.Run("IncludesAllExtensionsTogether", func(t *testing.T) {
+ cookie := []byte{0xaa, 0xbb}
+ state := newTestState13(t, false)
+ state.SelectedGroup = elliptic.P256
+ state.Cookie = cookie
+ cfg := testHandshakeConfig13(t)
+
+ serverHello := serverHelloFromFlight13_2(t, state, cfg)
+
+ require.Len(t, serverHello.Extensions, 3)
+
+ supportedVersions, ok := findSupportedVersions(serverHello.Extensions)
+ require.True(t, ok)
+ assert.Equal(t, protocol.Version1_3, supportedVersions.Version)
+
+ keyShare, ok := findRetryKeyShare(serverHello.Extensions)
+ require.True(t, ok)
+ assert.Equal(t, elliptic.P256, keyShare.SelectedGroup)
+
+ cookieExt, ok := findCookie(serverHello.Extensions)
+ require.True(t, ok)
+ assert.Equal(t, cookie, cookieExt.Cookie)
+ })
+}
+
+func TestFlight13_4Generate(t *testing.T) {
+ t.Run("RequestsClientCertificateWhenConfigured", func(t *testing.T) {
+ certificate, err := selfsign.GenerateSelfSigned()
+ require.NoError(t, err)
+ keypair, err := elliptic.GenerateKeypair(testCurves13[0])
+ require.NoError(t, err)
+
+ tests := map[string]struct {
+ clientAuth dtlsconfig.ClientAuthType
+ wantRequest bool
+ }{
+ "NoClientCert": {clientAuth: dtlsconfig.NoClientCert},
+ "RequestClientCert": {clientAuth: dtlsconfig.RequestClientCert, wantRequest: true},
+ "RequireAnyClientCert": {clientAuth: dtlsconfig.RequireAnyClientCert, wantRequest: true},
+ "VerifyClientCertIfGiven": {clientAuth: dtlsconfig.VerifyClientCertIfGiven, wantRequest: true},
+ "RequireAndVerifyClientCert": {clientAuth: dtlsconfig.RequireAndVerifyClientCert, wantRequest: true},
+ }
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ cfg.LocalCertificates = []tls.Certificate{certificate}
+ cfg.ClientAuth = test.clientAuth
+
+ state := newTestState13(t, false)
+ state.CipherSuite = cfg.LocalCipherSuites[0]
+ state.LocalKeypair = keypair
+ state.RemoteSignatureSchemes = append([]signaturehash.Algorithm(nil), cfg.LocalSignatureSchemes...)
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight4, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+
+ var certificateRequest *handshake.MessageCertificateRequest13
+ for _, pkt := range pkts {
+ handshakePacket, ok := pkt.Content.(*handshake.Handshake)
+ if !ok {
+ continue
+ }
+ request, ok := handshakePacket.Message.(*handshake.MessageCertificateRequest13)
+ if !ok {
+ continue
+ }
+ require.Nil(t, certificateRequest, "server flight contains multiple CertificateRequest messages")
+ certificateRequest = request
+ assert.Equal(t, dtlsflight13.EpochHandshake, pkt.Epoch)
+ assert.True(t, pkt.Protection == dtlsflight.ProtectionCiphertext)
+ }
+
+ if !test.wantRequest {
+ assert.Nil(t, certificateRequest)
+
+ return
+ }
+
+ require.NotNil(t, certificateRequest)
+ assert.Empty(t, certificateRequest.CertificateRequestContext)
+ require.Len(t, certificateRequest.Extensions, 1)
+ signatureAlgorithms, ok := certificateRequest.Extensions[0].(*extension.SignatureAlgorithms)
+ require.True(t, ok)
+ assert.Equal(t, dtlsflight.SignatureSchemeIDs(cfg.LocalSignatureSchemes), signatureAlgorithms.Schemes)
+ })
+ }
+ })
+
+ t.Run("GeneratesCertificateAuthenticatedServerFlight", func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ certificate, err := selfsign.GenerateSelfSigned()
+ require.NoError(t, err)
+ cfg.LocalCertificates = []tls.Certificate{certificate}
+ group := cfg.EllipticCurves[0]
+ keypair, err := elliptic.GenerateKeypair(group)
+ require.NoError(t, err)
+
+ state := newTestState13(t, false)
+ state.CipherSuite = cfg.LocalCipherSuites[0]
+ state.LocalKeypair = keypair
+ state.RemoteSignatureSchemes = append([]signaturehash.Algorithm(nil), cfg.LocalSignatureSchemes...)
+ state.LocalRandom = handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01, 0x02, 0x03}}
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight4, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, pkts, 5)
+ assert.Equal(t, uint64(0), pkts[0].Epoch)
+ assert.False(t, pkts[0].Protection == dtlsflight.ProtectionCiphertext)
+
+ serverHelloHandshake, ok := pkts[0].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ serverHello, ok := serverHelloHandshake.Message.(*handshake.MessageServerHello)
+ require.True(t, ok)
+ assert.Equal(t, protocol.Version1_2, serverHello.Version)
+ assert.Equal(t, state.LocalRandom, serverHello.Random)
+ require.NotNil(t, serverHello.CipherSuiteID)
+ assert.Equal(t, uint16(cfg.LocalCipherSuites[0].ID()), *serverHello.CipherSuiteID)
+
+ keyShare, ok := findServerKeyShare(serverHello.Extensions)
+ require.True(t, ok)
+ assert.Equal(t, group, keyShare.Share.Group)
+ assert.Equal(t, keypair.PublicKey, keyShare.Share.KeyExchange)
+
+ supportedVersions, ok := findSupportedVersions(serverHello.Extensions)
+ require.True(t, ok)
+ assert.Equal(t, protocol.Version1_3, supportedVersions.Version)
+
+ encryptedExtensionsHandshake, ok := pkts[1].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ assert.Equal(t, dtlsflight13.EpochHandshake, pkts[1].Epoch)
+ assert.True(t, pkts[1].Protection == dtlsflight.ProtectionCiphertext)
+ encryptedExtensions, ok := encryptedExtensionsHandshake.Message.(*handshake.MessageEncryptedExtensions)
+ require.True(t, ok)
+ assert.Empty(t, encryptedExtensions.Extensions)
+
+ certificateHandshake, ok := pkts[2].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ assert.Equal(t, dtlsflight13.EpochHandshake, pkts[2].Epoch)
+ assert.True(t, pkts[2].Protection == dtlsflight.ProtectionCiphertext)
+ certificateMessage, ok := certificateHandshake.Message.(*handshake.MessageCertificate13)
+ require.True(t, ok)
+ assert.Empty(t, certificateMessage.CertificateRequestContext)
+ require.Len(t, certificateMessage.CertificateList, len(certificate.Certificate))
+
+ certificateVerifyHandshake, ok := pkts[3].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ assert.Equal(t, dtlsflight13.EpochHandshake, pkts[3].Epoch)
+ assert.True(t, pkts[3].Protection == dtlsflight.ProtectionCiphertext)
+ certificateVerify, ok := certificateVerifyHandshake.Message.(*handshake.MessageCertificateVerify)
+ require.True(t, ok)
+ assert.Empty(t, certificateVerify.Signature)
+ assert.NotNil(t, pkts[3].CertificateVerifySigner)
+
+ finishedHandshake, ok := pkts[4].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ assert.Equal(t, dtlsflight13.EpochHandshake, pkts[4].Epoch)
+ assert.True(t, pkts[4].Protection == dtlsflight.ProtectionCiphertext)
+ _, ok = finishedHandshake.Message.(*handshake.MessageFinished)
+ require.True(t, ok)
+ })
+
+ t.Run("RejectsWithoutCipherSuite", func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight4, &handshakeTestContext13{state: state, cfg: cfg})
+ require.ErrorIs(t, err, dtlserrors.ErrCipherSuiteUnset)
+ require.Nil(t, dtlsAlert)
+ require.Nil(t, pkts)
+ })
+
+ t.Run("RejectsWithoutLocalKeypair", func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ state.CipherSuite = cfg.LocalCipherSuites[0]
+
+ pkts, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight4, &handshakeTestContext13{state: state, cfg: cfg})
+ require.ErrorIs(t, err, dtlserrors.ErrServerKeyShareMissing)
+ require.Nil(t, dtlsAlert)
+ require.Nil(t, pkts)
+ })
+}
+
+func pushClientHello13(
+ t *testing.T,
+ cache *dtlsflight.Cache,
+ version protocol.Version,
+ exts []extension.Value,
+) {
+ t.Helper()
+
+ pushClientHello13WithSequence(t, cache, version, 0, exts)
+}
+
+func pushClientHello13WithSequence(t *testing.T, cache *dtlsflight.Cache, version protocol.Version, seq uint16, exts []extension.Value) []byte {
+ t.Helper()
+
+ content := &handshake.Handshake{
+ Header: handshake.Header{MessageSequence: seq},
+ Message: &handshake.MessageClientHello{Version: version, Random: handshake.Random{}, CipherSuiteIDs: []uint16{uint16(cryptosuite.TLS_AES_128_GCM_SHA256)}, CompressionMethods: dtlsflight.DefaultCompressionMethods(), Extensions: exts},
+ }
+
+ raw, err := content.Marshal()
+ require.NoError(t, err)
+
+ cache.Push(raw, 0, seq, handshake.TypeClientHello, true)
+
+ return raw
+}
+
+func flight13_2Context(state *dtlsstate.State13, cache *dtlsflight.Cache, cfg *dtlsconfig.HandshakeConfig) *handshakeTestContext13 {
+ seedFlight13RetryRequest(state, cache, cfg)
+
+ return &handshakeTestContext13{
+ state: state,
+ cache: cache,
+ cfg: cfg,
+ transcript: dtlshandshake.NewTranscript(),
+ }
+}
+
+func seedFlight13RetryRequest(
+ state *dtlsstate.State13,
+ cache *dtlsflight.Cache,
+ cfg *dtlsconfig.HandshakeConfig,
+) {
+ if request := state.HelloRetryRequest; request.HasCookie || request.HasSelectedGroup {
+ return
+ }
+ pull := cache.FullPullMapItems(state.HandshakeRecvSequence, state.CipherSuite, dtlsflight.HandshakeCachePullRule{Typ: handshake.TypeClientHello, Epoch: cfg.InitialEpoch, IsClient: true})
+ retry, ok := pull.Messages[handshake.TypeClientHello].(*handshake.MessageClientHello)
+ if pull.Err != nil || !pull.Ready || !ok {
+ return
+ }
+ clientHello := *retry
+ clientHello.Extensions = slices.DeleteFunc(slices.Clone(retry.Extensions), func(value extension.Value) bool { return value.ExtensionType() == extension.TypeCookie })
+ _, initial, err := negotiation.FinalizeClientHello(&clientHello, nil)
+ if err != nil {
+ return
+ }
+ state.RemoteClientHelloSnapshots.Reset()
+ if recordErr := state.RemoteClientHelloSnapshots.Record(initial); recordErr != nil {
+ return
+ }
+ id := uint16(cfg.LocalCipherSuites[0].ID())
+ extensions := []extension.Value{&extension13.SelectedVersion{Version: protocol.Version1_3}, &extension13.Cookie{Cookie: state.Cookie}}
+ request, err := negotiation.ValidateHelloRetryRequest(initial, &handshake.MessageServerHello{CipherSuiteID: &id, Extensions: extensions})
+ if err == nil {
+ state.HelloRetryRequest = request
+ }
+}
+
+func TestFlight13_2Parse(t *testing.T) {
+ cookie := []byte{0xde, 0xad, 0xbe, 0xef}
+
+ t.Run("AdvancesToFlight4OnMatchingCookie", func(t *testing.T) {
+ state := newTestState13(t, false)
+ state.Cookie = cookie
+ cache := dtlsflight.NewCache()
+ cfg := testHandshakeConfig13(t)
+
+ exts := append(requiredClientHello13Extensions(t, cfg), &extension13.Cookie{Cookie: cookie})
+ pushClientHello13(t, cache, protocol.Version1_2, exts)
+
+ next, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight2, context.Background(), flight13_2Context(state, cache, cfg))
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight4, next)
+ assert.Equal(t, 1, state.HandshakeRecvSequence)
+ })
+
+ t.Run("GeneratesX25519MLKEM768KeypairAfterMatchingCookie", func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ cfg.EllipticCurves = []elliptic.Curve{elliptic.X25519MLKEM768}
+ clientKeypair, err := elliptic.GenerateKeypair(elliptic.X25519MLKEM768)
+ require.NoError(t, err)
+
+ state := newTestState13(t, false)
+ state.Cookie = cookie
+ cache := dtlsflight.NewCache()
+ pushClientHello13(t, cache, protocol.Version1_2, []extension.Value{
+ &extension.SignatureAlgorithms{Schemes: dtlsflight.SignatureSchemeIDs(cfg.LocalSignatureSchemes)},
+ &extension.SupportedGroups{Groups: cfg.EllipticCurves},
+ &extension13.ClientKeyShare{
+ Shares: []extension13.KeyShareEntry{
+ {Group: elliptic.X25519MLKEM768, KeyExchange: clientKeypair.PublicKey},
+ },
+ },
+ &extension13.OfferedVersions{Versions: []protocol.Version{protocol.Version1_3}},
+ &extension13.Cookie{Cookie: cookie},
+ })
+
+ next, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight2, context.Background(), flight13_2Context(state, cache, cfg))
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight4, next)
+ require.NotNil(t, state.LocalKeypair)
+ assert.Equal(t, elliptic.X25519MLKEM768, state.SelectedGroup)
+ assert.Equal(t, elliptic.X25519MLKEM768, state.LocalKeypair.Curve)
+ assert.Len(t, state.LocalKeypair.PublicKey, elliptic.X25519MLKEM768ServerPublicKeySize)
+
+ clientSecret, err := prf.PreMasterSecret(state.LocalKeypair.PublicKey, clientKeypair.PrivateKey, elliptic.X25519MLKEM768)
+ require.NoError(t, err)
+ assert.Equal(t, clientSecret, state.KeyAgreementSecret)
+ assert.Len(t, state.KeyAgreementSecret, elliptic.X25519MLKEM768SharedSecretSize)
+ })
+
+ t.Run("RejectsUnsupportedSupportedGroupsAfterMatchingCookie", func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ cfg.EllipticCurves = []elliptic.Curve{elliptic.P256}
+ clientKeypair, err := elliptic.GenerateKeypair(elliptic.P384)
+ require.NoError(t, err)
+
+ state := newTestState13(t, false)
+ state.Cookie = cookie
+ cache := dtlsflight.NewCache()
+ pushClientHello13(t, cache, protocol.Version1_2, []extension.Value{
+ &extension.SignatureAlgorithms{Schemes: dtlsflight.SignatureSchemeIDs(cfg.LocalSignatureSchemes)},
+ &extension.SupportedGroups{Groups: []elliptic.Curve{elliptic.P384}},
+ &extension13.ClientKeyShare{
+ Shares: []extension13.KeyShareEntry{
+ {Group: elliptic.P384, KeyExchange: clientKeypair.PublicKey},
+ },
+ },
+ &extension13.OfferedVersions{Versions: []protocol.Version{protocol.Version1_3}},
+ &extension13.Cookie{Cookie: cookie},
+ })
+
+ next, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight2, context.Background(), flight13_2Context(state, cache, cfg))
+ require.ErrorIs(t, err, dtlserrors.ErrNoSupportedEllipticCurves)
+ assert.Equal(t, dtlsflight13.Flight(0), next)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlsAlert)
+ assert.Empty(t, state.KeyAgreementSecret)
+ assert.Nil(t, state.LocalKeypair)
+ assert.Zero(t, state.SelectedGroup)
+ assert.False(t, state.RemoteClientHelloSnapshots.Current().Offered(extension.TypeCookie))
+ })
+
+ t.Run("KeepsWaitingWhenNoClientHelloCached", func(t *testing.T) {
+ state := newTestState13(t, false)
+ state.Cookie = cookie
+ cache := dtlsflight.NewCache()
+ cfg := testHandshakeConfig13(t)
+
+ next, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight2, context.Background(), flight13_2Context(state, cache, cfg))
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight(0), next)
+ assert.Equal(t, 0, state.HandshakeRecvSequence)
+ })
+
+ t.Run("RejectsMissingCookie", func(t *testing.T) {
+ state := newTestState13(t, false)
+ state.Cookie = cookie
+ state.ServerName = "original.example"
+ cache := dtlsflight.NewCache()
+ cfg := testHandshakeConfig13(t)
+
+ exts := append(requiredClientHello13Extensions(t, cfg), &extension.ServerNameOffer{ServerName: "poison.example"})
+ pushClientHello13(t, cache, protocol.Version1_2, exts)
+
+ next, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight2, context.Background(), flight13_2Context(state, cache, cfg))
+ require.ErrorIs(t, err, dtlserrors.ErrCookieMismatch)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight(0), next)
+ assert.Equal(t, 0, state.HandshakeRecvSequence)
+ assert.Equal(t, "original.example", state.ServerName)
+ assert.Empty(t, state.RemoteSignatureSchemes)
+ assert.Empty(t, state.RemoteGroups)
+ })
+
+ t.Run("RejectsCookieMismatch", func(t *testing.T) {
+ state := newTestState13(t, false)
+ state.Cookie = cookie
+ state.ServerName = "original.example"
+ cache := dtlsflight.NewCache()
+ cfg := testHandshakeConfig13(t)
+
+ exts := append(requiredClientHello13Extensions(t, cfg), &extension.ServerNameOffer{ServerName: "poison.example"}, &extension13.Cookie{Cookie: []byte{0x00, 0x01, 0x02, 0x03}})
+ pushClientHello13(t, cache, protocol.Version1_2, exts)
+
+ next, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight2, context.Background(), flight13_2Context(state, cache, cfg))
+ require.ErrorIs(t, err, dtlserrors.ErrCookieMismatch)
+ assert.Equal(t, dtlsflight13.Flight(0), next)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, dtlsAlert)
+ assert.Equal(t, 0, state.HandshakeRecvSequence)
+ assert.Equal(t, "original.example", state.ServerName)
+ assert.Empty(t, state.RemoteSignatureSchemes)
+ assert.Empty(t, state.RemoteGroups)
+ })
+
+ for _, test := range []struct {
+ name string
+ version protocol.Version
+ err error
+ alert alert.Description
+ }{
+ {"RejectsUnsupportedVersion", protocol.Version1_3, dtlserrors.ErrUnsupportedProtocolVersion, alert.ProtocolVersion},
+ {"RejectsMissingCertificateAuthExtensions", protocol.Version1_2, dtlserrors.ErrMissingClientHelloExtension, alert.MissingExtension},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ state := newTestState13(t, false)
+ state.Cookie = cookie
+ cache := dtlsflight.NewCache()
+ cfg := testHandshakeConfig13(t)
+ pushClientHello13(t, cache, test.version, []extension.Value{&extension13.Cookie{Cookie: cookie}})
+
+ next, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight2, context.Background(), flight13_2Context(state, cache, cfg))
+ require.ErrorIs(t, err, test.err)
+ assert.Equal(t, dtlsflight13.Flight(0), next)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: test.alert}, dtlsAlert)
+ })
+ }
+}
+
+func findConnectionID(exts []extension.Value) (*extension.ConnectionID, bool) {
+ for _, ext := range exts {
+ if typed, ok := ext.(*extension.ConnectionID); ok {
+ return typed, true
+ }
+ }
+
+ return nil, false
+}
+
+func clientHello13SnapshotHistory(t *testing.T, extensions []extension.Value) (history negotiation.ClientHelloSnapshots) {
+ t.Helper()
+ _, snapshot, err := negotiation.FinalizeClientHello(&handshake.MessageClientHello{
+ Version: protocol.Version1_2, CipherSuiteIDs: []uint16{uint16(cryptosuite.TLS_AES_128_GCM_SHA256)},
+ CompressionMethods: dtlsflight.DefaultCompressionMethods(),
+ Extensions: extensions,
+ }, nil)
+ require.NoError(t, err)
+ require.NoError(t, history.Record(snapshot))
+
+ return history
+}
+
+func assertSnapshotConnectionID(t *testing.T, snapshot negotiation.ClientHelloSnapshot, expectedCID []byte, expectedPresent bool) {
+ t.Helper()
+ cid, present := negotiation.ConnectionIDOffer(snapshot)
+ assert.Equal(t, expectedPresent, present)
+ assert.True(t, bytes.Equal(expectedCID, cid))
+}
+
+func assertConnectionIDValue(t *testing.T, expected, actual []byte) {
+ t.Helper()
+ assert.True(t, bytes.Equal(expected, actual))
+}
+
+func assertConnectionIDs(t *testing.T, state *dtlsstate.State13, localCID, remoteCID []byte, negotiated bool) {
+ t.Helper()
+ assert.Equal(t, [3]bool{negotiated, negotiated, negotiated}, [3]bool{state.LocalCIDOffered, state.RemoteCIDOffered, state.CID.Negotiated})
+ assertConnectionIDValue(t, localCID, state.LocalConnectionID())
+ assertConnectionIDValue(t, remoteCID, state.RemoteConnectionID)
+ if !negotiated {
+ assert.Zero(t, state.CID)
+
+ return
+ }
+ assertConnectionIDValue(t, localCID, state.LocalConnectionIDForInboundRecords())
+ assert.Equal(t, len(localCID), state.CID.Receive.Length)
+ assert.Equal(t, len(localCID) > 0, state.CID.Receive.Expected)
+ assert.Equal(t, len(localCID) > 0, state.CID.Receive.CanSendNewConnectionID)
+ assert.Equal(t, len(remoteCID) > 0, state.CID.Send.UseCID)
+ assertConnectionIDValue(t, remoteCID, state.CID.Send.Active)
+}
+
+func clientHelloFromFlight13Packet(t *testing.T, packet *dtlsflight.Outbound) *handshake.MessageClientHello {
+ t.Helper()
+
+ hand, ok := packet.Content.(*handshake.Handshake)
+ require.True(t, ok)
+ raw, err := hand.Marshal()
+ require.NoError(t, err)
+
+ var parsed handshake.Handshake
+ require.NoError(t, parsed.Unmarshal(raw))
+ clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
+ require.True(t, ok)
+
+ return clientHello
+}
+
+func setConnectionIDs(clientHello *handshake.MessageClientHello, cids ...[]byte) {
+ extensions := clientHello.Extensions[:0]
+ for _, ext := range clientHello.Extensions {
+ _, isConnectionID := ext.(*extension.ConnectionID)
+ _, isRRC := ext.(*extension.ReturnRoutabilityCheck)
+ if !isConnectionID && (!isRRC || len(cids) != 0) {
+ extensions = append(extensions, ext)
+ }
+ }
+ for _, cid := range cids {
+ extensions = append(extensions, &extension.ConnectionID{CID: cid})
+ }
+ clientHello.Extensions = extensions
+}
+
+type connectionIDNegotiationCase struct {
+ clientCID []byte
+ serverCIDs [][]byte
+ clientOffers bool
+ expectedErr error
+ description alert.Description
+}
+
+func connectionIDNegotiationCases() map[string]connectionIDNegotiationCase {
+ return map[string]connectionIDNegotiationCase{
+ "ServerDeclinesEmpty": {clientOffers: true},
+ "ServerDeclinesNonEmpty": {clientOffers: true, clientCID: []byte{1}},
+ "BothEmpty": {clientOffers: true, serverCIDs: [][]byte{{}}},
+ "OnlyClientSendsCID": {clientOffers: true, serverCIDs: [][]byte{{0x10, 0x11}}},
+ "OnlyServerSendsCID": {clientOffers: true, clientCID: []byte{1, 2}, serverCIDs: [][]byte{{}}},
+ "Bidirectional": {clientOffers: true, clientCID: []byte{1, 2}, serverCIDs: [][]byte{{0x10, 0x11}}},
+ }
+}
+
+func TestFlight13_1GenerateConnectionIDOffer(t *testing.T) {
+ tests := map[string]struct {
+ generated, expected []byte
+ hooked [][]byte
+ generator, hook bool
+ invalid bool
+ }{
+ "Disabled": {},
+ "Empty": {generator: true},
+ "NonEmpty": {generated: []byte{1, 2, 3, 4}, expected: []byte{1, 2, 3, 4}, generator: true},
+ "HookRemove": {generated: []byte{1, 2}, generator: true, hook: true},
+ "HookReplace": {generated: []byte{1, 2}, expected: []byte{0xaa, 0xbb}, hooked: [][]byte{{0xaa, 0xbb}}, generator: true, hook: true},
+ "HookAdd": {expected: []byte{0xcc}, hooked: [][]byte{{0xcc}}, hook: true},
+ "HookDuplicate": {generated: []byte{1}, hooked: [][]byte{{1}, {2}}, generator: true, hook: true, invalid: true},
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ cfg, calls := testHandshakeConfig13(t), 0
+ cfg.EnableRRC = true
+ if test.generator {
+ cfg.ReceiveCIDLength = len(test.generated)
+ cfg.ConnectionIDGenerator = func() []byte {
+ calls++
+
+ return test.generated
+ }
+ }
+ if test.hook {
+ cfg.ClientHelloMessageHook = func(ch handshake.MessageClientHello) handshake.Message {
+ setConnectionIDs(&ch, test.hooked...)
+
+ return &ch
+ }
+ }
+ state := newTestState13(t, true)
+ if test.invalid {
+ state.LocalClientHelloSnapshots.Reset()
+ }
+ packets, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+ if test.invalid {
+ require.ErrorIs(t, err, dtlserrors.ErrInvalidClientHello)
+ assert.Nil(t, dtlsAlert)
+ assert.Nil(t, packets)
+ assert.False(t, state.LocalClientHelloSnapshots.Current().Valid())
+ } else {
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, packets, 1)
+ clientHelloExtensions := clientHelloFromFlight13Packet(t, packets[0]).Extensions
+ cid, present := findConnectionID(clientHelloExtensions)
+ expectedPresent := test.generator
+ if test.hook {
+ expectedPresent = len(test.hooked) == 1
+ }
+ assert.Equal(t, expectedPresent, present)
+ rrcPresent := slices.ContainsFunc(clientHelloExtensions, func(value extension.Value) bool { return value.ExtensionType() == extension.TypeReturnRoutabilityCheck })
+ assert.Equal(t, test.generator && expectedPresent, rrcPresent)
+ assertSnapshotConnectionID(t, state.LocalClientHelloSnapshots.Current(), test.expected, present)
+ assertConnectionIDValue(t, test.expected, state.LocalConnectionIDForInboundRecords())
+ if present {
+ require.NotNil(t, cid)
+ assertConnectionIDValue(t, test.expected, cid.CID)
+ }
+ }
+ assert.Equal(t, test.generator, calls == 1)
+ assertConnectionIDs(t, state, nil, nil, false)
+ })
+ }
+}
+
+func TestFlight13_1GenerateRejectsConnectionIDLength(t *testing.T) {
+ for name, hook := range map[string]bool{"generator": false, "hook": true} {
+ t.Run(name, func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ cfg.ReceiveCIDLength = 2
+ generated := []byte{0x01}
+ if hook {
+ generated = append(generated, 0x02)
+ cfg.ClientHelloMessageHook = func(clientHello handshake.MessageClientHello) handshake.Message {
+ setConnectionIDs(&clientHello, []byte{0xff})
+
+ return &clientHello
+ }
+ }
+ cfg.ConnectionIDGenerator = func() []byte { return generated }
+ state := newTestState13(t, true)
+
+ packets, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+ require.ErrorIs(t, err, dtlserrors.ErrInvalidConnectionIDLength)
+ assert.Nil(t, dtlsAlert)
+ assert.Nil(t, packets)
+ assert.False(t, state.LocalClientHelloSnapshots.Current().Valid())
+ })
+ }
+}
+
+func TestFlight13_3GenerateConnectionIDOffer(t *testing.T) { //nolint:cyclop // Compact scenario table.
+ tests := map[string]struct {
+ generated, expected []byte
+ retryCIDs [][]byte
+ generator, hookAll bool
+ mutateRetry, invalid bool
+ }{
+ "ReuseEmpty": {generator: true},
+ "ReuseNonEmpty": {generated: []byte{1, 2, 3, 4}, expected: []byte{1, 2, 3, 4}, generator: true},
+ "ReuseHookOnly": {expected: []byte{0xcc}, retryCIDs: [][]byte{{0xcc}}, hookAll: true},
+ "RejectRemove": {generated: []byte{1}, generator: true, mutateRetry: true, invalid: true},
+ "RejectReplace": {generated: []byte{1}, retryCIDs: [][]byte{{0xff}}, generator: true, mutateRetry: true, invalid: true},
+ "RejectAdd": {retryCIDs: [][]byte{{0xff}}, mutateRetry: true, invalid: true},
+ "RejectDuplicate": {generated: []byte{1}, retryCIDs: [][]byte{{1}, {1}}, generator: true, mutateRetry: true, invalid: true},
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ cfg, generatorCalls, hookCalls := testHandshakeConfig13(t), 0, 0
+ if test.generator {
+ cfg.ReceiveCIDLength = len(test.generated)
+ cfg.ConnectionIDGenerator = func() []byte {
+ generatorCalls++
+ if generatorCalls == 1 {
+ return test.generated
+ }
+
+ return []byte{0xff}
+ }
+ }
+ if test.hookAll || test.mutateRetry {
+ cfg.ClientHelloMessageHook = func(ch handshake.MessageClientHello) handshake.Message {
+ hookCalls++
+ if test.hookAll || hookCalls == 2 {
+ setConnectionIDs(&ch, test.retryCIDs...)
+ }
+
+ return &ch
+ }
+ }
+ state := newTestState13(t, true)
+ firstFlight, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, firstFlight, 1)
+ initialSnapshot := state.LocalClientHelloSnapshots.Current()
+ state.RemoteVersions, state.Cookie = []protocol.Version{protocol.Version1_3}, []byte{0xaa}
+ state.HelloRetryRequest = retryRequestForTest(t, state, cfg, initialSnapshot, 0)
+
+ secondFlight, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight3, &handshakeTestContext13{state: state, cfg: cfg})
+ assert.Equal(t, test.generator, generatorCalls == 1)
+ if test.hookAll || test.mutateRetry {
+ assert.Equal(t, 2, hookCalls)
+ }
+ if test.invalid {
+ require.ErrorIs(t, err, dtlserrors.ErrInvalidClientHello)
+ assert.Nil(t, dtlsAlert)
+ assert.Nil(t, secondFlight)
+ assertSnapshotConnectionID(t, initialSnapshot, test.generated, test.generator)
+ assertSnapshotConnectionID(t, state.LocalClientHelloSnapshots.Current(), test.generated, test.generator)
+ } else {
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.Len(t, secondFlight, 1)
+ firstCID, firstPresent := findConnectionID(clientHelloFromFlight13Packet(t, firstFlight[0]).Extensions)
+ secondCID, secondPresent := findConnectionID(clientHelloFromFlight13Packet(t, secondFlight[0]).Extensions)
+ require.True(t, firstPresent)
+ require.True(t, secondPresent)
+ assert.Equal(t, firstCID.CID, secondCID.CID)
+ assertConnectionIDValue(t, test.expected, secondCID.CID)
+ assertSnapshotConnectionID(t, state.LocalClientHelloSnapshots.Current(), test.expected, true)
+ }
+ assertConnectionIDs(t, state, nil, nil, false)
+ })
+ }
+}
+
+func TestFlight13_2ParseConnectionIDOffer(t *testing.T) {
+ cookie := []byte{0xde, 0xad, 0xbe, 0xef}
+ tests := map[string]struct {
+ firstPresent bool
+ firstCID []byte
+ secondCIDs [][]byte
+ expectedErr error
+ }{
+ "RejectRemovedEmpty": {firstPresent: true, firstCID: []byte{}, expectedErr: dtlserrors.ErrInvalidClientHello},
+ "RejectAddedEmpty": {secondCIDs: [][]byte{{}}, expectedErr: dtlserrors.ErrInvalidClientHello},
+ "RejectChangedValue": {firstPresent: true, firstCID: []byte{1}, secondCIDs: [][]byte{{2}}, expectedErr: dtlserrors.ErrInvalidClientHello},
+ "RejectDuplicate": {firstPresent: true, firstCID: []byte{1}, secondCIDs: [][]byte{{1}, {1}}, expectedErr: dtlserrors.ErrDuplicateExtension},
+ "RepeatEmpty": {firstPresent: true, firstCID: []byte{}, secondCIDs: [][]byte{{}}},
+ "RepeatNonEmpty": {firstPresent: true, firstCID: []byte{1, 2}, secondCIDs: [][]byte{{1, 2}}},
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ state := newTestState13(t, false)
+ state.Cookie = cookie
+ initialExtensions := requiredClientHello13Extensions(t, cfg)
+ if test.firstPresent {
+ initialExtensions = append(initialExtensions, &extension.ConnectionID{CID: test.firstCID})
+ }
+ state.RemoteClientHelloSnapshots = clientHello13SnapshotHistory(t, initialExtensions)
+ state.HelloRetryRequest = retryRequestForTest(
+ t, state, cfg, state.RemoteClientHelloSnapshots.Initial(), 0,
+ )
+ cache := dtlsflight.NewCache()
+ initial, err := negotiation.ClientHelloFromSnapshot(state.RemoteClientHelloSnapshots.Initial())
+ require.NoError(t, err)
+ exts := slices.DeleteFunc(slices.Clone(initial.Extensions), func(value extension.Value) bool { return value.ExtensionType() == extension.TypeConnectionID })
+ for _, cid := range test.secondCIDs {
+ exts = append(exts, &extension.ConnectionID{CID: cid})
+ }
+ exts = append(exts, &extension13.Cookie{Cookie: cookie})
+ pushClientHello13(t, cache, protocol.Version1_2, exts)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight2, context.Background(), flight13_2Context(state, cache, cfg))
+
+ if test.expectedErr != nil {
+ require.ErrorIs(t, err, test.expectedErr)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, dtlsAlert)
+ assert.Zero(t, nextFlight)
+ assert.Nil(t, state.LocalKeypair)
+ } else {
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight4, nextFlight)
+ }
+ assertSnapshotConnectionID(
+ t, state.RemoteClientHelloSnapshots.Current(), test.firstCID, test.firstPresent,
+ )
+ assertConnectionIDs(t, state, nil, nil, false)
+ })
+ }
+}
+
+func TestFlight13_4GenerateNegotiatesConnectionIDs(t *testing.T) { //nolint:cyclop // Compact scenario matrix.
+ tests := connectionIDNegotiationCases()
+ tests["NoClientOffer"] = connectionIDNegotiationCase{serverCIDs: [][]byte{{0x10}}}
+ tests["InvalidServerCID"] = connectionIDNegotiationCase{clientOffers: true, serverCIDs: [][]byte{make([]byte, 256)}}
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ var serverCID []byte
+ if len(test.serverCIDs) > 0 {
+ serverCID = test.serverCIDs[0]
+ }
+ cfg := testHandshakeConfig13(t)
+ cfg.EnableRRC = true
+ certificate, err := selfsign.GenerateSelfSigned()
+ require.NoError(t, err)
+ cfg.LocalCertificates = []tls.Certificate{certificate}
+ generatorCalls := 0
+ if len(test.serverCIDs) > 0 {
+ cfg.ReceiveCIDLength = len(serverCID)
+ cfg.ConnectionIDGenerator = func() []byte {
+ generatorCalls++
+
+ return serverCID
+ }
+ }
+ keypair, err := elliptic.GenerateKeypair(cfg.EllipticCurves[0])
+ require.NoError(t, err)
+ state := newTestState13(t, false)
+ state.CipherSuite = cfg.LocalCipherSuites[0]
+ state.LocalKeypair = keypair
+ state.RemoteSignatureSchemes = append([]signaturehash.Algorithm(nil), cfg.LocalSignatureSchemes...)
+ offerExtensions := requiredClientHello13Extensions(t, cfg)
+ if test.clientOffers {
+ offerExtensions = append(offerExtensions, &extension.ConnectionID{CID: test.clientCID}, &extension.ReturnRoutabilityCheck{})
+ }
+ state.RemoteClientHelloSnapshots = clientHello13SnapshotHistory(t, offerExtensions)
+
+ if len(serverCID) > 255 {
+ packets, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight4, &handshakeTestContext13{state: state, cfg: cfg})
+ require.ErrorIs(t, err, dtlserrors.ErrInvalidCIDFormat)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, dtlsAlert)
+ assert.Nil(t, packets)
+ assert.Equal(t, 1, generatorCalls)
+ assertConnectionIDs(t, state, nil, nil, false)
+
+ return
+ }
+
+ expectedNegotiated := test.clientOffers && len(test.serverCIDs) > 0
+ for range 2 {
+ packets, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight4, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ require.NotEmpty(t, packets)
+ serverHelloHandshake, ok := packets[0].Content.(*handshake.Handshake)
+ require.True(t, ok)
+ serverHello, ok := serverHelloHandshake.Message.(*handshake.MessageServerHello)
+ require.True(t, ok)
+ connectionID, present := findConnectionID(serverHello.Extensions)
+ assert.Equal(t, expectedNegotiated, present)
+ assert.Equal(t, expectedNegotiated, state.CID.Negotiated)
+ assert.Equal(t, expectedNegotiated, state.RRCNegotiated)
+ if expectedNegotiated {
+ require.NotNil(t, connectionID)
+ if len(serverCID) == 0 {
+ assert.Empty(t, connectionID.CID)
+ } else {
+ assert.Equal(t, serverCID, connectionID.CID)
+ }
+ }
+ }
+ assertSnapshotConnectionID(
+ t, state.RemoteClientHelloSnapshots.Current(), test.clientCID, test.clientOffers,
+ )
+ if !expectedNegotiated {
+ assert.Zero(t, generatorCalls)
+ assertConnectionIDs(t, state, nil, nil, false)
+
+ return
+ }
+
+ assert.Equal(t, 1, generatorCalls)
+ assertConnectionIDs(t, state, serverCID, test.clientCID, true)
+ })
+ }
+}
+
+func TestFlight13_0ParseConnectionIDOffer(t *testing.T) {
+ tests := map[string]struct {
+ cids [][]byte
+ expected []byte
+ invalid bool
+ }{
+ "Absent": {},
+ "Empty": {cids: [][]byte{{}}},
+ "NonEmpty": {cids: [][]byte{{1, 2, 3, 4}}, expected: []byte{1, 2, 3, 4}},
+ "Duplicate": {cids: [][]byte{{1}, {2}}, invalid: true},
+ }
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ cfg := testHandshakeConfig13(t)
+ generatorCalls := 0
+ cfg.ConnectionIDGenerator = func() []byte {
+ generatorCalls++
+
+ return []byte{0xaa}
+ }
+ state := newTestState13(t, false)
+ if test.invalid {
+ state.RemoteClientHelloSnapshots.Reset()
+ }
+ cache := dtlsflight.NewCache()
+ exts := requiredClientHello13Extensions(t, cfg)
+ for _, cid := range test.cids {
+ exts = append(exts, &extension.ConnectionID{CID: cid})
+ }
+ pushFlight13_0ClientHello(t, cache, cfg, exts)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight0, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+ if test.invalid {
+ require.ErrorIs(t, err, dtlserrors.ErrDuplicateExtension)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, dtlsAlert)
+ assert.Zero(t, nextFlight)
+ assert.False(t, state.RemoteClientHelloSnapshots.Current().Valid())
+ } else {
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ assert.Equal(t, dtlsflight13.Flight2, nextFlight)
+ present := len(test.cids) == 1
+ assertSnapshotConnectionID(t, state.RemoteClientHelloSnapshots.Current(), test.expected, present)
+ }
+ assert.Zero(t, generatorCalls, "server CID generation is deferred until the final ServerHello")
+ assertConnectionIDs(t, state, nil, nil, false)
+ })
+ }
+}
+
+func parseFlight13ServerHelloConnectionID(t *testing.T, clientOffers bool, clientCID []byte, serverCIDs [][]byte) (*dtlsstate.State13, dtlsflight13.Flight, *alert.Alert, error) {
+ t.Helper()
+
+ cfg := testHandshakeConfig13(t)
+ if clientOffers {
+ cfg.ReceiveCIDLength = len(clientCID)
+ cfg.ConnectionIDGenerator = func() []byte {
+ return clientCID
+ }
+ }
+ state := newTestState13(t, true)
+ transcript := dtlshandshake.NewTranscript()
+ clientHello, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ appended, err := dtlshandshake.AppendClientHelloInitialFlights(transcript, clientHello)
+ require.NoError(t, err)
+ require.True(t, appended)
+
+ group := cfg.EllipticCurves[0]
+ serverKeypair, err := elliptic.GenerateKeypair(group)
+ require.NoError(t, err)
+ extensions := []extension.Value{&extension13.SelectedVersion{Version: protocol.Version1_3}, &extension13.ServerKeyShare{Share: extension13.KeyShareEntry{Group: group, KeyExchange: serverKeypair.PublicKey}}}
+ for _, cid := range serverCIDs {
+ extensions = append(extensions, &extension.ConnectionID{CID: cid})
+ }
+ rawServerHello := marshalServerHello(t, cfg, handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}}, extensions)
+ cache := dtlsflight.NewCache()
+ cache.Push(rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight3, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg, transcript: transcript})
+
+ return state, nextFlight, dtlsAlert, err
+}
+
+func TestFlight13_3ParseConnectionID(t *testing.T) {
+ tests := connectionIDNegotiationCases()
+ tests["UnsolicitedEmpty"] = connectionIDNegotiationCase{serverCIDs: [][]byte{{}}, expectedErr: dtlserrors.ErrUnsolicitedExtension, description: alert.UnsupportedExtension}
+ tests["UnsolicitedNonEmpty"] = connectionIDNegotiationCase{serverCIDs: [][]byte{{0x10, 0x11}}, expectedErr: dtlserrors.ErrUnsolicitedExtension, description: alert.UnsupportedExtension}
+ tests["Duplicate"] = connectionIDNegotiationCase{clientOffers: true, clientCID: []byte{1}, serverCIDs: [][]byte{{0x10}, {0x11}}, expectedErr: dtlserrors.ErrDuplicateExtension, description: alert.IllegalParameter}
+
+ for name, test := range tests {
+ t.Run(name, func(t *testing.T) {
+ state, nextFlight, dtlsAlert, err := parseFlight13ServerHelloConnectionID(t, test.clientOffers, test.clientCID, test.serverCIDs)
+ assert.Zero(t, nextFlight, "the parser should wait for the protected remainder of Flight 3")
+ if test.expectedErr != nil {
+ require.ErrorIs(t, err, test.expectedErr)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: test.description}, dtlsAlert)
+ assertConnectionIDs(t, state, nil, nil, false)
+
+ return
+ }
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ if len(test.serverCIDs) == 0 {
+ assertConnectionIDs(t, state, nil, nil, false)
+ } else {
+ assertConnectionIDs(t, state, test.clientCID, test.serverCIDs[0], true)
+ }
+ })
+ }
+}
+
+func TestFlight13_1ParseRejectsHelloRetryRequestExtension(t *testing.T) {
+ for name, test := range map[string]struct {
+ extension extension.Value
+ expectedErr error
+ description alert.Description
+ isClient bool
+ generate bool
+ }{
+ "ConnectionID": {extension: &extension.ConnectionID{}, expectedErr: dtlserrors.ErrExtensionNotAllowed, description: alert.IllegalParameter, isClient: true},
+ "Unsolicited": {extension: extension.Raw{Type: 0xfafa}, expectedErr: dtlserrors.ErrUnsolicitedExtension, description: alert.UnsupportedExtension, generate: true},
+ } {
+ t.Run(name, func(t *testing.T) {
+ cfg, state := testHandshakeConfig13(t), newTestState13(t, test.isClient)
+ if test.generate {
+ _, dtlsAlert, err := flight13GenerateForTest(t, dtlsflight13.Flight1, &handshakeTestContext13{state: state, cfg: cfg})
+ require.NoError(t, err)
+ require.Nil(t, dtlsAlert)
+ }
+ rawServerHello := marshalHelloRetryRequestServerHello(t, cfg, []extension.Value{&extension13.SelectedVersion{Version: protocol.Version1_3}, test.extension})
+ cache := dtlsflight.NewCache()
+ cache.Push(rawServerHello, cfg.InitialEpoch, 0, handshake.TypeServerHello, false)
+
+ nextFlight, dtlsAlert, err := flight13ParseForTest(t, dtlsflight13.Flight1, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+ require.ErrorIs(t, err, test.expectedErr)
+ require.NotNil(t, dtlsAlert)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: test.description}, dtlsAlert)
+ assert.Zero(t, nextFlight)
+ })
+ }
+}
+
+// Certificate-free flight fixtures must explicitly negotiate PSK authentication.
+func configureTestPSK13(cfg *dtlsconfig.HandshakeConfig) {
+ cfg.GetPSKs = func() ([]dtlsstate.PSK, error) {
+ return []dtlsstate.PSK{{Identity: []byte("client"), Secret: []byte("shared secret"), Hash: crypto.SHA256, External: true}}, nil
+ }
+ cfg.SelectPSK = func([][]byte) (int, []byte, crypto.Hash, error) {
+ return 0, []byte("shared secret"), crypto.SHA256, nil
+ }
+}
+
+func TestFlight13RejectsMissingServerAuthentication(t *testing.T) {
+ fixture := newFlight13ProtectedServerFlightFixture(t, false)
+ _, _, err := flight13ParseForTest(t, dtlsflight13.Flight3, t.Context(), &handshakeTestContext13{
+ state: fixture.state, cache: fixture.cacheWithFinished(fixture.rawFinished), cfg: fixture.cfg, transcript: fixture.transcript,
+ })
+ require.ErrorIs(t, err, dtlserrors.ErrCertificateVerifyNoCertificate)
+}
+
+func firstPSKSecret13(state *dtlsstate.State13) []byte {
+ if len(state.LocalPSKs) == 0 {
+ return nil
+ }
+
+ return state.LocalPSKs[0].Secret
+}
+
+func TestFlight13_0ParseRejectsNoSharedGroup(t *testing.T) {
+ for _, skipVerify := range []bool{false, true} {
+ for _, group := range []elliptic.Curve{elliptic.P384, elliptic.Curve(0x0a0a)} {
+ cfg := testHandshakeConfig13(t)
+ cfg.EllipticCurves = []elliptic.Curve{elliptic.P256}
+ cfg.InsecureSkipHelloVerify = skipVerify
+ state := newTestState13(t, false)
+ cache := dtlsflight.NewCache()
+ pushFlight13_0ClientHello(t, cache, cfg, []extension.Value{
+ &extension.SignatureAlgorithms{Schemes: dtlsflight.SignatureSchemeIDs(cfg.LocalSignatureSchemes)},
+ &extension.SupportedGroups{Groups: []elliptic.Curve{group}},
+ &extension13.ClientKeyShare{Shares: []extension13.KeyShareEntry{{Group: group, KeyExchange: []byte{0x12, 0x34}}}},
+ &extension13.OfferedVersions{Versions: []protocol.Version{protocol.Version1_3}},
+ })
+ next, received, err := flight13ParseForTest(t, dtlsflight13.Flight0, context.Background(), &handshakeTestContext13{state: state, cache: cache, cfg: cfg})
+ require.ErrorIs(t, err, dtlserrors.ErrNoSupportedEllipticCurves)
+ assert.Zero(t, next)
+ assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, received)
+ }
+ }
+}
diff --git a/flighthandler.go b/flighthandler.go
deleted file mode 100644
index 50f0af450..000000000
--- a/flighthandler.go
+++ /dev/null
@@ -1,81 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
-)
-
-// Parse received handshakes and return next flightVal.
-type flightParser func(
- context.Context,
- flightConn,
- *dtlsstate.State,
- *handshakeCache,
- *handshakeConfig,
-) (flightVal, *alert.Alert, error)
-
-// Generate flights.
-type flightGenerator func(
- flightConn,
- *dtlsstate.State,
- *handshakeCache,
- *handshakeConfig,
-) ([]*packet, *alert.Alert, error)
-
-func (f flightVal) getFlightParser() (flightParser, error) { //nolint:cyclop
- switch f {
- case flight0:
- return flight0Parse, nil
- case flight1:
- return flight1Parse, nil
- case flight2:
- return flight2Parse, nil
- case flight3:
- return flight3Parse, nil
- case flight4:
- return flight4Parse, nil
- case flight4b:
- return flight4bParse, nil
- case flight5:
- return flight5Parse, nil
- case flight5b:
- return flight5bParse, nil
- case flight6:
- return flight6Parse, nil
- default:
- return nil, dtlserrors.ErrInvalidFlight
- }
-}
-
-func (f flightVal) getFlightGenerator() (gen flightGenerator, retransmit bool, err error) { //nolint:cyclop
- switch f {
- case flight0:
- return flight0Generate, true, nil
- case flight1:
- return flight1Generate, true, nil
- case flight2:
- // https://tools.ietf.org/html/rfc6347#section-3.2.1
- // HelloVerifyRequests must not be retransmitted.
- return flight2Generate, false, nil
- case flight3:
- return flight3Generate, true, nil
- case flight4:
- return flight4Generate, true, nil
- case flight4b:
- return flight4bGenerate, true, nil
- case flight5:
- return flight5Generate, true, nil
- case flight5b:
- return flight5bGenerate, true, nil
- case flight6:
- return flight6Generate, true, nil
- default:
- return nil, false, dtlserrors.ErrInvalidFlight
- }
-}
diff --git a/flighthandler_13.go b/flighthandler_13.go
deleted file mode 100644
index 336ebe370..000000000
--- a/flighthandler_13.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// SPDX-FileCopyrightText: 2023 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
-)
-
-type handshakeContext13 struct {
- state *dtlsstate.State
- cache *handshakeCache
- cfg *handshakeConfig
- transcript *handshakeTranscript13
-}
-
-// Parse received handshakes and return next flightVal.
-type flightParser13 func( //nolint:unused
- context.Context,
- flightConn,
- *handshakeContext13,
-) (flightVal13, *alert.Alert, error)
-
-//nolint:unused
-type flightGenerator13 func(flightConn, *handshakeContext13) ([]*packet, *alert.Alert, error)
-
-//nolint:unused
-func (f flightVal13) getFlightParser13() (flightParser13, error) {
- switch f {
- case flight13_0:
- return flight13_0Parse, nil
- case flight13_1:
- return flight13_1Parse, nil
- case flight13_2:
- return flight13_2Parse, nil
- case flight13_3:
- return flight13_3Parse, nil
-
- default:
- return nil, dtlserrors.ErrFlightUnimplemented13
- }
-}
-
-func (f flightVal13) getFlightGenerator13() (gen flightGenerator13, retransmit bool, err error) {
- switch f {
- case flight13_0:
- return flight13_0Generate, true, nil
- case flight13_1:
- return flight13_1Generate, true, nil
- case flight13_2:
- return flight13_2Generate, false, nil
- case flight13_3:
- return flight13_3Generate, true, nil
- default:
- return nil, false, dtlserrors.ErrFlightUnimplemented13
- }
-}
diff --git a/flighthandlers_client_13.go b/flighthandlers_client_13.go
deleted file mode 100644
index 017236870..000000000
--- a/flighthandlers_client_13.go
+++ /dev/null
@@ -1,546 +0,0 @@
-// SPDX-FileCopyrightText: 2023 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "bytes"
- "context"
- "errors"
- "maps"
- "slices"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
- "github.com/pion/dtls/v3/pkg/crypto/prf"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
-)
-
-// we'll add the flight handlers for the DTLS 1.3 client here.
-//
-// +----------+
-// | Flight 1 |
-// | Flight 3 |
-// | Flight 5 |
-// +----------+
-//
-// +-----------+
-// | Flight 3a |
-// | Flight 5a |
-// +-----------+
-//
-// +-----------+
-// | Flight 3b |
-// | Flight 5b |
-// +-----------+
-//
-// +-----------+
-// | Flight 5c |
-// +-----------+
-
-func isHelloRetryRequest(sh *handshake.MessageServerHello) bool {
- randomBytes := sh.Random.MarshalFixed()
-
- return bytes.Equal(randomBytes[:], handshake.HelloRetryRequestRandom())
-}
-
-func serverHelloSelectedVersions(extensions []extension.Extension) ([]protocol.Version, bool, error) {
- seenSupportedVersions := false
- var versions []protocol.Version
- for _, val := range extensions {
- supportedVersions, ok := val.(*extension.SupportedVersions)
- if !ok {
- continue
- }
- if seenSupportedVersions || !supportedVersions.IsSelectedVersion() || len(supportedVersions.Versions) != 1 {
- return nil, true, dtlserrors.ErrInvalidServerHello
- }
- seenSupportedVersions = true
- versions = supportedVersions.Versions
- }
-
- return versions, seenSupportedVersions, nil
-}
-
-func validateHelloRetryRequestSelectedVersion(extensions []extension.Extension) error {
- versions, seenSupportedVersions, err := serverHelloSelectedVersions(extensions)
- if err != nil || !seenSupportedVersions {
- return dtlserrors.ErrInvalidHelloRetryRequest
- }
- if !versions[0].Equal(protocol.Version1_3) {
- return dtlserrors.ErrUnsupportedProtocolVersion
- }
-
- return nil
-}
-
-func selectServerHelloCipherSuite13(
- serverHello *handshake.MessageServerHello,
- cfg *handshakeConfig,
-) (CipherSuite, *alert.Alert, error) {
- if serverHello.CipherSuiteID == nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter},
- dtlserrors.ErrInvalidServerHello
- }
- remoteCipherSuite := cipherSuiteForID(CipherSuiteID(*serverHello.CipherSuiteID), cfg.customCipherSuites)
- if remoteCipherSuite == nil {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity},
- dtlserrors.ErrCipherSuiteNoIntersection
- }
- if !cipherSuiteIDSupportsVersion(remoteCipherSuite.ID(), protocol.Version1_3) {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity},
- dtlserrors.ErrInvalidCipherSuite
- }
- selectedCipherSuite, found := findMatchingCipherSuite(
- []CipherSuite{remoteCipherSuite}, cfg.localCipherSuites,
- )
- if !found {
- return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity},
- dtlserrors.ErrInvalidCipherSuite
- }
-
- return selectedCipherSuite, nil, nil
-}
-
-// nolint:unused,cyclop
-func flight13_1Parse(
- ctx context.Context,
- conn flightConn,
- flightCtx *handshakeContext13,
-) (flightVal13, *alert.Alert, error) {
- state := flightCtx.state
- cache := flightCtx.cache
- cfg := flightCtx.cfg
-
- seq, msgs, items, ok := cache.fullPullMapItems(state.HandshakeRecvSequence, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, true},
- )
- if !ok {
- // No valid message received. Keep reading
- return 0, nil, nil
- }
-
- sh, ok := msgs[handshake.TypeServerHello].(*handshake.MessageServerHello)
- if !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
-
- if !isHelloRetryRequest(sh) {
- // Flight1 and flight2 were skipped.
- // Parse as flight3.
- return flight13_3Parse(ctx, conn, flightCtx)
- }
- // Handle HelloRetryRequest
-
- if !sh.Version.Equal(protocol.Version1_0) && !sh.Version.Equal(protocol.Version1_2) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion},
- dtlserrors.ErrUnsupportedProtocolVersion
- }
- if err := validateHelloRetryRequestSelectedVersion(sh.Extensions); err != nil {
- description := alert.IllegalParameter
- if errors.Is(err, dtlserrors.ErrUnsupportedProtocolVersion) {
- description = alert.ProtocolVersion
- }
-
- return 0, &alert.Alert{Level: alert.Fatal, Description: description}, err
- }
- selectedCipherSuite, dtlsAlert, err := selectServerHelloCipherSuite13(sh, cfg)
- if err != nil {
- return 0, dtlsAlert, err
- }
- state.CipherSuite = selectedCipherSuite
-
- // nolint:godox
- // TODO: negotiate minimial set of extensions necessary for the client
- // to generate a correct CH pair. As with the ServerHello, a
- // HelloRetryRequest MUST NOT contain any extensions that were not first
- // offered by the client in its ClientHello, with the exception of
- // optionally the "cookie" extension
- for _, val := range sh.Extensions {
- switch ext := val.(type) {
- case *extension.SupportedVersions:
- // nolint:godox
- // TODO: negotiate version
- state.RemoteVersions = ext.Versions
- case *extension.CookieExt:
- state.Cookie = ext.Cookie
- case *extension.KeyShare:
- if ext.SelectedGroup != nil {
- state.RemoteKeyEntries = &[]extension.KeyShareEntry{
- {Group: *ext.SelectedGroup},
- }
- }
- }
- }
-
- if err := appendInboundHandshakeCacheItems13(flightCtx.transcript, state.CipherSuite, items); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- state.HandshakeRecvSequence = seq
-
- return flight13_3, nil, nil
-}
-
-//nolint:cyclop
-func flight13_3Parse(
- _ context.Context,
- _ flightConn,
- flightCtx *handshakeContext13,
-) (flightVal13, *alert.Alert, error) {
- seq, msgs, items, ok := flightCtx.cache.fullPullMapItems(
- flightCtx.state.HandshakeRecvSequence, flightCtx.state.CipherSuite,
- handshakeCachePullRule{handshake.TypeServerHello, flightCtx.cfg.initialEpoch, false, false},
- )
- if !ok {
- return 0, nil, nil
- }
-
- serverHello, ok := msgs[handshake.TypeServerHello].(*handshake.MessageServerHello)
- if !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
-
- if isHelloRetryRequest(serverHello) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.UnexpectedMessage},
- dtlserrors.ErrUnexpectedSecondHelloRetryRequest
- }
-
- if !serverHello.Version.Equal(protocol.Version1_2) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion},
- dtlserrors.ErrUnsupportedProtocolVersion
- }
-
- versions, seenSupportedVersions, err := serverHelloSelectedVersions(serverHello.Extensions)
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, dtlserrors.ErrInvalidServerHello
- }
- if !seenSupportedVersions || !versions[0].Equal(protocol.Version1_3) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion},
- dtlserrors.ErrUnsupportedProtocolVersion
- }
- flightCtx.state.RemoteVersions = versions
- flightCtx.state.LocalVersion = protocol.Version1_3
-
- selectedCipherSuite, dtlsAlert, err := selectServerHelloCipherSuite13(serverHello, flightCtx.cfg)
- if err != nil {
- return 0, dtlsAlert, err
- }
- flightCtx.state.CipherSuite = selectedCipherSuite
- flightCtx.state.RemoteRandom = serverHello.Random
- flightCtx.cfg.log.Tracef("[handshake13] use cipher suite: %s", selectedCipherSuite.String())
-
- var serverShare *extension.KeyShareEntry
- for _, ext := range serverHello.Extensions {
- keyShare, isKeyShare := ext.(*extension.KeyShare)
- if isKeyShare && keyShare.ServerShare != nil {
- serverShare = keyShare.ServerShare
-
- break
- }
- }
- if serverShare == nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, dtlserrors.ErrServerKeyShareMissing
- }
-
- localKeypair, ok := flightCtx.state.LocalKeypairs[serverShare.Group]
- if !ok || localKeypair == nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter},
- dtlserrors.ErrServerKeyShareUnknownGroup
- }
-
- preMasterSecret, err := prf.PreMasterSecret(serverShare.KeyExchange, localKeypair.PrivateKey, serverShare.Group)
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- flightCtx.state.PreMasterSecret = preMasterSecret
- flightCtx.state.NamedCurve = serverShare.Group
- flightCtx.state.RemoteKeyEntries = &[]extension.KeyShareEntry{*serverShare}
-
- if err := appendInboundHandshakeCacheItems13(flightCtx.transcript, flightCtx.state.CipherSuite, items); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- if err := deriveAndStoreHandshakeTrafficSecrets13(flightCtx.state, flightCtx.transcript); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- flightCtx.state.HandshakeRecvSequence = seq
-
- return flight13_5, nil, nil
-}
-
-//nolint:cyclop
-func flight13_1Generate(
- _ flightConn,
- flightCtx *handshakeContext13,
-) ([]*packet, *alert.Alert, error) {
- state := flightCtx.state
- cfg := flightCtx.cfg
-
- var zeroEpoch uint16
- state.LocalEpoch.Store(zeroEpoch)
- state.RemoteEpoch.Store(zeroEpoch)
- if len(cfg.ellipticCurves) < 1 {
- return nil, nil, dtlserrors.ErrEmptyEllipticCurves
- }
- if len(cfg.localSignatureSchemes) < 1 {
- return nil, nil, dtlserrors.ErrNoAvailableSignatureSchemes
- }
- state.NamedCurve = cfg.ellipticCurves[0]
- state.Cookie = nil
-
- if err := state.LocalRandom.Populate(); err != nil {
- return nil, nil, err
- }
-
- if cfg.helloRandomBytesGenerator != nil {
- state.LocalRandom.RandomBytes = cfg.helloRandomBytesGenerator()
- }
-
- extensions := []extension.Extension{
- &extension.SupportedSignatureAlgorithms{
- SignatureHashAlgorithms: cfg.localSignatureSchemes,
- },
- }
-
- if cfg.extendedMasterSecret == RequestExtendedMasterSecret ||
- cfg.extendedMasterSecret == RequireExtendedMasterSecret {
- extensions = append(extensions, &extension.UseExtendedMasterSecret{
- Supported: true,
- })
- }
-
- extensions = append(extensions, &extension.RenegotiationInfo{
- RenegotiatedConnection: 0,
- })
-
- var setEllipticCurveCryptographyClientHelloExtensions bool
- for _, c := range cfg.localCipherSuites {
- if c.ECC() {
- setEllipticCurveCryptographyClientHelloExtensions = true
-
- break
- }
- }
-
- if setEllipticCurveCryptographyClientHelloExtensions {
- extensions = append(extensions, []extension.Extension{
- &extension.SupportedEllipticCurves{
- EllipticCurves: cfg.ellipticCurves,
- },
- &extension.SupportedPointFormats{
- PointFormats: []elliptic.CurvePointFormat{elliptic.CurvePointFormatUncompressed},
- },
- }...)
- }
-
- if len(cfg.supportedProtocols) > 0 {
- extensions = append(extensions, &extension.ALPN{ProtocolNameList: cfg.supportedProtocols})
- }
-
- entries := make([]extension.KeyShareEntry, 0, len(cfg.ellipticCurves))
- keypairs := make(map[elliptic.Curve]*elliptic.Keypair, len(cfg.ellipticCurves))
- for _, group := range cfg.ellipticCurves {
- keypair, err := elliptic.GenerateKeypair(group)
- if err != nil {
- return nil, nil, err
- }
- entries = append(entries, extension.KeyShareEntry{
- Group: keypair.Curve, KeyExchange: keypair.PublicKey,
- })
- keypairs[keypair.Curve] = keypair
- }
- state.LocalKeyEntries = entries
- state.LocalKeypairs = keypairs
- extensions = append(extensions, &extension.KeyShare{
- ClientShares: entries,
- })
-
- extensions = append(extensions, &extension.SupportedVersions{
- Versions: supportedVersionsRange(cfg.minVersion, cfg.maxVersion),
- })
-
- if len(cfg.localCertSignatureSchemes) > 0 {
- extensions = append(extensions, &extension.SignatureAlgorithmsCert{
- SignatureHashAlgorithms: cfg.localCertSignatureSchemes,
- })
- }
-
- if len(cfg.serverName) > 0 {
- extensions = append(extensions, &extension.ServerName{ServerName: cfg.serverName})
- }
-
- if len(cfg.localSRTPProtectionProfiles) > 0 {
- extensions = append(extensions, &extension.UseSRTP{
- ProtectionProfiles: cfg.localSRTPProtectionProfiles,
- MasterKeyIdentifier: cfg.localSRTPMasterKeyIdentifier,
- })
- }
-
- // connection ID
-
- // Pre_shared_key must be last extension
-
- clientHello := &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- SessionID: state.SessionID,
- Cookie: nil,
- Random: state.LocalRandom,
- // Add DTLS 1.3 ciphersuites
- CipherSuiteIDs: cipherSuiteIDs(cfg.localCipherSuites),
- CompressionMethods: defaultCompressionMethods(),
- Extensions: extensions,
- }
-
- var content handshake.Handshake
-
- if cfg.clientHelloMessageHook != nil {
- content = handshake.Handshake{Message: cfg.clientHelloMessageHook(*clientHello)}
- } else {
- content = handshake.Handshake{Message: clientHello}
- }
-
- return []*packet{
- {
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &content,
- },
- },
- }, nil, nil
-}
-
-// nolint:cyclop
-func flight13_3Generate(
- _ flightConn,
- flightCtx *handshakeContext13,
-) ([]*packet, *alert.Alert, error) {
- if len(flightCtx.cfg.localSignatureSchemes) < 1 {
- return nil, nil, dtlserrors.ErrNoAvailableSignatureSchemes
- }
-
- extensions := []extension.Extension{
- &extension.SupportedSignatureAlgorithms{
- SignatureHashAlgorithms: flightCtx.cfg.localSignatureSchemes,
- },
- }
-
- if flightCtx.cfg.extendedMasterSecret == RequestExtendedMasterSecret ||
- flightCtx.cfg.extendedMasterSecret == RequireExtendedMasterSecret {
- extensions = append(extensions, &extension.UseExtendedMasterSecret{
- Supported: true,
- })
- }
-
- extensions = append(extensions, &extension.RenegotiationInfo{
- RenegotiatedConnection: 0,
- })
-
- if flightCtx.state.NamedCurve != 0 {
- extensions = append(extensions, []extension.Extension{
- &extension.SupportedEllipticCurves{
- EllipticCurves: flightCtx.cfg.ellipticCurves,
- },
- &extension.SupportedPointFormats{
- PointFormats: []elliptic.CurvePointFormat{elliptic.CurvePointFormatUncompressed},
- },
- }...)
- }
-
- if len(flightCtx.cfg.supportedProtocols) > 0 {
- extensions = append(extensions, &extension.ALPN{ProtocolNameList: flightCtx.cfg.supportedProtocols})
- }
-
- var localGroups []elliptic.Curve
- var newEntries []extension.KeyShareEntry
- newKeypairs := map[elliptic.Curve]*elliptic.Keypair{}
- if flightCtx.state.RemoteKeyEntries != nil {
- for _, entry := range flightCtx.state.LocalKeyEntries {
- localGroups = append(localGroups, entry.Group)
- }
-
- for _, entry := range *flightCtx.state.RemoteKeyEntries {
- if !slices.Contains(localGroups, entry.Group) && slices.Contains(flightCtx.cfg.ellipticCurves, entry.Group) {
- keypair, err := elliptic.GenerateKeypair(entry.Group)
- if err != nil {
- return nil, nil, err
- }
- newEntries = append(newEntries, extension.KeyShareEntry{
- Group: keypair.Curve, KeyExchange: keypair.PublicKey,
- })
- newKeypairs[keypair.Curve] = keypair
- }
- }
- }
- if len(newEntries) > 0 {
- flightCtx.state.LocalKeyEntries = append(newEntries, flightCtx.state.LocalKeyEntries...)
- if flightCtx.state.LocalKeypairs == nil {
- flightCtx.state.LocalKeypairs = make(map[elliptic.Curve]*elliptic.Keypair, len(newKeypairs))
- }
- maps.Copy(flightCtx.state.LocalKeypairs, newKeypairs)
- }
- extensions = append(extensions, &extension.KeyShare{
- ClientShares: flightCtx.state.LocalKeyEntries,
- })
-
- if !slices.Contains(flightCtx.state.RemoteVersions, protocol.Version1_3) {
- return nil, nil, dtlserrors.ErrNoCommonProtocolVersion
- }
- extensions = append(extensions, &extension.SupportedVersions{
- Versions: supportedVersionsRange(flightCtx.cfg.minVersion, flightCtx.cfg.maxVersion),
- })
-
- if len(flightCtx.cfg.localCertSignatureSchemes) > 0 {
- extensions = append(extensions, &extension.SignatureAlgorithmsCert{
- SignatureHashAlgorithms: flightCtx.cfg.localCertSignatureSchemes,
- })
- }
-
- if len(flightCtx.cfg.serverName) > 0 {
- extensions = append(extensions, &extension.ServerName{ServerName: flightCtx.cfg.serverName})
- }
-
- if len(flightCtx.cfg.localSRTPProtectionProfiles) > 0 {
- extensions = append(extensions, &extension.UseSRTP{
- ProtectionProfiles: flightCtx.cfg.localSRTPProtectionProfiles,
- MasterKeyIdentifier: flightCtx.cfg.localSRTPMasterKeyIdentifier,
- })
- }
-
- if len(flightCtx.state.Cookie) > 0 {
- extensions = append(extensions, &extension.CookieExt{Cookie: flightCtx.state.Cookie})
- }
-
- clientHello := &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- SessionID: flightCtx.state.SessionID,
- Cookie: []byte{},
- Random: flightCtx.state.LocalRandom,
- CipherSuiteIDs: cipherSuiteIDs(flightCtx.cfg.localCipherSuites),
- CompressionMethods: defaultCompressionMethods(),
- Extensions: extensions,
- }
-
- var content handshake.Handshake
-
- if flightCtx.cfg.clientHelloMessageHook != nil {
- content = handshake.Handshake{Message: flightCtx.cfg.clientHelloMessageHook(*clientHello)}
- } else {
- content = handshake.Handshake{Message: clientHello}
- }
-
- return []*packet{
- {
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &content,
- },
- },
- }, nil, nil
-}
diff --git a/flighthandlers_client_13_test.go b/flighthandlers_client_13_test.go
deleted file mode 100644
index 1317a56b2..000000000
--- a/flighthandlers_client_13_test.go
+++ /dev/null
@@ -1,983 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
- "testing"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
- "github.com/pion/dtls/v3/pkg/crypto/prf"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-type rawExtension struct {
- typeValue extension.TypeValue
- raw []byte
-}
-
-func (e rawExtension) Marshal() ([]byte, error) {
- return append([]byte(nil), e.raw...), nil
-}
-
-func (e rawExtension) Unmarshal([]byte) error {
- return nil
-}
-
-func (e rawExtension) TypeValue() extension.TypeValue {
- return e.typeValue
-}
-
-func marshalHelloRetryRequestServerHello(
- t *testing.T,
- cfg *handshakeConfig,
- extensions []extension.Extension,
-) []byte {
- t.Helper()
-
- var hrrRandomFixed [handshake.RandomLength]byte
- copy(hrrRandomFixed[:], handshake.HelloRetryRequestRandom())
- var hrrRandom handshake.Random
- hrrRandom.UnmarshalFixed(hrrRandomFixed)
-
- return marshalServerHello(t, cfg, hrrRandom, extensions)
-}
-
-func marshalServerHello(
- t *testing.T,
- cfg *handshakeConfig,
- random handshake.Random,
- extensions []extension.Extension,
-) []byte {
- t.Helper()
-
- return marshalServerHelloWithSequence(t, cfg, random, extensions, 0)
-}
-
-func marshalServerHelloWithSequence(
- t *testing.T,
- cfg *handshakeConfig,
- random handshake.Random,
- extensions []extension.Extension,
- seq uint16,
-) []byte {
- t.Helper()
-
- cipherSuiteID := uint16(cfg.localCipherSuites[0].ID())
- serverHello := &handshake.MessageServerHello{
- Version: protocol.Version1_2,
- Random: random,
- CipherSuiteID: &cipherSuiteID,
- CompressionMethod: defaultCompressionMethods()[0],
- Extensions: extensions,
- }
- rawServerHello, err := (&handshake.Handshake{
- Header: handshake.Header{MessageSequence: seq},
- Message: serverHello,
- }).Marshal()
- require.NoError(t, err)
-
- return rawServerHello
-}
-
-func generateFlight13_1ClientHello(t *testing.T, cfg *handshakeConfig) *handshake.MessageClientHello {
- t.Helper()
-
- state := &dtlsstate.State{}
-
- pkts, dtlsAlert, err := flight13_1Generate(nil, &handshakeContext13{
- state: state,
- cfg: cfg,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- require.Len(t, pkts, 1)
-
- hand, ok := pkts[0].record.Content.(*handshake.Handshake)
- require.True(t, ok)
- raw, err := hand.Marshal()
- require.NoError(t, err)
-
- var parsed handshake.Handshake
- require.NoError(t, parsed.Unmarshal(raw))
- clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
- require.True(t, ok)
-
- return clientHello
-}
-
-func TestFlight13_1GenerateClientHelloUsesSupportedVersionsVector(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{}
-
- pkts, dtlsAlert, err := flight13_1Generate(nil, &handshakeContext13{
- state: state,
- cfg: cfg,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- require.Len(t, pkts, 1)
-
- hand, ok := pkts[0].record.Content.(*handshake.Handshake)
- require.True(t, ok)
- raw, err := hand.Marshal()
- require.NoError(t, err)
-
- var parsed handshake.Handshake
- require.NoError(t, parsed.Unmarshal(raw))
- clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
- require.True(t, ok)
-
- var supportedVersions *extension.SupportedVersions
- for _, ext := range clientHello.Extensions {
- if sv, ok := ext.(*extension.SupportedVersions); ok {
- supportedVersions = sv
-
- break
- }
- }
- require.NotNil(t, supportedVersions)
- assert.Equal(t, []protocol.Version{protocol.Version1_3}, supportedVersions.Versions)
- assert.False(t, supportedVersions.IsSelectedVersion())
-}
-
-func TestFlight13_1GenerateClientHelloIncludesSignatureAlgorithms(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- cfg.localCertSignatureSchemes = cfg.localSignatureSchemes[:1]
-
- clientHello := generateFlight13_1ClientHello(t, cfg)
-
- var signatureAlgorithms *extension.SupportedSignatureAlgorithms
- var signatureAlgorithmsCert *extension.SignatureAlgorithmsCert
- for _, ext := range clientHello.Extensions {
- switch typed := ext.(type) {
- case *extension.SupportedSignatureAlgorithms:
- signatureAlgorithms = typed
- case *extension.SignatureAlgorithmsCert:
- signatureAlgorithmsCert = typed
- }
- }
-
- require.NotNil(t, signatureAlgorithms)
- assert.Equal(t, cfg.localSignatureSchemes, signatureAlgorithms.SignatureHashAlgorithms)
- require.NotNil(t, signatureAlgorithmsCert)
- assert.Equal(t, cfg.localCertSignatureSchemes, signatureAlgorithmsCert.SignatureHashAlgorithms)
-}
-
-func TestFlight13_1GenerateClientHelloIncludesSupportedGroups(t *testing.T) {
- cfg := testHandshakeConfig13(t)
-
- clientHello := generateFlight13_1ClientHello(t, cfg)
-
- var supportedGroups *extension.SupportedEllipticCurves
- for _, ext := range clientHello.Extensions {
- if typed, ok := ext.(*extension.SupportedEllipticCurves); ok {
- supportedGroups = typed
-
- break
- }
- }
-
- require.NotNil(t, supportedGroups)
- assert.Equal(t, cfg.ellipticCurves, supportedGroups.EllipticCurves)
-}
-
-func TestFlight13_1GenerateRetainsPrivateKeysForAdvertisedShares(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{}
-
- pkts, dtlsAlert, err := flight13_1Generate(nil, &handshakeContext13{
- state: state,
- cfg: cfg,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- require.Len(t, pkts, 1)
-
- hand, ok := pkts[0].record.Content.(*handshake.Handshake)
- require.True(t, ok)
- raw, err := hand.Marshal()
- require.NoError(t, err)
-
- var parsed handshake.Handshake
- require.NoError(t, parsed.Unmarshal(raw))
- clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
- require.True(t, ok)
-
- var keyShare *extension.KeyShare
- for _, ext := range clientHello.Extensions {
- if ks, ok := ext.(*extension.KeyShare); ok {
- keyShare = ks
-
- break
- }
- }
- require.NotNil(t, keyShare)
- require.Len(t, keyShare.ClientShares, len(cfg.ellipticCurves))
- require.Len(t, state.LocalKeyEntries, len(keyShare.ClientShares))
- require.Len(t, state.LocalKeypairs, len(keyShare.ClientShares))
-
- for _, entry := range keyShare.ClientShares {
- t.Run(entry.Group.String(), func(t *testing.T) {
- localKeypair, ok := state.LocalKeypairs[entry.Group]
- require.True(t, ok)
- require.Equal(t, entry.KeyExchange, localKeypair.PublicKey)
-
- peerKeypair, err := elliptic.GenerateKeypair(entry.Group)
- require.NoError(t, err)
-
- localSecret, err := prf.PreMasterSecret(peerKeypair.PublicKey, localKeypair.PrivateKey, entry.Group)
- require.NoError(t, err)
-
- peerSecret, err := prf.PreMasterSecret(localKeypair.PublicKey, peerKeypair.PrivateKey, entry.Group)
- require.NoError(t, err)
-
- assert.Equal(t, peerSecret, localSecret)
- })
- }
-}
-
-func TestFlight13_1ParseStoresHelloRetryRequestSelectedGroup(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- selectedGroup := elliptic.P384
-
- rawServerHello := marshalHelloRetryRequestServerHello(
- t,
- cfg,
- []extension.Extension{
- &extension.SupportedVersions{
- Versions: []protocol.Version{protocol.Version1_3},
- SelectedVersion: true,
- },
- &extension.KeyShare{SelectedGroup: &selectedGroup},
- },
- )
-
- state := &dtlsstate.State{}
- cache := newHandshakeCache()
- cache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
-
- nextFlight, dtlsAlert, err := flight13_1Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flight13_3, nextFlight)
- entries := *state.RemoteKeyEntries
- require.Len(t, entries, 1)
- assert.Equal(t, selectedGroup, entries[0].Group)
- assert.Empty(t, entries[0].KeyExchange)
-}
-
-func TestFlight13_1ParseRejectsHelloRetryRequestWithoutSupportedVersions(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- selectedGroup := elliptic.P384
-
- rawServerHello := marshalHelloRetryRequestServerHello(
- t,
- cfg,
- []extension.Extension{
- &extension.KeyShare{SelectedGroup: &selectedGroup},
- },
- )
-
- state := &dtlsstate.State{}
- cache := newHandshakeCache()
- cache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
-
- nextFlight, dtlsAlert, err := flight13_1Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrInvalidHelloRetryRequest)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, alert.Fatal, dtlsAlert.Level)
- assert.Equal(t, alert.IllegalParameter, dtlsAlert.Description)
- assert.Zero(t, nextFlight)
- assert.Nil(t, state.RemoteKeyEntries)
-}
-
-func TestFlight13_1ParseRejectsHelloRetryRequestWithWrongSelectedVersion(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- selectedGroup := elliptic.P384
-
- rawServerHello := marshalHelloRetryRequestServerHello(
- t,
- cfg,
- []extension.Extension{
- &extension.SupportedVersions{
- Versions: []protocol.Version{protocol.Version1_2},
- SelectedVersion: true,
- },
- &extension.KeyShare{SelectedGroup: &selectedGroup},
- },
- )
-
- state := &dtlsstate.State{}
- cache := newHandshakeCache()
- cache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
-
- nextFlight, dtlsAlert, err := flight13_1Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrUnsupportedProtocolVersion)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, alert.Fatal, dtlsAlert.Level)
- assert.Equal(t, alert.ProtocolVersion, dtlsAlert.Description)
- assert.Zero(t, nextFlight)
- assert.Nil(t, state.RemoteKeyEntries)
-}
-
-func TestFlight13_1ParseRejectsHelloRetryRequestWithClientHelloSupportedVersionsEncoding(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- selectedGroup := elliptic.P384
-
- rawServerHello := marshalHelloRetryRequestServerHello(
- t,
- cfg,
- []extension.Extension{
- rawExtension{
- typeValue: extension.SupportedVersionsTypeValue,
- raw: []byte{
- 0x00, 0x2b, // supported_versions
- 0x00, 0x03, // extension_data length
- 0x02, // ClientHello vector length
- 0xfe, 0xfc, // DTLS v1.3
- },
- },
- &extension.KeyShare{SelectedGroup: &selectedGroup},
- },
- )
-
- state := &dtlsstate.State{}
- cache := newHandshakeCache()
- cache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
-
- nextFlight, dtlsAlert, err := flight13_1Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrInvalidHelloRetryRequest)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, alert.Fatal, dtlsAlert.Level)
- assert.Equal(t, alert.IllegalParameter, dtlsAlert.Description)
- assert.Zero(t, nextFlight)
- assert.Nil(t, state.RemoteKeyEntries)
-}
-
-func TestPickVersionFromServerResponseRejectsHelloRetryRequestWithoutSupportedVersions(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- cfg.minVersion = protocol.Version1_2
- cfg.maxVersion = protocol.Version1_3
- selectedGroup := elliptic.P384
-
- rawServerHello := marshalHelloRetryRequestServerHello(
- t,
- cfg,
- []extension.Extension{
- &extension.KeyShare{SelectedGroup: &selectedGroup},
- },
- )
-
- conn := &Conn{
- handshakeCache: newHandshakeCache(),
- handshakeConfig: cfg,
- }
- conn.handshakeCache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
-
- ok, err := conn.pickVersionFromServerResponse()
-
- require.ErrorIs(t, err, dtlserrors.ErrInvalidHelloRetryRequest)
- assert.False(t, ok)
- assert.Equal(t, protocol.Version{}, conn.state.LocalVersion)
-}
-
-func TestPickVersionFromServerResponseRejectsServerHelloWithClientHelloSupportedVersionsEncoding(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- cfg.minVersion = protocol.Version1_2
- cfg.maxVersion = protocol.Version1_3
- random := handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}}
-
- rawServerHello := marshalServerHello(
- t,
- cfg,
- random,
- []extension.Extension{
- rawExtension{
- typeValue: extension.SupportedVersionsTypeValue,
- raw: []byte{
- 0x00, 0x2b, // supported_versions
- 0x00, 0x03, // extension_data length
- 0x02, // ClientHello vector length
- 0xfe, 0xfc, // DTLS v1.3
- },
- },
- },
- )
-
- conn := &Conn{
- handshakeCache: newHandshakeCache(),
- handshakeConfig: cfg,
- }
- conn.handshakeCache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
-
- ok, err := conn.pickVersionFromServerResponse()
-
- require.ErrorIs(t, err, dtlserrors.ErrInvalidServerHello)
- assert.False(t, ok)
- assert.Equal(t, protocol.Version{}, conn.state.LocalVersion)
-}
-
-func TestFlight13_3GenerateRejectsWithoutCommonVersion(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{}
- require.NoError(t, state.LocalRandom.Populate())
-
- pkts, dtlsAlert, err := flight13_3Generate(nil, &handshakeContext13{
- state: state,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrNoCommonProtocolVersion)
- require.Nil(t, dtlsAlert)
- require.Nil(t, pkts)
-}
-
-func TestFlight13_3GenerateIncludesCookieAndSupportedVersions(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{
- Cookie: []byte{0x01, 0x02, 0x03, 0x04},
- RemoteVersions: []protocol.Version{protocol.Version1_3},
- }
- require.NoError(t, state.LocalRandom.Populate())
-
- pkts, dtlsAlert, err := flight13_3Generate(nil, &handshakeContext13{
- state: state,
- cfg: cfg,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- require.Len(t, pkts, 1)
-
- hand, ok := pkts[0].record.Content.(*handshake.Handshake)
- require.True(t, ok)
- raw, err := hand.Marshal()
- require.NoError(t, err)
-
- var parsed handshake.Handshake
- require.NoError(t, parsed.Unmarshal(raw))
- clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
- require.True(t, ok)
-
- var supportedVersions *extension.SupportedVersions
- for _, ext := range clientHello.Extensions {
- if sv, ok := ext.(*extension.SupportedVersions); ok {
- supportedVersions = sv
-
- break
- }
- }
- require.NotNil(t, supportedVersions)
- assert.Equal(t, []protocol.Version{protocol.Version1_3}, supportedVersions.Versions)
- assert.False(t, supportedVersions.IsSelectedVersion())
-
- var signatureAlgorithms *extension.SupportedSignatureAlgorithms
- for _, ext := range clientHello.Extensions {
- if sigAlgs, ok := ext.(*extension.SupportedSignatureAlgorithms); ok {
- signatureAlgorithms = sigAlgs
-
- break
- }
- }
- require.NotNil(t, signatureAlgorithms)
- assert.Equal(t, cfg.localSignatureSchemes, signatureAlgorithms.SignatureHashAlgorithms)
-
- var cookieExt *extension.CookieExt
- for _, ext := range clientHello.Extensions {
- if c, ok := ext.(*extension.CookieExt); ok {
- cookieExt = c
-
- break
- }
- }
- require.NotNil(t, cookieExt)
- assert.Equal(t, state.Cookie, cookieExt.Cookie)
-}
-
-func TestFlight13_3GeneratePrioritizesHelloRetryRequestSelectedGroup(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- selectedGroup := elliptic.P384
-
- originalKeypair, err := elliptic.GenerateKeypair(elliptic.X25519)
- require.NoError(t, err)
- state := &dtlsstate.State{
- RemoteVersions: []protocol.Version{protocol.Version1_3},
- LocalKeyEntries: []extension.KeyShareEntry{
- {Group: originalKeypair.Curve, KeyExchange: originalKeypair.PublicKey},
- },
- RemoteKeyEntries: &[]extension.KeyShareEntry{{Group: selectedGroup}},
- }
- require.NoError(t, state.LocalRandom.Populate())
-
- pkts, dtlsAlert, err := flight13_3Generate(nil, &handshakeContext13{
- state: state,
- cfg: cfg,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- require.Len(t, pkts, 1)
-
- hand, ok := pkts[0].record.Content.(*handshake.Handshake)
- require.True(t, ok)
- raw, err := hand.Marshal()
- require.NoError(t, err)
-
- var parsed handshake.Handshake
- require.NoError(t, parsed.Unmarshal(raw))
- clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
- require.True(t, ok)
-
- var keyShare *extension.KeyShare
- for _, ext := range clientHello.Extensions {
- if ks, ok := ext.(*extension.KeyShare); ok {
- keyShare = ks
-
- break
- }
- }
- require.NotNil(t, keyShare)
- require.Len(t, keyShare.ClientShares, 2)
- assert.Equal(t, selectedGroup, keyShare.ClientShares[0].Group)
- assert.NotEmpty(t, keyShare.ClientShares[0].KeyExchange)
- assert.Equal(t, elliptic.X25519, keyShare.ClientShares[1].Group)
-
- selectedKeypair := state.LocalKeypairs[selectedGroup]
- require.NotNil(t, selectedKeypair)
- assert.Equal(t, keyShare.ClientShares[0].KeyExchange, selectedKeypair.PublicKey)
-}
-
-func TestFlight13_3GenerateDoesNotRegenerateAlreadyAdvertisedGroup(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- selectedGroup := elliptic.X25519
-
- keypair, err := elliptic.GenerateKeypair(selectedGroup)
- require.NoError(t, err)
- state := &dtlsstate.State{
- RemoteVersions: []protocol.Version{protocol.Version1_3},
- LocalKeyEntries: []extension.KeyShareEntry{
- {Group: keypair.Curve, KeyExchange: keypair.PublicKey},
- },
- RemoteKeyEntries: &[]extension.KeyShareEntry{{Group: selectedGroup}},
- }
- require.NoError(t, state.LocalRandom.Populate())
-
- pkts, dtlsAlert, err := flight13_3Generate(nil, &handshakeContext13{
- state: state,
- cfg: cfg,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- require.Len(t, pkts, 1)
-
- hand, ok := pkts[0].record.Content.(*handshake.Handshake)
- require.True(t, ok)
- raw, err := hand.Marshal()
- require.NoError(t, err)
-
- var parsed handshake.Handshake
- require.NoError(t, parsed.Unmarshal(raw))
- clientHello, ok := parsed.Message.(*handshake.MessageClientHello)
- require.True(t, ok)
-
- var keyShare *extension.KeyShare
- for _, ext := range clientHello.Extensions {
- if ks, ok := ext.(*extension.KeyShare); ok {
- keyShare = ks
-
- break
- }
- }
- require.NotNil(t, keyShare)
- require.Len(t, keyShare.ClientShares, 1)
- assert.Equal(t, selectedGroup, keyShare.ClientShares[0].Group)
-}
-
-func TestFlight13_3ParseNegotiatesVersionCipherAndKeyShare(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{}
- transcript := newHandshakeTranscript13()
- clientHello, _, err := flight13_1Generate(nil, &handshakeContext13{state: state, cfg: cfg})
- require.NoError(t, err)
- appended, err := appendClientHelloInitialFlights13(transcript, clientHello)
- require.NoError(t, err)
- require.True(t, appended)
-
- group := cfg.ellipticCurves[0]
- serverKeypair, err := elliptic.GenerateKeypair(group)
- require.NoError(t, err)
-
- random := handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01, 0x02, 0x03}}
- rawServerHello := marshalServerHello(t, cfg, random, []extension.Extension{
- &extension.SupportedVersions{Versions: []protocol.Version{protocol.Version1_3}, SelectedVersion: true},
- &extension.KeyShare{ServerShare: &extension.KeyShareEntry{Group: group, KeyExchange: serverKeypair.PublicKey}},
- })
-
- cache := newHandshakeCache()
- cache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
- nextFlight, dtlsAlert, err := flight13_3Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- transcript: transcript,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flight13_5, nextFlight)
-
- assert.Equal(t, protocol.Version1_3, state.LocalVersion)
- assert.Equal(t, []protocol.Version{protocol.Version1_3}, state.RemoteVersions)
- require.NotNil(t, state.CipherSuite)
- assert.Equal(t, cfg.localCipherSuites[0].ID(), state.CipherSuite.ID())
- assert.Equal(t, group, state.NamedCurve)
- assert.Equal(t, random.RandomBytes, state.RemoteRandom.RandomBytes)
- require.NotNil(t, state.RemoteKeyEntries)
- require.Len(t, *state.RemoteKeyEntries, 1)
- assert.Equal(t, group, (*state.RemoteKeyEntries)[0].Group)
-
- clientKeypair := state.LocalKeypairs[group]
- require.NotNil(t, clientKeypair)
- expected, err := prf.PreMasterSecret(clientKeypair.PublicKey, serverKeypair.PrivateKey, group)
- require.NoError(t, err)
- assert.Equal(t, expected, state.PreMasterSecret)
- assert.NotEmpty(t, state.PreMasterSecret)
- transcriptHash, err := transcript.sum()
- require.NoError(t, err)
- expectedSecrets, err := deriveHandshakeTrafficSecrets13(state.CipherSuite.HashFunc(), expected, transcriptHash)
- require.NoError(t, err)
- assert.Equal(t, expectedSecrets, state.HandshakeTrafficSecrets13)
- assert.NotEqual(t, state.HandshakeTrafficSecrets13.Client, state.HandshakeTrafficSecrets13.Server)
-}
-
-func TestFlight13ClientParseAppendsNoHRRTranscriptOrder(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{}
- transcript := newHandshakeTranscript13()
-
- pkts, dtlsAlert, err := flight13_1Generate(nil, &handshakeContext13{
- state: state,
- cfg: cfg,
- transcript: transcript,
- })
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- appended, err := appendClientHelloInitialFlights13(transcript, pkts)
- require.NoError(t, err)
- require.True(t, appended)
- clientHelloCanonical := canonicalPacketHandshake13(t, pkts[0])
-
- group := cfg.ellipticCurves[0]
- serverKeypair, err := elliptic.GenerateKeypair(group)
- require.NoError(t, err)
- rawServerHello := marshalServerHello(t, cfg, handshake.Random{
- RandomBytes: [handshake.RandomBytesLength]byte{0x01},
- }, []extension.Extension{
- &extension.SupportedVersions{Versions: []protocol.Version{protocol.Version1_3}, SelectedVersion: true},
- &extension.KeyShare{ServerShare: &extension.KeyShareEntry{Group: group, KeyExchange: serverKeypair.PublicKey}},
- })
- serverHelloCanonical, err := canonicalHandshake13(rawServerHello)
- require.NoError(t, err)
-
- cache := newHandshakeCache()
- cache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
- nextFlight, dtlsAlert, err := flight13_1Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- transcript: transcript,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flight13_5, nextFlight)
- assert.Equal(t, []transcriptMessage13{
- {id: transcriptMessageID13{sender: transcriptClient13, seq: 0}, typ: handshake.TypeClientHello},
- {id: transcriptMessageID13{sender: transcriptServer13, seq: 0}, typ: handshake.TypeServerHello},
- }, transcript.order)
- assert.Equal(t, append(append([]byte(nil), clientHelloCanonical...), serverHelloCanonical...), transcript.transcript)
-}
-
-func TestFlight13ClientParseAppendsHRRTranscriptOrder(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{}
- transcript := newHandshakeTranscript13()
-
- pkts, dtlsAlert, err := flight13_1Generate(nil, &handshakeContext13{
- state: state,
- cfg: cfg,
- transcript: transcript,
- })
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- appended, err := appendClientHelloInitialFlights13(transcript, pkts)
- require.NoError(t, err)
- require.True(t, appended)
- clientHello1Canonical := canonicalPacketHandshake13(t, pkts[0])
-
- group := cfg.ellipticCurves[0]
- rawHelloRetryRequest := marshalHelloRetryRequestServerHello(t, cfg, []extension.Extension{
- &extension.SupportedVersions{Versions: []protocol.Version{protocol.Version1_3}, SelectedVersion: true},
- &extension.KeyShare{SelectedGroup: &group},
- })
- helloRetryRequestCanonical, err := canonicalHandshake13(rawHelloRetryRequest)
- require.NoError(t, err)
-
- cache := newHandshakeCache()
- cache.push(rawHelloRetryRequest, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
- nextFlight, dtlsAlert, err := flight13_1Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- transcript: transcript,
- })
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flight13_3, nextFlight)
-
- clientHello2, dtlsAlert, err := flight13_3Generate(nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- transcript: transcript,
- })
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- require.Len(t, clientHello2, 1)
- clientHello2Handshake, ok := clientHello2[0].record.Content.(*handshake.Handshake)
- require.True(t, ok)
- clientHello2Handshake.Header.MessageSequence = 1
- require.NoError(t, appendOutboundHandshakeFlight13(transcript, true, state.CipherSuite, clientHello2))
- clientHello2Canonical := canonicalPacketHandshake13(t, clientHello2[0])
-
- serverKeypair, err := elliptic.GenerateKeypair(group)
- require.NoError(t, err)
- rawServerHello := marshalServerHelloWithSequence(
- t,
- cfg,
- handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x02}},
- []extension.Extension{
- &extension.SupportedVersions{Versions: []protocol.Version{protocol.Version1_3}, SelectedVersion: true},
- &extension.KeyShare{ServerShare: &extension.KeyShareEntry{Group: group, KeyExchange: serverKeypair.PublicKey}},
- },
- 1,
- )
- serverHelloCanonical, err := canonicalHandshake13(rawServerHello)
- require.NoError(t, err)
- cache.push(rawServerHello, cfg.initialEpoch, 1, handshake.TypeServerHello, false)
-
- nextFlight, dtlsAlert, err = flight13_3Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- transcript: transcript,
- })
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flight13_5, nextFlight)
-
- clientHello1Hash := hashTranscript13(clientHello1Canonical)
- messageHash := canonicalTranscriptHandshake13(handshake.TypeMessageHash, clientHello1Hash)
- expectedTranscript := append(append(append(append([]byte(nil), messageHash...), helloRetryRequestCanonical...),
- clientHello2Canonical...), serverHelloCanonical...)
- assert.Equal(t, []transcriptMessage13{
- {id: transcriptMessageID13{sender: transcriptClient13, seq: 0}, typ: handshake.TypeClientHello},
- {id: transcriptMessageID13{sender: transcriptServer13, seq: 0}, typ: handshake.TypeServerHello},
- {id: transcriptMessageID13{sender: transcriptClient13, seq: 1}, typ: handshake.TypeClientHello},
- {id: transcriptMessageID13{sender: transcriptServer13, seq: 1}, typ: handshake.TypeServerHello},
- }, transcript.order)
- assert.Equal(t, expectedTranscript, transcript.transcript)
-}
-
-func TestFlight13_3ParseKeepsReadingWithoutServerHello(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{}
-
- nextFlight, dtlsAlert, err := flight13_3Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: newHandshakeCache(),
- cfg: cfg,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Zero(t, nextFlight)
-}
-
-func TestFlight13_3ParseRejectsSecondHelloRetryRequest(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{}
- _, _, err := flight13_1Generate(nil, &handshakeContext13{state: state, cfg: cfg})
- require.NoError(t, err)
-
- rawServerHello := marshalHelloRetryRequestServerHello(t, cfg, []extension.Extension{
- &extension.SupportedVersions{Versions: []protocol.Version{protocol.Version1_3}, SelectedVersion: true},
- })
-
- cache := newHandshakeCache()
- cache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
- nextFlight, dtlsAlert, err := flight13_3Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrUnexpectedSecondHelloRetryRequest)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, alert.Fatal, dtlsAlert.Level)
- assert.Equal(t, alert.UnexpectedMessage, dtlsAlert.Description)
- assert.Zero(t, nextFlight)
-}
-
-func TestFlight13_3ParseRejectsWrongLegacyVersion(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{}
- _, _, err := flight13_1Generate(nil, &handshakeContext13{state: state, cfg: cfg})
- require.NoError(t, err)
-
- group := cfg.ellipticCurves[0]
- serverKeypair, err := elliptic.GenerateKeypair(group)
- require.NoError(t, err)
-
- cipherSuiteID := uint16(cfg.localCipherSuites[0].ID())
- serverHello := &handshake.MessageServerHello{
- Version: protocol.Version1_0,
- Random: handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}},
- CipherSuiteID: &cipherSuiteID,
- CompressionMethod: defaultCompressionMethods()[0],
- Extensions: []extension.Extension{
- &extension.SupportedVersions{Versions: []protocol.Version{protocol.Version1_3}, SelectedVersion: true},
- &extension.KeyShare{ServerShare: &extension.KeyShareEntry{Group: group, KeyExchange: serverKeypair.PublicKey}},
- },
- }
- rawServerHello, err := (&handshake.Handshake{Message: serverHello}).Marshal()
- require.NoError(t, err)
-
- cache := newHandshakeCache()
- cache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
- nextFlight, dtlsAlert, err := flight13_3Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrUnsupportedProtocolVersion)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, alert.ProtocolVersion, dtlsAlert.Description)
- assert.Zero(t, nextFlight)
-}
-
-func TestFlight13_3ParseRejectsMissingSupportedVersions(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{}
- _, _, err := flight13_1Generate(nil, &handshakeContext13{state: state, cfg: cfg})
- require.NoError(t, err)
-
- group := cfg.ellipticCurves[0]
- serverKeypair, err := elliptic.GenerateKeypair(group)
- require.NoError(t, err)
-
- random := handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}}
- rawServerHello := marshalServerHello(t, cfg, random, []extension.Extension{
- &extension.KeyShare{ServerShare: &extension.KeyShareEntry{Group: group, KeyExchange: serverKeypair.PublicKey}},
- })
-
- cache := newHandshakeCache()
- cache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
- nextFlight, dtlsAlert, err := flight13_3Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrUnsupportedProtocolVersion)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, alert.ProtocolVersion, dtlsAlert.Description)
- assert.Zero(t, nextFlight)
-}
-
-func TestFlight13_3ParseRejectsMissingKeyShare(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{}
- _, _, err := flight13_1Generate(nil, &handshakeContext13{state: state, cfg: cfg})
- require.NoError(t, err)
-
- random := handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}}
- rawServerHello := marshalServerHello(t, cfg, random, []extension.Extension{
- &extension.SupportedVersions{Versions: []protocol.Version{protocol.Version1_3}, SelectedVersion: true},
- })
-
- cache := newHandshakeCache()
- cache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
- nextFlight, dtlsAlert, err := flight13_3Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrServerKeyShareMissing)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, alert.IllegalParameter, dtlsAlert.Description)
- assert.Zero(t, nextFlight)
-}
-
-func TestFlight13_3ParseRejectsUnofferedKeyShareGroup(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{}
-
- group := cfg.ellipticCurves[0]
- serverKeypair, err := elliptic.GenerateKeypair(group)
- require.NoError(t, err)
-
- random := handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}}
- rawServerHello := marshalServerHello(t, cfg, random, []extension.Extension{
- &extension.SupportedVersions{Versions: []protocol.Version{protocol.Version1_3}, SelectedVersion: true},
- &extension.KeyShare{ServerShare: &extension.KeyShareEntry{Group: group, KeyExchange: serverKeypair.PublicKey}},
- })
-
- cache := newHandshakeCache()
- cache.push(rawServerHello, cfg.initialEpoch, 0, handshake.TypeServerHello, false)
- nextFlight, dtlsAlert, err := flight13_3Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrServerKeyShareUnknownGroup)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, alert.IllegalParameter, dtlsAlert.Description)
- assert.Zero(t, nextFlight)
-}
diff --git a/flighthandlers_server_13.go b/flighthandlers_server_13.go
deleted file mode 100644
index 3b8982ae4..000000000
--- a/flighthandlers_server_13.go
+++ /dev/null
@@ -1,406 +0,0 @@
-// SPDX-FileCopyrightText: 2023 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "bytes"
- "context"
- "crypto/rand"
- "slices"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
-)
-
-// we'll add the flight handlers for the DTLS 1.3 server here.
-//
-// +----------+
-// | Flight0 |
-// +----------+
-//
-// +----------+
-// | Flight 2 |
-// | Flight 4 |
-// | Flight 6 |
-// +----------+
-//
-// +-----------+
-// | Flight 4a |
-// | Flight 6a |
-// +-----------+
-//
-// +-----------+
-// | Flight 4b |
-// | Flight 6b |
-// +-----------+
-//
-// +-----------+
-// | Flight 4c |
-// +-----------+
-
-type clientHello13ExtensionSet struct {
- hasPreSharedKey bool
- hasSignatureAlgorithms bool
- hasSupportedGroups bool
-}
-
-type clientHello13ExtensionFailure struct {
- alert *alert.Alert
- err error
-}
-
-func newClientHello13ExtensionFailure(
- description alert.Description,
- err error,
-) *clientHello13ExtensionFailure {
- return &clientHello13ExtensionFailure{
- alert: &alert.Alert{Level: alert.Fatal, Description: description},
- err: err,
- }
-}
-
-func processClientHello13Extensions(
- state *dtlsstate.State,
- cfg *handshakeConfig,
- clientHello *handshake.MessageClientHello,
-) *clientHello13ExtensionFailure {
- var seen clientHello13ExtensionSet
-
- for _, val := range clientHello.Extensions {
- if failure := processClientHello13SecurityExtension(state, cfg, &seen, val); failure != nil {
- return failure
- }
- processClientHello13StateExtension(state, cfg, val)
- }
-
- if !seen.hasPreSharedKey && (!seen.hasSignatureAlgorithms || !seen.hasSupportedGroups) {
- return newClientHello13ExtensionFailure(alert.MissingExtension, dtlserrors.ErrMissingClientHelloExtension)
- }
-
- return nil
-}
-
-func processClientHello13SecurityExtension(
- state *dtlsstate.State,
- cfg *handshakeConfig,
- seen *clientHello13ExtensionSet,
- val extension.Extension,
-) *clientHello13ExtensionFailure {
- switch ext := val.(type) {
- case *extension.SupportedEllipticCurves:
- seen.hasSupportedGroups = true
- if len(ext.EllipticCurves) == 0 {
- return newClientHello13ExtensionFailure(alert.InsufficientSecurity, dtlserrors.ErrNoSupportedEllipticCurves)
- }
- state.RemoteGroups = ext.EllipticCurves
- case *extension.UseSRTP:
- profile, ok := findMatchingSRTPProfile(cfg.localSRTPProtectionProfiles, ext.ProtectionProfiles)
- if !ok {
- return newClientHello13ExtensionFailure(alert.InsufficientSecurity, dtlserrors.ErrServerNoMatchingSRTPProfile)
- }
- state.SetSRTPProtectionProfile(profile)
- state.RemoteSRTPMasterKeyIdentifier = ext.MasterKeyIdentifier
- case *extension.SupportedSignatureAlgorithms:
- seen.hasSignatureAlgorithms = true
- state.RemoteSignatureSchemes = ext.SignatureHashAlgorithms
- case *extension.SupportedVersions:
- if ext.IsSelectedVersion() {
- return newClientHello13ExtensionFailure(alert.IllegalParameter, dtlserrors.ErrInvalidClientHello)
- }
- state.RemoteVersions = ext.Versions
- case *extension.PreSharedKey:
- seen.hasPreSharedKey = true
- }
-
- return nil
-}
-
-func processClientHello13StateExtension(
- state *dtlsstate.State,
- cfg *handshakeConfig,
- val extension.Extension,
-) {
- switch ext := val.(type) {
- case *extension.UseExtendedMasterSecret:
- if cfg.extendedMasterSecret != DisableExtendedMasterSecret {
- state.ExtendedMasterSecret = true
- }
- case *extension.ServerName:
- state.ServerName = ext.ServerName // remote server name
- case *extension.RenegotiationInfo:
- state.RemoteSupportsRenegotiation = true
- case *extension.ALPN:
- state.PeerSupportedProtocols = ext.ProtocolNameList
- case *extension.ConnectionID:
- // Only set connection ID to be sent if server supports connection IDs.
- if cfg.connectionIDGenerator != nil {
- state.RemoteConnectionID = ext.CID
- }
- case *extension.SignatureAlgorithmsCert:
- // Store the client's certificate signature schemes for later validation.
- state.RemoteCertSignatureSchemes = ext.SignatureHashAlgorithms
- case *extension.KeyShare:
- state.RemoteKeyEntries = &ext.ClientShares
- }
-}
-
-//nolint:cyclop,gocognit,gocyclo,unused
-func flight13_0Parse(
- _ context.Context,
- _ flightConn,
- flightCtx *handshakeContext13,
-) (flightVal13, *alert.Alert, error) {
- state := flightCtx.state
- cache := flightCtx.cache
- cfg := flightCtx.cfg
-
- if state.LocalVersion != protocol.Version1_3 {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError},
- dtlserrors.ErrInvalidProtocolVersionState
- }
- seq, msgs, items, ok := cache.fullPullMapItems(0, state.CipherSuite,
- handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
- )
- if !ok {
- // No valid message received. Keep reading
- return 0, nil, nil
- }
-
- // Connection Identifiers must be negotiated afresh on session resumption.
- // https://datatracker.ietf.org/doc/html/rfc9146#name-the-connection_id-extension
- state.SetLocalConnectionID(nil)
- state.RemoteConnectionID = nil
-
- state.HandshakeRecvSequence = seq
-
- var clientHello *handshake.MessageClientHello
-
- // Validate type
- if clientHello, ok = msgs[handshake.TypeClientHello].(*handshake.MessageClientHello); !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
-
- if !clientHello.Version.Equal(protocol.Version1_2) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion},
- dtlserrors.ErrUnsupportedProtocolVersion
- }
-
- state.RemoteRandom = clientHello.Random
-
- cipherSuites := []CipherSuite{}
- for _, id := range clientHello.CipherSuiteIDs {
- if id == renegotiationInfoSCSV {
- state.RemoteSupportsRenegotiation = true
-
- continue
- }
- if c := cipherSuiteForID(CipherSuiteID(id), cfg.customCipherSuites); c != nil {
- cipherSuites = append(cipherSuites, c)
- }
- }
-
- // nolint:godox
- // TODO: check for DTLS 1.3 cipher suites
- if state.CipherSuite, ok = findMatchingCipherSuite(cipherSuites, cfg.localCipherSuites); !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrCipherSuiteNoIntersection //nolint:lll
- }
-
- if failure := processClientHello13Extensions(state, cfg, clientHello); failure != nil {
- return 0, failure.alert, failure.err
- }
-
- if !slices.Contains(state.RemoteVersions, protocol.Version1_3) {
- // nolint:godox
- // TODO: This should actually handover the state machine to DTLS 1.2
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError},
- dtlserrors.ErrInvalidProtocolVersionState
- }
-
- // If the client doesn't support connection IDs, the server should not
- // expect one to be sent.
- if state.RemoteConnectionID == nil {
- state.SetLocalConnectionID(nil)
- }
-
- if cfg.extendedMasterSecret == RequireExtendedMasterSecret && !state.ExtendedMasterSecret {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, dtlserrors.ErrServerRequiredButNoClientEMS //nolint:lll
- }
-
- nextFlight := flight13_2
-
- // nolint:nestif
- if state.RemoteKeyEntries != nil && state.RemoteGroups != nil {
- // Overlapping groups between client and server
- var groups []elliptic.Curve
- for _, group := range state.RemoteGroups {
- if slices.Contains(cfg.ellipticCurves, group) {
- groups = append(groups, group)
- }
- }
- // Find key entry group in supported groups by client and server
- foundEntry := false
- for _, entry := range *state.RemoteKeyEntries {
- if slices.Contains(groups, entry.Group) {
- state.NamedCurve = entry.Group
- foundEntry = true
- // Ensure that first matching entry is chosen
- break
- }
- }
- if foundEntry && (state.LocalKeypair == nil || state.LocalKeypair.Curve != state.NamedCurve) {
- var err error
- state.LocalKeypair, err = elliptic.GenerateKeypair(state.NamedCurve)
- if err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, err
- }
- }
- }
-
- if cfg.insecureSkipHelloVerify {
- nextFlight = flight13_4
- }
-
- if err := appendInboundHandshakeCacheItems13(flightCtx.transcript, state.CipherSuite, items); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
-
- return nextFlight, nil, nil
-}
-
-// nolint:unparam
-func flight13_0Generate(
- _ flightConn,
- flightCtx *handshakeContext13,
-) ([]*packet, *alert.Alert, error) {
- state := flightCtx.state
- cfg := flightCtx.cfg
-
- if !cfg.insecureSkipHelloVerify {
- state.Cookie = make([]byte, cookieLength)
- if _, err := rand.Read(state.Cookie); err != nil {
- return nil, nil, err
- }
- }
-
- var zeroEpoch uint16
- state.LocalEpoch.Store(zeroEpoch)
- state.RemoteEpoch.Store(zeroEpoch)
- if len(cfg.ellipticCurves) < 1 {
- return nil, nil, dtlserrors.ErrEmptyEllipticCurves
- }
-
- if err := state.LocalRandom.Populate(); err != nil {
- return nil, nil, err
- }
-
- return nil, nil, nil
-}
-
-func flight13_2Parse(
- _ context.Context,
- _ flightConn,
- flightCtx *handshakeContext13,
-) (flightVal13, *alert.Alert, error) {
- seq, msgs, items, ok := flightCtx.cache.fullPullMapItems(
- flightCtx.state.HandshakeRecvSequence, flightCtx.state.CipherSuite,
- handshakeCachePullRule{handshake.TypeClientHello, flightCtx.cfg.initialEpoch, true, false},
- )
- if !ok {
- return 0, nil, nil
- }
-
- clientHello, ok := msgs[handshake.TypeClientHello].(*handshake.MessageClientHello)
- if !ok {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
- }
-
- if !clientHello.Version.Equal(protocol.Version1_2) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion},
- dtlserrors.ErrUnsupportedProtocolVersion
- }
-
- var cookie []byte
- for _, ext := range clientHello.Extensions {
- if cookieExt, ok := ext.(*extension.CookieExt); ok {
- cookie = cookieExt.Cookie
-
- break
- }
- }
-
- if len(cookie) == 0 {
- return 0, nil, nil
- }
- if !bytes.Equal(flightCtx.state.Cookie, cookie) {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.AccessDenied}, dtlserrors.ErrCookieMismatch
- }
-
- if failure := processClientHello13Extensions(flightCtx.state, flightCtx.cfg, clientHello); failure != nil {
- return 0, failure.alert, failure.err
- }
- if err := appendInboundHandshakeCacheItems13(flightCtx.transcript, flightCtx.state.CipherSuite, items); err != nil {
- return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
- }
- flightCtx.state.HandshakeRecvSequence = seq
-
- return flight13_4, nil, nil
-}
-
-func flight13_2Generate(
- _ flightConn,
- flightCtx *handshakeContext13,
-) ([]*packet, *alert.Alert, error) {
- flightCtx.state.HandshakeSendSequence = 0
- if flightCtx.state.CipherSuite == nil {
- return nil, nil, dtlserrors.ErrCipherSuiteUnset
- }
-
- random := handshake.Random{}
- random.UnmarshalFixed([32]byte(handshake.HelloRetryRequestRandom()))
-
- exts := []extension.Extension{}
-
- exts = append(exts, &extension.SupportedVersions{
- Versions: []protocol.Version{protocol.Version1_3},
- SelectedVersion: true,
- })
- cipherSuiteID := uint16(flightCtx.state.CipherSuite.ID())
-
- if flightCtx.state.NamedCurve != 0 {
- exts = append(exts, &extension.KeyShare{
- SelectedGroup: &flightCtx.state.NamedCurve,
- })
- }
-
- if len(flightCtx.state.Cookie) > 0 {
- exts = append(exts, &extension.CookieExt{
- Cookie: flightCtx.state.Cookie,
- })
- }
-
- return []*packet{
- {
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Message: &handshake.MessageServerHello{
- Version: protocol.Version1_2,
- Random: random,
- CipherSuiteID: &cipherSuiteID,
- CompressionMethod: defaultCompressionMethods()[0],
- Extensions: exts,
- },
- },
- },
- },
- }, nil, nil
-}
diff --git a/flighthandlers_server_13_test.go b/flighthandlers_server_13_test.go
deleted file mode 100644
index 079f55492..000000000
--- a/flighthandlers_server_13_test.go
+++ /dev/null
@@ -1,692 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
- "testing"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestFlight13_0ParseGeneratesKeypairForNegotiatedGroup(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- cfg.ellipticCurves = []elliptic.Curve{elliptic.P384, elliptic.P256}
-
- clientKeypair, err := elliptic.GenerateKeypair(elliptic.P384)
- require.NoError(t, err)
- staleServerKeypair, err := elliptic.GenerateKeypair(elliptic.X25519)
- require.NoError(t, err)
-
- clientHello := &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- Random: handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}},
- CipherSuiteIDs: []uint16{
- uint16(cfg.localCipherSuites[0].ID()),
- },
- CompressionMethods: defaultCompressionMethods(),
- Extensions: []extension.Extension{
- &extension.SupportedSignatureAlgorithms{
- SignatureHashAlgorithms: cfg.localSignatureSchemes,
- },
- &extension.SupportedEllipticCurves{
- EllipticCurves: []elliptic.Curve{elliptic.P384},
- },
- &extension.KeyShare{
- ClientShares: []extension.KeyShareEntry{
- {Group: elliptic.P384, KeyExchange: clientKeypair.PublicKey},
- },
- },
- &extension.SupportedVersions{
- Versions: []protocol.Version{protocol.Version1_3},
- },
- },
- }
- rawClientHello, err := (&handshake.Handshake{Message: clientHello}).Marshal()
- require.NoError(t, err)
-
- state := &dtlsstate.State{
- LocalVersion: protocol.Version1_3,
- NamedCurve: elliptic.X25519,
- LocalKeypair: staleServerKeypair,
- }
- cache := newHandshakeCache()
- cache.push(rawClientHello, cfg.initialEpoch, 0, handshake.TypeClientHello, true)
-
- nextFlight, dtlsAlert, err := flight13_0Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flight13_2, nextFlight)
- require.NotNil(t, state.LocalKeypair)
- assert.Equal(t, elliptic.P384, state.NamedCurve)
- assert.Equal(t, elliptic.P384, state.LocalKeypair.Curve)
-}
-
-func TestFlight13_0ParseRejectsClientHelloWithSelectedSupportedVersion(t *testing.T) {
- cfg := testHandshakeConfig13(t)
-
- clientHello := &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- Random: handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}},
- CipherSuiteIDs: []uint16{
- uint16(cfg.localCipherSuites[0].ID()),
- },
- CompressionMethods: defaultCompressionMethods(),
- Extensions: []extension.Extension{
- &extension.SupportedVersions{
- Versions: []protocol.Version{protocol.Version1_3},
- SelectedVersion: true,
- },
- },
- }
- rawClientHello, err := (&handshake.Handshake{Message: clientHello}).Marshal()
- require.NoError(t, err)
-
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3}
- cache := newHandshakeCache()
- cache.push(rawClientHello, cfg.initialEpoch, 0, handshake.TypeClientHello, true)
-
- nextFlight, dtlsAlert, err := flight13_0Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrInvalidClientHello)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, alert.Fatal, dtlsAlert.Level)
- assert.Equal(t, alert.IllegalParameter, dtlsAlert.Description)
- assert.Zero(t, nextFlight)
- assert.Empty(t, state.RemoteVersions)
-}
-
-func pushFlight13_0ClientHello(
- t *testing.T,
- cache *handshakeCache,
- cfg *handshakeConfig,
- exts []extension.Extension,
-) []byte {
- t.Helper()
-
- clientHello := &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- Random: handshake.Random{RandomBytes: [handshake.RandomBytesLength]byte{0x01}},
- CipherSuiteIDs: []uint16{
- uint16(cfg.localCipherSuites[0].ID()),
- },
- CompressionMethods: defaultCompressionMethods(),
- Extensions: exts,
- }
- rawClientHello, err := (&handshake.Handshake{Message: clientHello}).Marshal()
- require.NoError(t, err)
-
- cache.push(rawClientHello, cfg.initialEpoch, 0, handshake.TypeClientHello, true)
-
- return rawClientHello
-}
-
-func requiredClientHello13Extensions(t *testing.T, cfg *handshakeConfig) []extension.Extension {
- t.Helper()
-
- clientKeypair, err := elliptic.GenerateKeypair(cfg.ellipticCurves[0])
- require.NoError(t, err)
-
- return []extension.Extension{
- &extension.SupportedSignatureAlgorithms{
- SignatureHashAlgorithms: cfg.localSignatureSchemes,
- },
- &extension.SupportedEllipticCurves{
- EllipticCurves: cfg.ellipticCurves,
- },
- &extension.KeyShare{
- ClientShares: []extension.KeyShareEntry{
- {Group: clientKeypair.Curve, KeyExchange: clientKeypair.PublicKey},
- },
- },
- &extension.SupportedVersions{
- Versions: []protocol.Version{protocol.Version1_3},
- },
- }
-}
-
-func TestFlight13_0ParseRequiresCertificateAuthClientHelloExtensions(t *testing.T) {
- t.Run("AcceptsSignatureAlgorithmsAndSupportedGroups", func(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3}
- cache := newHandshakeCache()
- pushFlight13_0ClientHello(t, cache, cfg, requiredClientHello13Extensions(t, cfg))
-
- nextFlight, dtlsAlert, err := flight13_0Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flight13_2, nextFlight)
- assert.Equal(t, cfg.localSignatureSchemes, state.RemoteSignatureSchemes)
- assert.Equal(t, cfg.ellipticCurves, state.RemoteGroups)
- })
-
- t.Run("AllowsPreSharedKeyWithoutCertificateAuthExtensions", func(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3}
- cache := newHandshakeCache()
- binder := make([]byte, 32)
- pushFlight13_0ClientHello(t, cache, cfg, []extension.Extension{
- &extension.SupportedVersions{
- Versions: []protocol.Version{protocol.Version1_3},
- },
- &extension.PreSharedKey{
- Identities: []extension.PskIdentity{
- {Identity: []byte("psk"), ObfuscatedTicketAge: 0},
- },
- Binders: []extension.PskBinderEntry{binder},
- },
- })
-
- nextFlight, dtlsAlert, err := flight13_0Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flight13_2, nextFlight)
- })
-
- t.Run("RejectsMissingSignatureAlgorithms", func(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3}
- cache := newHandshakeCache()
- exts := requiredClientHello13Extensions(t, cfg)[1:]
- pushFlight13_0ClientHello(t, cache, cfg, exts)
-
- nextFlight, dtlsAlert, err := flight13_0Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrMissingClientHelloExtension)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.MissingExtension}, dtlsAlert)
- assert.Zero(t, nextFlight)
- })
-
- t.Run("RejectsSignatureAlgorithmsCertAsSubstitute", func(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3}
- cache := newHandshakeCache()
- exts := requiredClientHello13Extensions(t, cfg)[1:]
- exts = append([]extension.Extension{
- &extension.SignatureAlgorithmsCert{
- SignatureHashAlgorithms: cfg.localSignatureSchemes,
- },
- }, exts...)
- pushFlight13_0ClientHello(t, cache, cfg, exts)
-
- nextFlight, dtlsAlert, err := flight13_0Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrMissingClientHelloExtension)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.MissingExtension}, dtlsAlert)
- assert.Zero(t, nextFlight)
- })
-
- t.Run("RejectsMissingSupportedGroups", func(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3}
- cache := newHandshakeCache()
- required := requiredClientHello13Extensions(t, cfg)
- exts := []extension.Extension{required[0], required[2], required[3]}
- pushFlight13_0ClientHello(t, cache, cfg, exts)
-
- nextFlight, dtlsAlert, err := flight13_0Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- })
-
- require.ErrorIs(t, err, dtlserrors.ErrMissingClientHelloExtension)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.MissingExtension}, dtlsAlert)
- assert.Zero(t, nextFlight)
- })
-}
-
-func TestFlight13ServerParseAppendsNoHRRTranscriptOrder(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- cfg.insecureSkipHelloVerify = true
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3}
- cache := newHandshakeCache()
- rawClientHello := pushFlight13_0ClientHello(t, cache, cfg, requiredClientHello13Extensions(t, cfg))
- clientHelloCanonical, err := canonicalHandshake13(rawClientHello)
- require.NoError(t, err)
- transcript := newHandshakeTranscript13()
-
- nextFlight, dtlsAlert, err := flight13_0Parse(context.Background(), nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- transcript: transcript,
- })
-
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flight13_4, nextFlight)
- assert.Equal(t, []transcriptMessage13{
- {id: transcriptMessageID13{sender: transcriptClient13, seq: 0}, typ: handshake.TypeClientHello},
- }, transcript.order)
- assert.Equal(t, clientHelloCanonical, transcript.transcript)
-}
-
-func TestFlight13ServerParseAppendsHRRTranscriptOrder(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- cookie := []byte{0xde, 0xad, 0xbe, 0xef}
- state := &dtlsstate.State{
- LocalVersion: protocol.Version1_3,
- Cookie: cookie,
- }
- cache := newHandshakeCache()
- rawClientHello1 := pushFlight13_0ClientHello(t, cache, cfg, requiredClientHello13Extensions(t, cfg))
- clientHello1Canonical, err := canonicalHandshake13(rawClientHello1)
- require.NoError(t, err)
- transcript := newHandshakeTranscript13()
- flightCtx := &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- transcript: transcript,
- }
-
- nextFlight, dtlsAlert, err := flight13_0Parse(context.Background(), nil, flightCtx)
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flight13_2, nextFlight)
-
- helloRetryRequest, dtlsAlert, err := flight13_2Generate(nil, flightCtx)
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- require.Len(t, helloRetryRequest, 1)
- require.NoError(t, appendOutboundHandshakeFlight13(transcript, false, state.CipherSuite, helloRetryRequest))
- helloRetryRequestCanonical := canonicalPacketHandshake13(t, helloRetryRequest[0])
-
- exts := append(requiredClientHello13Extensions(t, cfg), &extension.CookieExt{Cookie: cookie})
- rawClientHello2 := pushClientHello13WithSequence(t, cache, protocol.Version1_2, 1, exts)
- clientHello2Canonical, err := canonicalHandshake13(rawClientHello2)
- require.NoError(t, err)
-
- nextFlight, dtlsAlert, err = flight13_2Parse(context.Background(), nil, flightCtx)
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flight13_4, nextFlight)
-
- clientHello1Hash := hashTranscript13(clientHello1Canonical)
- messageHash := canonicalTranscriptHandshake13(handshake.TypeMessageHash, clientHello1Hash)
- expectedTranscript := append(append(append([]byte(nil), messageHash...), helloRetryRequestCanonical...),
- clientHello2Canonical...)
- assert.Equal(t, []transcriptMessage13{
- {id: transcriptMessageID13{sender: transcriptClient13, seq: 0}, typ: handshake.TypeClientHello},
- {id: transcriptMessageID13{sender: transcriptServer13, seq: 0}, typ: handshake.TypeServerHello},
- {id: transcriptMessageID13{sender: transcriptClient13, seq: 1}, typ: handshake.TypeClientHello},
- }, transcript.order)
- assert.Equal(t, expectedTranscript, transcript.transcript)
-}
-
-func serverHelloFromFlight13_2(
- t *testing.T, state *dtlsstate.State, cfg *handshakeConfig,
-) *handshake.MessageServerHello {
- t.Helper()
-
- if state.CipherSuite == nil {
- state.CipherSuite = cfg.localCipherSuites[0]
- }
- pkts, dtlsAlert, err := flight13_2Generate(nil, flight13_2Context(state, newHandshakeCache(), cfg))
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- require.Len(t, pkts, 1)
-
- require.NotNil(t, pkts[0].record)
- assert.Equal(t, protocol.Version1_2, pkts[0].record.Header.Version)
-
- content, ok := pkts[0].record.Content.(*handshake.Handshake)
- require.True(t, ok)
-
- serverHello, ok := content.Message.(*handshake.MessageServerHello)
- require.True(t, ok)
-
- return serverHello
-}
-
-func findSupportedVersions(exts []extension.Extension) (*extension.SupportedVersions, bool) {
- for _, ext := range exts {
- if typed, ok := ext.(*extension.SupportedVersions); ok {
- return typed, true
- }
- }
-
- return nil, false
-}
-
-func findKeyShare(exts []extension.Extension) (*extension.KeyShare, bool) {
- for _, ext := range exts {
- if typed, ok := ext.(*extension.KeyShare); ok {
- return typed, true
- }
- }
-
- return nil, false
-}
-
-func findCookie(exts []extension.Extension) (*extension.CookieExt, bool) {
- for _, ext := range exts {
- if typed, ok := ext.(*extension.CookieExt); ok {
- return typed, true
- }
- }
-
- return nil, false
-}
-
-func TestFlight13_2Generate(t *testing.T) {
- t.Run("ServerHelloIsHelloRetryRequest", func(t *testing.T) {
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3}
- cfg := testHandshakeConfig13(t)
-
- serverHello := serverHelloFromFlight13_2(t, state, cfg)
-
- assert.Equal(t, protocol.Version1_2, serverHello.Version)
- assert.Equal(t, [32]byte(handshake.HelloRetryRequestRandom()), serverHello.Random.MarshalFixed())
- })
-
- t.Run("ResetsHandshakeSendSequence", func(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{
- LocalVersion: protocol.Version1_3,
- CipherSuite: cfg.localCipherSuites[0],
- HandshakeSendSequence: 7,
- }
-
- _, dtlsAlert, err := flight13_2Generate(nil, flight13_2Context(state, newHandshakeCache(), cfg))
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
-
- assert.Equal(t, 0, state.HandshakeSendSequence)
- })
-
- t.Run("RejectsWithoutCipherSuite", func(t *testing.T) {
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3}
- cfg := testHandshakeConfig13(t)
-
- pkts, dtlsAlert, err := flight13_2Generate(nil, flight13_2Context(state, newHandshakeCache(), cfg))
- require.ErrorIs(t, err, dtlserrors.ErrCipherSuiteUnset)
- require.Nil(t, dtlsAlert)
- require.Nil(t, pkts)
- })
-
- t.Run("AlwaysIncludesSupportedVersions", func(t *testing.T) {
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3}
- cfg := testHandshakeConfig13(t)
-
- serverHello := serverHelloFromFlight13_2(t, state, cfg)
-
- supportedVersions, ok := findSupportedVersions(serverHello.Extensions)
- require.True(t, ok, "SupportedVersions extension must always be present")
- assert.Equal(t, []protocol.Version{protocol.Version1_3}, supportedVersions.Versions)
- assert.True(t, supportedVersions.IsSelectedVersion())
- })
-
- t.Run("IncludesCipherSuiteAndCompressionMethod", func(t *testing.T) {
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3}
- cfg := testHandshakeConfig13(t)
-
- serverHello := serverHelloFromFlight13_2(t, state, cfg)
-
- require.NotNil(t, serverHello.CipherSuiteID)
- assert.Equal(t, uint16(cfg.localCipherSuites[0].ID()), *serverHello.CipherSuiteID)
- require.NotNil(t, serverHello.CompressionMethod)
- assert.Equal(t, defaultCompressionMethods()[0], serverHello.CompressionMethod)
-
- raw, err := (&handshake.Handshake{Message: serverHello}).Marshal()
- require.NoError(t, err)
-
- var parsed handshake.Handshake
- require.NoError(t, parsed.Unmarshal(raw))
- parsedServerHello, ok := parsed.Message.(*handshake.MessageServerHello)
- require.True(t, ok)
- require.NotNil(t, parsedServerHello.CipherSuiteID)
- assert.Equal(t, *serverHello.CipherSuiteID, *parsedServerHello.CipherSuiteID)
- })
-
- t.Run("OmitsKeyShareAndCookieByDefault", func(t *testing.T) {
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3}
- cfg := testHandshakeConfig13(t)
-
- serverHello := serverHelloFromFlight13_2(t, state, cfg)
-
- _, hasKeyShare := findKeyShare(serverHello.Extensions)
- assert.False(t, hasKeyShare, "KeyShare must be omitted when no remote key entries were offered")
-
- _, hasCookie := findCookie(serverHello.Extensions)
- assert.False(t, hasCookie, "Cookie must be omitted when no cookie is set")
-
- require.Len(t, serverHello.Extensions, 1)
- })
-
- t.Run("IncludesKeyShareWhenRemoteKeyEntriesPresent", func(t *testing.T) {
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3, NamedCurve: elliptic.X25519}
- cfg := testHandshakeConfig13(t)
-
- serverHello := serverHelloFromFlight13_2(t, state, cfg)
-
- keyShare, ok := findKeyShare(serverHello.Extensions)
- require.True(t, ok, "KeyShare must be present when remote key entries were offered")
- require.NotNil(t, keyShare.SelectedGroup)
- assert.Equal(t, elliptic.X25519, *keyShare.SelectedGroup)
- })
-
- t.Run("IncludesCookieWhenSet", func(t *testing.T) {
- cookie := []byte{0x01, 0x02, 0x03, 0x04}
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3, Cookie: cookie}
- cfg := testHandshakeConfig13(t)
-
- serverHello := serverHelloFromFlight13_2(t, state, cfg)
-
- cookieExt, ok := findCookie(serverHello.Extensions)
- require.True(t, ok, "Cookie must be present when set on state")
- assert.Equal(t, cookie, cookieExt.Cookie)
- })
-
- t.Run("IncludesAllExtensionsTogether", func(t *testing.T) {
- cookie := []byte{0xaa, 0xbb}
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3, NamedCurve: elliptic.P256, Cookie: cookie}
- cfg := testHandshakeConfig13(t)
-
- serverHello := serverHelloFromFlight13_2(t, state, cfg)
-
- require.Len(t, serverHello.Extensions, 3)
-
- supportedVersions, ok := findSupportedVersions(serverHello.Extensions)
- require.True(t, ok)
- assert.Equal(t, supportedVersionsRange(cfg.minVersion, cfg.maxVersion), supportedVersions.Versions)
-
- keyShare, ok := findKeyShare(serverHello.Extensions)
- require.True(t, ok)
- require.NotNil(t, keyShare.SelectedGroup)
- assert.Equal(t, elliptic.P256, *keyShare.SelectedGroup)
-
- cookieExt, ok := findCookie(serverHello.Extensions)
- require.True(t, ok)
- assert.Equal(t, cookie, cookieExt.Cookie)
- })
-}
-
-func pushClientHello13(
- t *testing.T,
- cache *handshakeCache,
- version protocol.Version,
- exts []extension.Extension,
-) {
- t.Helper()
-
- pushClientHello13WithSequence(t, cache, version, 0, exts)
-}
-
-func pushClientHello13WithSequence(
- t *testing.T,
- cache *handshakeCache,
- version protocol.Version,
- seq uint16,
- exts []extension.Extension,
-) []byte {
- t.Helper()
-
- content := &handshake.Handshake{
- Header: handshake.Header{MessageSequence: seq},
- Message: &handshake.MessageClientHello{
- Version: version,
- Random: handshake.Random{},
- CipherSuiteIDs: []uint16{},
- CompressionMethods: defaultCompressionMethods(),
- Extensions: exts,
- },
- }
-
- raw, err := content.Marshal()
- require.NoError(t, err)
-
- cache.push(raw, 0, seq, handshake.TypeClientHello, true)
-
- return raw
-}
-
-func flight13_2Context(state *dtlsstate.State, cache *handshakeCache, cfg *handshakeConfig) *handshakeContext13 {
- return &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- transcript: newHandshakeTranscript13(),
- }
-}
-
-func TestFlight13_2Parse(t *testing.T) {
- cookie := []byte{0xde, 0xad, 0xbe, 0xef}
-
- t.Run("AdvancesToFlight4OnMatchingCookie", func(t *testing.T) {
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3, Cookie: cookie}
- cache := newHandshakeCache()
- cfg := testHandshakeConfig13(t)
-
- exts := append(requiredClientHello13Extensions(t, cfg), &extension.CookieExt{Cookie: cookie})
- pushClientHello13(t, cache, protocol.Version1_2, exts)
-
- next, dtlsAlert, err := flight13_2Parse(context.Background(), nil, flight13_2Context(state, cache, cfg))
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flight13_4, next)
- assert.Equal(t, 1, state.HandshakeRecvSequence)
- })
-
- t.Run("KeepsWaitingWhenNoClientHelloCached", func(t *testing.T) {
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3, Cookie: cookie}
- cache := newHandshakeCache()
- cfg := testHandshakeConfig13(t)
-
- next, dtlsAlert, err := flight13_2Parse(context.Background(), nil, flight13_2Context(state, cache, cfg))
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flightVal13(0), next)
- assert.Equal(t, 0, state.HandshakeRecvSequence)
- })
-
- t.Run("KeepsWaitingWhenCookieNotYetEchoed", func(t *testing.T) {
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3, Cookie: cookie, ServerName: "original.example"}
- cache := newHandshakeCache()
- cfg := testHandshakeConfig13(t)
-
- exts := append(requiredClientHello13Extensions(t, cfg), &extension.ServerName{ServerName: "poison.example"})
- pushClientHello13(t, cache, protocol.Version1_2, exts)
-
- next, dtlsAlert, err := flight13_2Parse(context.Background(), nil, flight13_2Context(state, cache, cfg))
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- assert.Equal(t, flightVal13(0), next)
- assert.Equal(t, 0, state.HandshakeRecvSequence)
- assert.Equal(t, "original.example", state.ServerName)
- assert.Empty(t, state.RemoteSignatureSchemes)
- assert.Empty(t, state.RemoteGroups)
- })
-
- t.Run("RejectsCookieMismatch", func(t *testing.T) {
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3, Cookie: cookie, ServerName: "original.example"}
- cache := newHandshakeCache()
- cfg := testHandshakeConfig13(t)
-
- exts := append(requiredClientHello13Extensions(t, cfg), &extension.ServerName{ServerName: "poison.example"},
- &extension.CookieExt{Cookie: []byte{0x00, 0x01, 0x02, 0x03}})
- pushClientHello13(t, cache, protocol.Version1_2, exts)
-
- next, dtlsAlert, err := flight13_2Parse(context.Background(), nil, flight13_2Context(state, cache, cfg))
- require.ErrorIs(t, err, dtlserrors.ErrCookieMismatch)
- assert.Equal(t, flightVal13(0), next)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.AccessDenied}, dtlsAlert)
- assert.Equal(t, 0, state.HandshakeRecvSequence)
- assert.Equal(t, "original.example", state.ServerName)
- assert.Empty(t, state.RemoteSignatureSchemes)
- assert.Empty(t, state.RemoteGroups)
- })
-
- t.Run("RejectsUnsupportedVersion", func(t *testing.T) {
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3, Cookie: cookie}
- cache := newHandshakeCache()
- cfg := testHandshakeConfig13(t)
-
- pushClientHello13(t, cache, protocol.Version{Major: 0xfe, Minor: 0xfd - 1}, []extension.Extension{
- &extension.CookieExt{Cookie: cookie},
- })
-
- next, dtlsAlert, err := flight13_2Parse(context.Background(), nil, flight13_2Context(state, cache, cfg))
- require.ErrorIs(t, err, dtlserrors.ErrUnsupportedProtocolVersion)
- assert.Equal(t, flightVal13(0), next)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion}, dtlsAlert)
- })
-
- t.Run("RejectsMissingCertificateAuthExtensions", func(t *testing.T) {
- state := &dtlsstate.State{LocalVersion: protocol.Version1_3, Cookie: cookie}
- cache := newHandshakeCache()
- cfg := testHandshakeConfig13(t)
-
- pushClientHello13(t, cache, protocol.Version1_2, []extension.Extension{
- &extension.CookieExt{Cookie: cookie},
- })
-
- next, dtlsAlert, err := flight13_2Parse(context.Background(), nil, flight13_2Context(state, cache, cfg))
- require.ErrorIs(t, err, dtlserrors.ErrMissingClientHelloExtension)
- assert.Equal(t, flightVal13(0), next)
- require.NotNil(t, dtlsAlert)
- assert.Equal(t, &alert.Alert{Level: alert.Fatal, Description: alert.MissingExtension}, dtlsAlert)
- })
-}
diff --git a/fragment_buffer.go b/fragment_buffer.go
deleted file mode 100644
index 9c5f8b2cc..000000000
--- a/fragment_buffer.go
+++ /dev/null
@@ -1,152 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
-)
-
-const (
- // 2 megabytes.
- fragmentBufferMaxSize = 2000000
- fragmentBufferMaxCount = 1000
-)
-
-type fragment struct {
- recordLayerHeader recordlayer.Header
- handshakeHeader handshake.Header
- data []byte
-}
-
-type fragments struct {
- fragmentByOffset map[uint32]*fragment
- fragmentsLength uint32
- handshakeLength uint32
-}
-
-type fragmentBuffer struct {
- // map of MessageSequenceNumbers that hold slices of fragments
- cache map[uint16]*fragments
-
- currentMessageSequenceNumber uint16
-
- totalBufferSize int
- totalFragmentCount int
-}
-
-func newFragmentBuffer() *fragmentBuffer {
- return &fragmentBuffer{cache: map[uint16]*fragments{}}
-}
-
-// current total size of buffer.
-func (f *fragmentBuffer) size() int {
- return f.totalBufferSize
-}
-
-// Attempts to push a DTLS packet to the fragmentBuffer
-// when it returns true it means the fragmentBuffer has inserted and the buffer shouldn't be handled
-// when an error returns it is fatal, and the DTLS connection should be stopped.
-func (f *fragmentBuffer) push(buf []byte) (isHandshake, isRetransmit bool, err error) { //nolint:cyclop
- if f.size()+len(buf) >= fragmentBufferMaxSize || f.totalFragmentCount >= fragmentBufferMaxCount {
- return false, false, dtlserrors.ErrFragmentBufferOverflow
- }
-
- recordLayerHeader := recordlayer.Header{}
- if err := recordLayerHeader.Unmarshal(buf); err != nil {
- return false, false, err
- }
-
- // fragment isn't a handshake, we don't need to handle it
- if recordLayerHeader.ContentType != protocol.ContentTypeHandshake {
- return false, false, nil
- }
-
- frag := new(fragment)
- for buf = buf[recordlayer.FixedHeaderSize:]; len(buf) != 0; frag = new(fragment) { //nolint:gosec // G602
- if err := frag.handshakeHeader.Unmarshal(buf); err != nil {
- return false, false, err
- }
-
- // Fragment is a retransmission. We have already assembled it before successfully
- isRetransmit = frag.handshakeHeader.FragmentOffset == 0 &&
- frag.handshakeHeader.MessageSequence < f.currentMessageSequenceNumber
-
- end := int(handshake.HeaderLength + frag.handshakeHeader.FragmentLength)
- if end > len(buf) {
- return false, false, dtlserrors.ErrBufferTooSmall
- }
- if frag.handshakeHeader.MessageSequence < f.currentMessageSequenceNumber {
- buf = buf[end:]
-
- continue
- }
-
- messageFragments, ok := f.cache[frag.handshakeHeader.MessageSequence]
- if !ok {
- messageFragments = &fragments{
- fragmentByOffset: map[uint32]*fragment{}, handshakeLength: frag.handshakeHeader.Length,
- }
- f.cache[frag.handshakeHeader.MessageSequence] = messageFragments
- }
-
- // Discard all headers, when rebuilding the packet we will re-build
- frag.data = append([]byte{}, buf[handshake.HeaderLength:end]...)
- frag.recordLayerHeader = recordLayerHeader
-
- if _, ok = messageFragments.fragmentByOffset[frag.handshakeHeader.FragmentOffset]; !ok {
- messageFragments.fragmentByOffset[frag.handshakeHeader.FragmentOffset] = frag
- messageFragments.fragmentsLength += frag.handshakeHeader.FragmentLength
- f.totalBufferSize += int(frag.handshakeHeader.FragmentLength)
- f.totalFragmentCount++
- }
- buf = buf[end:]
- }
-
- return true, isRetransmit, nil
-}
-
-func (f *fragmentBuffer) pop() (content []byte, epoch uint16) {
- frags, ok := f.cache[f.currentMessageSequenceNumber]
- if !ok {
- return nil, 0
- }
-
- if frags.fragmentsLength != frags.handshakeLength {
- return nil, 0
- }
-
- var rawMessage []byte
- targetOffset := uint32(0)
- for i := 0; i < len(frags.fragmentByOffset) && targetOffset < frags.handshakeLength; i++ {
- if frag, ok := frags.fragmentByOffset[targetOffset]; ok {
- rawMessage = append(rawMessage, frag.data...)
- targetOffset = frag.handshakeHeader.FragmentOffset + frag.handshakeHeader.FragmentLength
- } else {
- return nil, 0
- }
- }
-
- if int(frags.handshakeLength) != len(rawMessage) {
- return nil, 0
- }
-
- firstHeader := frags.fragmentByOffset[0].handshakeHeader
- firstHeader.FragmentOffset = 0
- firstHeader.FragmentLength = firstHeader.Length
-
- rawHeader, _ := firstHeader.Marshal()
-
- messageEpoch := frags.fragmentByOffset[0].recordLayerHeader.Epoch
-
- f.totalBufferSize -= int(frags.fragmentsLength)
- f.totalFragmentCount -= len(frags.fragmentByOffset)
-
- delete(f.cache, f.currentMessageSequenceNumber)
- f.currentMessageSequenceNumber++
-
- return append(rawHeader, rawMessage...), messageEpoch
-}
diff --git a/fragment_buffer_test.go b/fragment_buffer_test.go
deleted file mode 100644
index e1a9c60e7..000000000
--- a/fragment_buffer_test.go
+++ /dev/null
@@ -1,202 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "testing"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- "github.com/stretchr/testify/assert"
-)
-
-func TestFragmentBuffer(t *testing.T) {
- for _, test := range []struct {
- Name string
- In [][]byte
- Expected [][]byte
- Epoch uint16
- }{
- {
- Name: "Single Fragment",
- In: [][]byte{
- {
- 0x16, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x03,
- 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, 0x00,
- },
- },
- Expected: [][]byte{
- {0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, 0x00},
- },
- Epoch: 0,
- },
- {
- Name: "Single Fragment Epoch 3",
- In: [][]byte{
- {
- 0x16, 0xfe, 0xff, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x03,
- 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, 0x00,
- },
- },
- Expected: [][]byte{
- {0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, 0x00},
- },
- Epoch: 3,
- },
- {
- Name: "Multiple Fragments",
- In: [][]byte{
- {
- 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0x0b, 0x00,
- 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x01, 0x02, 0x03, 0x04,
- },
- {
- 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0x0b, 0x00,
- 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x05, 0x06, 0x07, 0x08, 0x09,
- },
- {
- 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0x0b, 0x00,
- 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x05, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
- },
- },
- Expected: [][]byte{
- {
- 0x0b, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x01,
- 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
- },
- },
- Epoch: 0,
- },
- {
- Name: "Multiple Unordered Fragments",
- In: [][]byte{
- {
- 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0x0b, 0x00,
- 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x01, 0x02, 0x03, 0x04,
- },
- {
- 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0x0b, 0x00,
- 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x05, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
- },
- {
- 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x81, 0x0b, 0x00,
- 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x05, 0x05, 0x06, 0x07, 0x08, 0x09,
- },
- },
- Expected: [][]byte{
- {
- 0x0b, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x01,
- 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
- },
- },
- Epoch: 0,
- },
- {
- Name: "Multiple Handshakes in Single Fragment",
- In: [][]byte{
- {
- 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x30, /* record header */
- 0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, 0x01, 0x01, /*handshake msg 1*/
- 0x03, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, 0x01, 0x01, /*handshake msg 2*/
- 0x03, 0x00, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, 0x01, 0x01, /*handshake msg 3*/
- },
- },
- Expected: [][]byte{
- {0x03, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, 0x01, 0x01},
- {0x03, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, 0x01, 0x01},
- {0x03, 0x00, 0x00, 0x04, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, 0x01, 0x01},
- },
- Epoch: 0,
- },
- // Assert that a zero length fragment doesn't cause the fragmentBuffer to enter an infinite loop
- {
- Name: "Zero Length Fragment",
- In: [][]byte{
- {
- 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- },
- },
- Expected: [][]byte{
- {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
- },
- Epoch: 0,
- },
- // Not aligned fragments should not be reassembled
- {
- Name: "Not Aligned Fragments",
- In: [][]byte{
- {
- 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0x0b, 0x00,
- 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x01, 0x02, 0x03, 0x04,
- },
- {
- 0x16, 0xfe, 0xfd, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x81, 0x0b, 0x00,
- 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x05, 0x05, 0x06, 0x07, 0x08, 0x09,
- },
- },
- Expected: [][]byte{nil},
- Epoch: 0,
- },
- } {
- fragmentBuffer := newFragmentBuffer()
- for _, frag := range test.In {
- status, _, err := fragmentBuffer.push(frag)
- assert.NoError(t, err)
- assert.Truef(t, status, "fragmentBuffer didn't accept fragments for '%s'", test.Name)
- }
-
- for _, expected := range test.Expected {
- out, epoch := fragmentBuffer.pop()
- assert.Equalf(t, expected, out, "fragmentBuffer '%s' pop should return expected output", test.Name)
- assert.Equalf(t, test.Epoch, epoch, "fragmentBuffer returend wrong epoch")
- }
-
- frag, _ := fragmentBuffer.pop()
- assert.Nilf(t, frag, "fragmentBuffer '%s' pop should return nil when no more fragments are available", test.Name)
- }
-}
-
-func TestFragmentBuffer_Overflow(t *testing.T) {
- fragmentBuffer := newFragmentBuffer()
-
- // Push a buffer that doesn't exceed size limits
- _, _, err := fragmentBuffer.push([]byte{
- 0x16, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x03,
- 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xfe, 0xff, 0x00,
- })
- assert.NoError(t, err)
-
- // Allocate a buffer that exceeds cache size
- largeBuffer := make([]byte, fragmentBufferMaxSize)
- _, _, err = fragmentBuffer.push(largeBuffer)
- assert.ErrorIs(t, err, dtlserrors.ErrFragmentBufferOverflow, "Pushing a large buffer should return an overflow error")
-}
-
-func TestFragmentBuffer_TooSmall(t *testing.T) {
- fragmentBuffer := newFragmentBuffer()
-
- // Push a buffer that is smaller than fragment length
- _, _, err := fragmentBuffer.push([]byte{
- 0x16, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x03,
- 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0xfe, 0xff, 0x00,
- })
- assert.ErrorIs(t, err, dtlserrors.ErrBufferTooSmall,
- "Pushing a buffer that is smaller than fragment length should return an error")
-}
-
-func TestFragmentBuffer_UnmarshalInvalid(t *testing.T) {
- fragmentBuffer := newFragmentBuffer()
-
- // Push a buffer with partial record layer header
- _, _, err := fragmentBuffer.push([]byte{
- 0x16, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- })
- assert.Error(t, err, "Pushing a buffer with partial record layer header should return an error")
-
- // Push a buffer with partial handshake header
- _, _, err = fragmentBuffer.push([]byte{
- 0x16, 0xfe, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x03,
- })
- assert.Error(t, err, "Pushing a buffer with partial handshake header should return an error")
-}
diff --git a/go.mod b/go.mod
index 01b71803a..08a489bde 100644
--- a/go.mod
+++ b/go.mod
@@ -1,24 +1,14 @@
-module github.com/pion/dtls/v3
+module github.com/pion/dtls/v4
require (
github.com/pion/logging v0.2.4
- github.com/pion/transport/v4 v4.0.2
- github.com/stretchr/testify v1.11.1
+ github.com/pion/transport/v5 v5.1.1
+ github.com/stretchr/testify v1.12.1
golang.org/x/crypto v0.48.0
- golang.org/x/net v0.49.0
+ golang.org/x/sys v0.41.0
+ golang.org/x/term v0.40.0
)
-require (
- github.com/davecgh/go-spew v1.1.1 // indirect
- github.com/pmezard/go-difflib v1.0.0 // indirect
- golang.org/x/sys v0.41.0 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
-)
+require go.yaml.in/yaml/v3 v3.0.5 // indirect
go 1.24.0
-
-// Retract version with broken RSA interop with OpenSSL DTLS 1.2.
-retract v3.1.0
-
-// Retract version with broken interoperability with firefox.
-retract v3.1.3
diff --git a/go.sum b/go.sum
index c34341904..b7b7259cb 100644
--- a/go.sum
+++ b/go.sum
@@ -1,20 +1,16 @@
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
-github.com/pion/transport/v4 v4.0.2 h1:ifYlPqNwsy6aKQ9y8yzxXlHae5431ZrH2avkD/Rn6Tk=
-github.com/pion/transport/v4 v4.0.2/go.mod h1:06hFI+jCFcok2X2MekVufNZ/uzNZXivGBPfviSVcjgM=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
-github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/pion/transport/v5 v5.1.1 h1:Sauq8YJqRopgRfIgnpfKpdpKG14g0N4jWlRgpWsEfN8=
+github.com/pion/transport/v5 v5.1.1/go.mod h1:Qxw6fCEjFWQkRDZOhS4Vf+neJBcihauvA3uyEa1J1F0=
+github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
+github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
+go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
+go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
-gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
+golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
diff --git a/handshake_cache.go b/handshake_cache.go
deleted file mode 100644
index 55cfbbf66..000000000
--- a/handshake_cache.go
+++ /dev/null
@@ -1,235 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "sync"
-
- "github.com/pion/dtls/v3/pkg/crypto/prf"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
-)
-
-type handshakeCacheItem struct {
- typ handshake.Type
- isClient bool
- epoch uint16
- messageSequence uint16
- data []byte
-}
-
-type handshakeCachePullRule struct {
- typ handshake.Type
- epoch uint16
- isClient bool
- optional bool
-}
-
-type handshakeCache struct {
- cache []*handshakeCacheItem
- mu sync.Mutex
-}
-
-func newHandshakeCache() *handshakeCache {
- return &handshakeCache{}
-}
-
-func (h *handshakeCache) push(data []byte, epoch, messageSequence uint16, typ handshake.Type, isClient bool) {
- h.mu.Lock()
- defer h.mu.Unlock()
-
- h.cache = append(h.cache, &handshakeCacheItem{
- data: data,
- epoch: epoch,
- messageSequence: messageSequence,
- typ: typ,
- isClient: isClient,
- })
-}
-
-// returns a list handshakes that match the requested rules
-// the list will contain null entries for rules that can't be satisfied
-// multiple entries may match a rule, but only the last match is returned (ie ClientHello with cookies).
-func (h *handshakeCache) pull(rules ...handshakeCachePullRule) []*handshakeCacheItem {
- h.mu.Lock()
- defer h.mu.Unlock()
-
- out := make([]*handshakeCacheItem, len(rules))
- for i, r := range rules {
- for _, c := range h.cache {
- if c.typ == r.typ && c.isClient == r.isClient && c.epoch == r.epoch {
- switch {
- case out[i] == nil:
- out[i] = c
- case out[i].messageSequence < c.messageSequence:
- out[i] = c
- }
- }
- }
- }
-
- return out
-}
-
-// fullPullMap pulls all handshakes between rules[0] to rules[len(rules)-1] as map.
-func (h *handshakeCache) fullPullMap(
- startSeq int,
- cipherSuite CipherSuite,
- rules ...handshakeCachePullRule,
-) (int, map[handshake.Type]handshake.Message, bool) {
- seq, msgs, _, ok := h.fullPullMapItems(startSeq, cipherSuite, rules...)
-
- return seq, msgs, ok
-}
-
-func (h *handshakeCache) fullPullMapItems(
- startSeq int,
- cipherSuite CipherSuite,
- rules ...handshakeCachePullRule,
-) (int, map[handshake.Type]handshake.Message, []*handshakeCacheItem, bool) {
- h.mu.Lock()
- defer h.mu.Unlock()
-
- ci, ok := h.pullLastCacheItems(rules)
- if !ok {
- return startSeq, nil, nil, false
- }
-
- return fullPullMapCacheItems(startSeq, cipherSuite, rules, ci)
-}
-
-func (h *handshakeCache) pullLastCacheItems(rules []handshakeCachePullRule) ([]*handshakeCacheItem, bool) {
- items := make([]*handshakeCacheItem, len(rules))
- for i, rule := range rules {
- items[i] = h.lastCacheItemForRule(rule)
- if !rule.optional && items[i] == nil {
- return nil, false
- }
- }
-
- return items, true
-}
-
-func (h *handshakeCache) lastCacheItemForRule(rule handshakeCachePullRule) *handshakeCacheItem {
- var last *handshakeCacheItem
- for _, c := range h.cache {
- if !cacheItemMatchesRule(c, rule) {
- continue
- }
- if last == nil || last.messageSequence < c.messageSequence {
- last = c
- }
- }
-
- return last
-}
-
-func cacheItemMatchesRule(item *handshakeCacheItem, rule handshakeCachePullRule) bool {
- return item.typ == rule.typ && item.isClient == rule.isClient && item.epoch == rule.epoch
-}
-
-func fullPullMapCacheItems(
- startSeq int,
- cipherSuite CipherSuite,
- rules []handshakeCachePullRule,
- ci []*handshakeCacheItem,
-) (int, map[handshake.Type]handshake.Message, []*handshakeCacheItem, bool) {
- out := make(map[handshake.Type]handshake.Message)
- items := make([]*handshakeCacheItem, 0, len(rules))
- seq := startSeq
- keyExchangeAlgorithm := keyExchangeAlgorithmForCipherSuite(cipherSuite)
- for i, r := range rules {
- typ := r.typ
- item := ci[i]
- if item == nil {
- continue
- }
- rawHandshake, ok := unmarshalCachedHandshake(item, keyExchangeAlgorithm)
- if !ok {
- return startSeq, nil, nil, false
- }
- if uint16(seq) != rawHandshake.Header.MessageSequence { //nolint:gosec // G115
- // There is a gap. Some messages are not arrived.
- return startSeq, nil, nil, false
- }
- seq++
- out[typ] = rawHandshake.Message
- items = append(items, item)
- }
- if len(items) == 0 {
- return seq, nil, nil, false
- }
-
- return seq, out, items, true
-}
-
-func keyExchangeAlgorithmForCipherSuite(cipherSuite CipherSuite) CipherSuiteKeyExchangeAlgorithm {
- if cipherSuite == nil {
- return 0
- }
-
- return cipherSuite.KeyExchangeAlgorithm()
-}
-
-func unmarshalCachedHandshake(
- item *handshakeCacheItem,
- keyExchangeAlgorithm CipherSuiteKeyExchangeAlgorithm,
-) (*handshake.Handshake, bool) {
- rawHandshake := &handshake.Handshake{
- KeyExchangeAlgorithm: keyExchangeAlgorithm,
- }
- if err := rawHandshake.Unmarshal(item.data); err != nil {
- return nil, false
- }
-
- return rawHandshake, true
-}
-
-// pullAndMerge calls pull and then merges the results, ignoring any null entries.
-func (h *handshakeCache) pullAndMerge(rules ...handshakeCachePullRule) []byte {
- merged := []byte{}
-
- for _, p := range h.pull(rules...) {
- if p != nil {
- merged = append(merged, p.data...)
- }
- }
-
- return merged
-}
-
-// sessionHash returns the session hash for Extended Master Secret support
-// https://tools.ietf.org/html/draft-ietf-tls-session-hash-06#section-4
-func (h *handshakeCache) sessionHash(hf prf.HashFunc, epoch uint16, additional ...[]byte) ([]byte, error) {
- merged := []byte{}
-
- // Order defined by https://tools.ietf.org/html/rfc5246#section-7.3
- handshakeBuffer := h.pull(
- handshakeCachePullRule{handshake.TypeClientHello, epoch, true, false},
- handshakeCachePullRule{handshake.TypeServerHello, epoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificate, epoch, false, false},
- handshakeCachePullRule{handshake.TypeServerKeyExchange, epoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificateRequest, epoch, false, false},
- handshakeCachePullRule{handshake.TypeServerHelloDone, epoch, false, false},
- handshakeCachePullRule{handshake.TypeCertificate, epoch, true, false},
- handshakeCachePullRule{handshake.TypeClientKeyExchange, epoch, true, false},
- )
-
- for _, p := range handshakeBuffer {
- if p == nil {
- continue
- }
-
- merged = append(merged, p.data...)
- }
- for _, a := range additional {
- merged = append(merged, a...)
- }
-
- hash := hf()
- if _, err := hash.Write(merged); err != nil {
- return []byte{}, err
- }
-
- return hash.Sum(nil), nil
-}
diff --git a/handshake_cache_test.go b/handshake_cache_test.go
deleted file mode 100644
index 565baf22b..000000000
--- a/handshake_cache_test.go
+++ /dev/null
@@ -1,264 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "testing"
-
- "github.com/pion/dtls/v3/internal/ciphersuite"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestHandshakeCacheSinglePush(t *testing.T) {
- for _, test := range []struct {
- Name string
- Rule []handshakeCachePullRule
- Input []handshakeCacheItem
- Expected []byte
- }{
- {
- Name: "Single Push",
- Input: []handshakeCacheItem{
- {0, true, 0, 0, []byte{0x00}},
- },
- Rule: []handshakeCachePullRule{
- {0, 0, true, false},
- },
- Expected: []byte{0x00},
- },
- {
- Name: "Multi Push",
- Input: []handshakeCacheItem{
- {0, true, 0, 0, []byte{0x00}},
- {1, true, 0, 1, []byte{0x01}},
- {2, true, 0, 2, []byte{0x02}},
- },
- Rule: []handshakeCachePullRule{
- {0, 0, true, false},
- {1, 0, true, false},
- {2, 0, true, false},
- },
- Expected: []byte{0x00, 0x01, 0x02},
- },
- {
- Name: "Multi Push, Rules set order",
- Input: []handshakeCacheItem{
- {2, true, 0, 2, []byte{0x02}},
- {0, true, 0, 0, []byte{0x00}},
- {1, true, 0, 1, []byte{0x01}},
- },
- Rule: []handshakeCachePullRule{
- {0, 0, true, false},
- {1, 0, true, false},
- {2, 0, true, false},
- },
- Expected: []byte{0x00, 0x01, 0x02},
- },
-
- {
- Name: "Multi Push, Dupe Seqnum",
- Input: []handshakeCacheItem{
- {0, true, 0, 0, []byte{0x00}},
- {1, true, 0, 1, []byte{0x01}},
- {1, true, 0, 1, []byte{0x01}},
- },
- Rule: []handshakeCachePullRule{
- {0, 0, true, false},
- {1, 0, true, false},
- },
- Expected: []byte{0x00, 0x01},
- },
- {
- Name: "Multi Push, Dupe Seqnum Client/Server",
- Input: []handshakeCacheItem{
- {0, true, 0, 0, []byte{0x00}},
- {1, true, 0, 1, []byte{0x01}},
- {1, false, 0, 1, []byte{0x02}},
- },
- Rule: []handshakeCachePullRule{
- {0, 0, true, false},
- {1, 0, true, false},
- {1, 0, false, false},
- },
- Expected: []byte{0x00, 0x01, 0x02},
- },
- {
- Name: "Multi Push, Dupe Seqnum with Unique HandshakeType",
- Input: []handshakeCacheItem{
- {1, true, 0, 0, []byte{0x00}},
- {2, true, 0, 1, []byte{0x01}},
- {3, false, 0, 0, []byte{0x02}},
- },
- Rule: []handshakeCachePullRule{
- {1, 0, true, false},
- {2, 0, true, false},
- {3, 0, false, false},
- },
- Expected: []byte{0x00, 0x01, 0x02},
- },
- {
- Name: "Multi Push, Wrong epoch",
- Input: []handshakeCacheItem{
- {1, true, 0, 0, []byte{0x00}},
- {2, true, 1, 1, []byte{0x01}},
- {2, true, 0, 2, []byte{0x11}},
- {3, false, 0, 0, []byte{0x02}},
- {3, false, 1, 0, []byte{0x12}},
- {3, false, 2, 0, []byte{0x12}},
- },
- Rule: []handshakeCachePullRule{
- {1, 0, true, false},
- {2, 1, true, false},
- {3, 0, false, false},
- },
- Expected: []byte{0x00, 0x01, 0x02},
- },
- } {
- h := newHandshakeCache()
- for _, i := range test.Input {
- h.push(i.data, i.epoch, i.messageSequence, i.typ, i.isClient)
- }
- verifyData := h.pullAndMerge(test.Rule...)
- assert.Equal(t, test.Expected, verifyData)
- }
-}
-
-func TestHandshakeCacheFullPullMapItemsReturnsAcceptedRawItems(t *testing.T) {
- cipherSuiteID := uint16(TLS_AES_128_GCM_SHA256)
- rawClientHello := marshalHandshakeCacheTestMessage(t, 0, &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- CipherSuiteIDs: []uint16{uint16(TLS_AES_128_GCM_SHA256)},
- CompressionMethods: defaultCompressionMethods(),
- })
- rawServerHello := marshalHandshakeCacheTestMessage(t, 1, &handshake.MessageServerHello{
- Version: protocol.Version1_2,
- CipherSuiteID: &cipherSuiteID,
- CompressionMethod: defaultCompressionMethods()[0],
- })
-
- cache := newHandshakeCache()
- cache.push(rawServerHello, 0, 1, handshake.TypeServerHello, false)
- cache.push(rawClientHello, 0, 0, handshake.TypeClientHello, true)
-
- seq, msgs, items, ok := cache.fullPullMapItems(0, nil,
- handshakeCachePullRule{handshake.TypeClientHello, 0, true, false},
- handshakeCachePullRule{handshake.TypeServerHello, 0, false, false},
- )
-
- require.True(t, ok)
- assert.Equal(t, 2, seq)
- require.IsType(t, &handshake.MessageClientHello{}, msgs[handshake.TypeClientHello])
- require.IsType(t, &handshake.MessageServerHello{}, msgs[handshake.TypeServerHello])
- require.Len(t, items, 2)
- assert.Equal(t, rawClientHello, items[0].data)
- assert.Equal(t, rawServerHello, items[1].data)
-}
-
-func marshalHandshakeCacheTestMessage(t *testing.T, seq uint16, message handshake.Message) []byte {
- t.Helper()
-
- raw, err := (&handshake.Handshake{
- Header: handshake.Header{MessageSequence: seq},
- Message: message,
- }).Marshal()
- require.NoError(t, err)
-
- return raw
-}
-
-func TestHandshakeCacheSessionHash(t *testing.T) {
- for _, test := range []struct {
- Name string
- Rule []handshakeCachePullRule
- Input []handshakeCacheItem
- Expected []byte
- }{
- {
- Name: "Standard Handshake",
- Input: []handshakeCacheItem{
- {handshake.TypeClientHello, true, 0, 0, []byte{0x00}},
- {handshake.TypeServerHello, false, 0, 1, []byte{0x01}},
- {handshake.TypeCertificate, false, 0, 2, []byte{0x02}},
- {handshake.TypeServerKeyExchange, false, 0, 3, []byte{0x03}},
- {handshake.TypeServerHelloDone, false, 0, 4, []byte{0x04}},
- {handshake.TypeClientKeyExchange, true, 0, 5, []byte{0x05}},
- },
- Expected: []byte{
- 0x17, 0xe8, 0x8d, 0xb1, 0x87, 0xaf, 0xd6, 0x2c, 0x16, 0xe5, 0xde, 0xbf, 0x3e, 0x65, 0x27, 0xcd,
- 0x00, 0x6b, 0xc0, 0x12, 0xbc, 0x90, 0xb5, 0x1a, 0x81, 0x0c, 0xd8, 0x0c, 0x2d, 0x51, 0x1f, 0x43,
- },
- },
- {
- Name: "Handshake With Client Cert Request",
- Input: []handshakeCacheItem{
- {handshake.TypeClientHello, true, 0, 0, []byte{0x00}},
- {handshake.TypeServerHello, false, 0, 1, []byte{0x01}},
- {handshake.TypeCertificate, false, 0, 2, []byte{0x02}},
- {handshake.TypeServerKeyExchange, false, 0, 3, []byte{0x03}},
- {handshake.TypeCertificateRequest, false, 0, 4, []byte{0x04}},
- {handshake.TypeServerHelloDone, false, 0, 5, []byte{0x05}},
- {handshake.TypeClientKeyExchange, true, 0, 6, []byte{0x06}},
- },
- Expected: []byte{
- 0x57, 0x35, 0x5a, 0xc3, 0x30, 0x3c, 0x14, 0x8f, 0x11, 0xae, 0xf7, 0xcb, 0x17, 0x94, 0x56, 0xb9,
- 0x23, 0x2c, 0xde, 0x33, 0xa8, 0x18, 0xdf, 0xda, 0x2c, 0x2f, 0xcb, 0x93, 0x25, 0x74, 0x9a, 0x6b,
- },
- },
- {
- Name: "Handshake Ignores after ClientKeyExchange",
- Input: []handshakeCacheItem{
- {handshake.TypeClientHello, true, 0, 0, []byte{0x00}},
- {handshake.TypeServerHello, false, 0, 1, []byte{0x01}},
- {handshake.TypeCertificate, false, 0, 2, []byte{0x02}},
- {handshake.TypeServerKeyExchange, false, 0, 3, []byte{0x03}},
- {handshake.TypeCertificateRequest, false, 0, 4, []byte{0x04}},
- {handshake.TypeServerHelloDone, false, 0, 5, []byte{0x05}},
- {handshake.TypeClientKeyExchange, true, 0, 6, []byte{0x06}},
- {handshake.TypeCertificateVerify, true, 0, 7, []byte{0x07}},
- {handshake.TypeFinished, true, 1, 7, []byte{0x08}},
- {handshake.TypeFinished, false, 1, 7, []byte{0x09}},
- },
- Expected: []byte{
- 0x57, 0x35, 0x5a, 0xc3, 0x30, 0x3c, 0x14, 0x8f, 0x11, 0xae, 0xf7, 0xcb, 0x17, 0x94, 0x56, 0xb9,
- 0x23, 0x2c, 0xde, 0x33, 0xa8, 0x18, 0xdf, 0xda, 0x2c, 0x2f, 0xcb, 0x93, 0x25, 0x74, 0x9a, 0x6b,
- },
- },
- {
- Name: "Handshake Ignores wrong epoch",
- Input: []handshakeCacheItem{
- {handshake.TypeClientHello, true, 0, 0, []byte{0x00}},
- {handshake.TypeServerHello, false, 0, 1, []byte{0x01}},
- {handshake.TypeCertificate, false, 0, 2, []byte{0x02}},
- {handshake.TypeServerKeyExchange, false, 0, 3, []byte{0x03}},
- {handshake.TypeCertificateRequest, false, 0, 4, []byte{0x04}},
- {handshake.TypeServerHelloDone, false, 0, 5, []byte{0x05}},
- {handshake.TypeClientKeyExchange, true, 0, 6, []byte{0x06}},
- {handshake.TypeCertificateVerify, true, 0, 7, []byte{0x07}},
- {handshake.TypeFinished, true, 0, 7, []byte{0xf0}},
- {handshake.TypeFinished, false, 0, 7, []byte{0xf1}},
- {handshake.TypeFinished, true, 1, 7, []byte{0x08}},
- {handshake.TypeFinished, false, 1, 7, []byte{0x09}},
- {handshake.TypeFinished, true, 0, 7, []byte{0xf0}},
- {handshake.TypeFinished, false, 0, 7, []byte{0xf1}},
- },
- Expected: []byte{
- 0x57, 0x35, 0x5a, 0xc3, 0x30, 0x3c, 0x14, 0x8f, 0x11, 0xae, 0xf7, 0xcb, 0x17, 0x94, 0x56, 0xb9,
- 0x23, 0x2c, 0xde, 0x33, 0xa8, 0x18, 0xdf, 0xda, 0x2c, 0x2f, 0xcb, 0x93, 0x25, 0x74, 0x9a, 0x6b,
- },
- },
- } {
- h := newHandshakeCache()
- for _, i := range test.Input {
- h.push(i.data, i.epoch, i.messageSequence, i.typ, i.isClient)
- }
-
- cipherSuite := ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256{}
- verifyData, err := h.sessionHash(cipherSuite.HashFunc(), 0)
- assert.NoError(t, err)
- assert.Equal(t, test.Expected, verifyData, "handshakeCacheSessionHash")
- }
-}
diff --git a/handshake_test.go b/handshake_test.go
deleted file mode 100644
index 91b56ee3d..000000000
--- a/handshake_test.go
+++ /dev/null
@@ -1,53 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "testing"
- "time"
-
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/stretchr/testify/assert"
-)
-
-func TestHandshakeMessage(t *testing.T) {
- rawHandshakeMessage := []byte{
- 0x01, 0x00, 0x00, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0xfe, 0xfd, 0xb6,
- 0x2f, 0xce, 0x5c, 0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42, 0x62, 0x15, 0xad,
- 0x16, 0xc9, 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec, 0xd8, 0x3d, 0xdc,
- 0x4b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- }
- parsedHandshake := &handshake.Handshake{
- Header: handshake.Header{
- Length: 0x29,
- FragmentLength: 0x29,
- Type: handshake.TypeClientHello,
- },
- Message: &handshake.MessageClientHello{
- Version: protocol.Version{Major: 0xFE, Minor: 0xFD},
- Random: handshake.Random{
- GMTUnixTime: time.Unix(3056586332, 0),
- RandomBytes: [28]byte{
- 0x42, 0x54, 0xff, 0x86, 0xe1, 0x24, 0x41, 0x91, 0x42, 0x62, 0x15, 0xad, 0x16, 0xc9,
- 0x15, 0x8d, 0x95, 0x71, 0x8a, 0xbb, 0x22, 0xd7, 0x47, 0xec, 0xd8, 0x3d, 0xdc, 0x4b,
- },
- },
- SessionID: []byte{},
- Cookie: []byte{},
- CipherSuiteIDs: []uint16{},
- CompressionMethods: []*protocol.CompressionMethod{},
- Extensions: []extension.Extension{},
- },
- }
-
- h := &handshake.Handshake{}
- assert.NoError(t, h.Unmarshal(rawHandshakeMessage))
- assert.Equal(t, parsedHandshake, h, "handshakeMessageClientHello unmarshal")
-
- raw, err := h.Marshal()
- assert.NoError(t, err)
- assert.Equal(t, rawHandshakeMessage, raw, "handshakeMessageClientHello marshal")
-}
diff --git a/handshake_transcript_13.go b/handshake_transcript_13.go
deleted file mode 100644
index 41bf51f4e..000000000
--- a/handshake_transcript_13.go
+++ /dev/null
@@ -1,414 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "crypto/hmac"
- "crypto/sha256"
- "hash"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/internal/util"
- "github.com/pion/dtls/v3/pkg/crypto/keyschedule"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
-)
-
-const tlsHandshakeHeaderLength13 = 4
-
-const (
- clientHandshakeTrafficLabel13 = "c hs traffic"
- serverHandshakeTrafficLabel13 = "s hs traffic"
- derivedSecretLabel13 = "derived"
- finishedLabel13 = "finished"
-
- serverCertificateVerifyContext13 = "TLS 1.3, server CertificateVerify\x00"
- clientCertificateVerifyContext13 = "TLS 1.3, client CertificateVerify\x00"
- certificateVerifyPaddingLen13 = 64
-)
-
-type transcriptSender13 uint8
-
-const (
- transcriptClient13 transcriptSender13 = iota
- transcriptServer13
-)
-
-type transcriptMessageID13 struct {
- sender transcriptSender13
- seq uint16
-}
-
-type seenTranscriptMessage13 struct {
- length int
- fingerprint [sha256.Size]byte
-}
-
-type transcriptMessage13 struct {
- id transcriptMessageID13
- typ handshake.Type
-}
-
-type handshakeTranscript13 struct {
- newHash func() hash.Hash
- h hash.Hash
-
- pending [][]byte
- transcript []byte
- seen map[transcriptMessageID13]seenTranscriptMessage13
- order []transcriptMessage13
-
- helloRetryApplied bool
-}
-
-func newHandshakeTranscript13() *handshakeTranscript13 {
- return &handshakeTranscript13{
- seen: make(map[transcriptMessageID13]seenTranscriptMessage13),
- }
-}
-
-func canonicalHandshake13(raw []byte) ([]byte, error) {
- if len(raw) < handshake.HeaderLength {
- return nil, dtlserrors.ErrBufferTooSmall
- }
-
- var header handshake.Header
- if err := header.Unmarshal(raw); err != nil {
- return nil, err
- }
-
- if header.FragmentOffset != 0 ||
- header.FragmentLength != header.Length ||
- len(raw) != handshake.HeaderLength+int(header.Length) {
- return nil, dtlserrors.ErrInvalidHandshakeTranscriptMessage
- }
-
- out := make([]byte, tlsHandshakeHeaderLength13+int(header.Length))
- copy(out[:tlsHandshakeHeaderLength13], raw[:tlsHandshakeHeaderLength13])
- copy(out[tlsHandshakeHeaderLength13:], raw[handshake.HeaderLength:])
-
- return out, nil
-}
-
-func (t *handshakeTranscript13) selectHash(newHash func() hash.Hash) error {
- if newHash == nil {
- return dtlserrors.ErrHandshakeTranscriptHashNotSelected
- }
- if t.h != nil {
- return dtlserrors.ErrHandshakeTranscriptHashAlreadySelected
- }
-
- h := newHash()
- if h == nil {
- return dtlserrors.ErrHandshakeTranscriptHashNotSelected
- }
-
- for _, message := range t.pending {
- if _, err := h.Write(message); err != nil {
- return err
- }
- }
- t.newHash = newHash
- t.h = h
- t.pending = nil
-
- return nil
-}
-
-func (t *handshakeTranscript13) appendCanonical(id transcriptMessageID13, message []byte) error {
- if err := validateCanonicalHandshake13(message); err != nil {
- return err
- }
-
- fingerprint := sha256.Sum256(message)
- if seen, ok := t.seen[id]; ok {
- if seen.length == len(message) && seen.fingerprint == fingerprint {
- return nil
- }
-
- return dtlserrors.ErrHandshakeTranscriptMessageChanged
- }
-
- messageCopy := append([]byte(nil), message...)
- t.seen[id] = seenTranscriptMessage13{
- length: len(messageCopy),
- fingerprint: fingerprint,
- }
- t.order = append(t.order, transcriptMessage13{
- id: id,
- typ: handshake.Type(messageCopy[0]),
- })
- t.transcript = append(t.transcript, messageCopy...)
-
- if t.h == nil {
- t.pending = append(t.pending, messageCopy)
-
- return nil
- }
-
- _, err := t.h.Write(messageCopy)
-
- return err
-}
-
-func (t *handshakeTranscript13) sum() ([]byte, error) {
- if t.h == nil {
- return nil, dtlserrors.ErrHandshakeTranscriptHashNotSelected
- }
-
- return t.h.Sum(nil), nil
-}
-
-func (t *handshakeTranscript13) sumWithSuffix(suffix []byte) ([]byte, error) {
- if t.h == nil {
- return nil, dtlserrors.ErrHandshakeTranscriptHashNotSelected
- }
-
- h := t.newHash()
- if h == nil {
- return nil, dtlserrors.ErrHandshakeTranscriptHashNotSelected
- }
- if _, err := h.Write(t.transcript); err != nil {
- return nil, err
- }
- if _, err := h.Write(suffix); err != nil {
- return nil, err
- }
-
- return h.Sum(nil), nil
-}
-
-func (t *handshakeTranscript13) hasInitialClientHello13() bool {
- return len(t.order) == 1 &&
- t.order[0].id.sender == transcriptClient13 &&
- t.order[0].typ == handshake.TypeClientHello
-}
-
-func (t *handshakeTranscript13) applyHelloRetryRequest() error {
- if t.h == nil {
- return dtlserrors.ErrHandshakeTranscriptHashNotSelected
- }
- if t.helloRetryApplied || !t.hasInitialClientHello13() {
- return dtlserrors.ErrHandshakeTranscriptHelloRetryRequestInvalid
- }
-
- clientHelloDigest := t.h.Sum(nil)
- messageHash := make([]byte, tlsHandshakeHeaderLength13+len(clientHelloDigest))
- messageHash[0] = byte(handshake.TypeMessageHash)
- util.PutBigEndianUint24(messageHash[1:], uint32(len(clientHelloDigest))) //nolint:gosec // G115
- copy(messageHash[tlsHandshakeHeaderLength13:], clientHelloDigest)
-
- h := t.newHash()
- if h == nil {
- return dtlserrors.ErrHandshakeTranscriptHashNotSelected
- }
- if _, err := h.Write(messageHash); err != nil {
- return err
- }
- t.h = h
- t.transcript = append(t.transcript[:0], messageHash...)
- t.helloRetryApplied = true
-
- return nil
-}
-
-func validateCanonicalHandshake13(message []byte) error {
- if len(message) < tlsHandshakeHeaderLength13 {
- return dtlserrors.ErrBufferTooSmall
- }
- if int(util.BigEndianUint24(message[1:])) != len(message)-tlsHandshakeHeaderLength13 {
- return dtlserrors.ErrInvalidHandshakeTranscriptMessage
- }
-
- return nil
-}
-
-func deriveHandshakeTrafficSecrets13(
- hashFunc func() hash.Hash,
- preMasterSecret, transcriptHash []byte,
-) (dtlsstate.HandshakeTrafficSecrets13, error) {
- hashSize, err := hashSize13(hashFunc)
- if err != nil {
- return dtlsstate.HandshakeTrafficSecrets13{}, err
- }
- if len(preMasterSecret) == 0 || len(transcriptHash) != hashSize {
- return dtlsstate.HandshakeTrafficSecrets13{}, dtlserrors.ErrLengthMismatch
- }
-
- zeroSecret := make([]byte, hashSize)
- earlySecret, err := keyschedule.HkdfExtract(hashFunc, nil, zeroSecret)
- if err != nil {
- return dtlsstate.HandshakeTrafficSecrets13{}, err
- }
-
- derivedSecret, err := keyschedule.DeriveSecret(hashFunc, earlySecret, derivedSecretLabel13, nil)
- if err != nil {
- return dtlsstate.HandshakeTrafficSecrets13{}, err
- }
-
- handshakeSecret, err := keyschedule.HkdfExtract(hashFunc, derivedSecret, preMasterSecret)
- if err != nil {
- return dtlsstate.HandshakeTrafficSecrets13{}, err
- }
-
- clientSecret, err := keyschedule.HkdfExpandLabel(
- hashFunc,
- handshakeSecret,
- clientHandshakeTrafficLabel13,
- transcriptHash,
- hashSize,
- )
- if err != nil {
- return dtlsstate.HandshakeTrafficSecrets13{}, err
- }
-
- serverSecret, err := keyschedule.HkdfExpandLabel(
- hashFunc,
- handshakeSecret,
- serverHandshakeTrafficLabel13,
- transcriptHash,
- hashSize,
- )
- if err != nil {
- return dtlsstate.HandshakeTrafficSecrets13{}, err
- }
-
- return dtlsstate.HandshakeTrafficSecrets13{
- Client: clientSecret,
- Server: serverSecret,
- }, nil
-}
-
-func deriveAndStoreHandshakeTrafficSecrets13(state *dtlsstate.State, transcript *handshakeTranscript13) error {
- if state == nil || state.CipherSuite == nil {
- return dtlserrors.ErrCipherSuiteNotSet
- }
- if transcript == nil {
- return dtlserrors.ErrHandshakeTranscriptHashNotSelected
- }
- if err := selectTranscriptHashIfReady13(transcript, state.CipherSuite); err != nil {
- return err
- }
-
- transcriptHash, err := transcript.sum()
- if err != nil {
- return err
- }
-
- secrets, err := deriveHandshakeTrafficSecrets13(
- state.CipherSuite.HashFunc(),
- state.PreMasterSecret,
- transcriptHash,
- )
- if err != nil {
- return err
- }
- state.HandshakeTrafficSecrets13 = secrets
-
- return nil
-}
-
-func certificateVerifyInputFromTranscript13(
- isClient bool,
- transcript *handshakeTranscript13,
-) ([]byte, error) {
- if transcript == nil {
- return nil, dtlserrors.ErrHandshakeTranscriptHashNotSelected
- }
-
- transcriptHash, err := transcript.sum()
- if err != nil {
- return nil, err
- }
-
- return certificateVerifyInput13(isClient, transcriptHash), nil
-}
-
-func certificateVerifyInput13(isClient bool, transcriptHash []byte) []byte {
- context := serverCertificateVerifyContext13
- if isClient {
- context = clientCertificateVerifyContext13
- }
-
- out := make([]byte, certificateVerifyPaddingLen13, certificateVerifyPaddingLen13+len(context)+len(transcriptHash))
- for i := range out {
- out[i] = 0x20
- }
- out = append(out, context...)
- out = append(out, transcriptHash...)
-
- return out
-}
-
-func finishedKey13(hashFunc func() hash.Hash, baseKey []byte) ([]byte, error) {
- hashSize, err := hashSize13(hashFunc)
- if err != nil {
- return nil, err
- }
-
- return keyschedule.HkdfExpandLabel(hashFunc, baseKey, finishedLabel13, nil, hashSize)
-}
-
-func finishedVerifyDataFromTranscript13(
- hashFunc func() hash.Hash,
- baseKey []byte,
- transcript *handshakeTranscript13,
-) ([]byte, error) {
- if transcript == nil {
- return nil, dtlserrors.ErrHandshakeTranscriptHashNotSelected
- }
-
- transcriptHash, err := transcript.sum()
- if err != nil {
- return nil, err
- }
-
- return finishedVerifyData13(hashFunc, baseKey, transcriptHash)
-}
-
-func finishedVerifyData13(hashFunc func() hash.Hash, baseKey, transcriptHash []byte) ([]byte, error) {
- hashSize, err := hashSize13(hashFunc)
- if err != nil {
- return nil, err
- }
- if len(transcriptHash) != hashSize {
- return nil, dtlserrors.ErrLengthMismatch
- }
-
- finishedKey, err := finishedKey13(hashFunc, baseKey)
- if err != nil {
- return nil, err
- }
-
- mac := hmac.New(hashFunc, finishedKey)
- if _, err := mac.Write(transcriptHash); err != nil {
- return nil, err
- }
-
- return mac.Sum(nil), nil
-}
-
-func verifyFinishedData13(hashFunc func() hash.Hash, baseKey, transcriptHash, verifyData []byte) error {
- expected, err := finishedVerifyData13(hashFunc, baseKey, transcriptHash)
- if err != nil {
- return err
- }
- if !hmac.Equal(expected, verifyData) {
- return dtlserrors.ErrVerifyDataMismatch
- }
-
- return nil
-}
-
-func hashSize13(hashFunc func() hash.Hash) (int, error) {
- if hashFunc == nil {
- return 0, dtlserrors.ErrKeyScheduleMissingHashFunction
- }
- h := hashFunc()
- if h == nil {
- return 0, dtlserrors.ErrKeyScheduleMissingHashFunction
- }
-
- return h.Size(), nil
-}
diff --git a/handshake_transcript_13_test.go b/handshake_transcript_13_test.go
deleted file mode 100644
index 4d4d05668..000000000
--- a/handshake_transcript_13_test.go
+++ /dev/null
@@ -1,519 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "bytes"
- "crypto/hmac"
- "crypto/sha256"
- "testing"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/internal/util"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestCanonicalHandshake13(t *testing.T) {
- const bodyLen = 3
-
- body := []byte{0xaa, 0xbb, 0xcc}
- raw := makeRawHandshake13(t, handshake.Header{
- Type: handshake.TypeClientHello,
- Length: bodyLen,
- MessageSequence: 7,
- FragmentLength: bodyLen,
- }, body)
-
- canonical, err := canonicalHandshake13(raw)
- assert.NoError(t, err)
- assert.Equal(t, []byte{
- byte(handshake.TypeClientHello), 0x00, 0x00, 0x03,
- 0xaa, 0xbb, 0xcc,
- }, canonical)
-}
-
-func TestCanonicalHandshake13RejectsInvalidMessages(t *testing.T) {
- const bodyLen = 2
-
- body := []byte{0xaa, 0xbb}
-
- for _, test := range []struct {
- name string
- raw []byte
- err error
- }{
- {
- name: "too small",
- raw: []byte{byte(handshake.TypeClientHello)},
- err: dtlserrors.ErrBufferTooSmall,
- },
- {
- name: "fragment offset",
- raw: makeRawHandshake13(t, handshake.Header{
- Type: handshake.TypeClientHello,
- Length: bodyLen,
- MessageSequence: 1,
- FragmentOffset: 1,
- FragmentLength: bodyLen,
- }, body),
- err: dtlserrors.ErrInvalidHandshakeTranscriptMessage,
- },
- {
- name: "fragment length",
- raw: makeRawHandshake13(t, handshake.Header{
- Type: handshake.TypeClientHello,
- Length: bodyLen,
- MessageSequence: 1,
- FragmentLength: bodyLen - 1,
- }, body),
- err: dtlserrors.ErrInvalidHandshakeTranscriptMessage,
- },
- {
- name: "body length",
- raw: makeRawHandshake13(t, handshake.Header{
- Type: handshake.TypeClientHello,
- Length: bodyLen + 1,
- MessageSequence: 1,
- FragmentLength: bodyLen + 1,
- }, body),
- err: dtlserrors.ErrInvalidHandshakeTranscriptMessage,
- },
- } {
- t.Run(test.name, func(t *testing.T) {
- _, err := canonicalHandshake13(test.raw)
- assert.ErrorIs(t, err, test.err)
- })
- }
-}
-
-func TestHandshakeTranscript13DeferredHashSelection(t *testing.T) {
- clientHello := canonicalTranscriptHandshake13(handshake.TypeClientHello, []byte{0x01, 0x02})
- serverHello := canonicalTranscriptHandshake13(handshake.TypeServerHello, []byte{0x03, 0x04})
- expectedClientHello := append([]byte(nil), clientHello...)
-
- transcript := newHandshakeTranscript13()
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13}, clientHello))
- clientHello[len(clientHello)-1] = 0xff
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptServer13}, serverHello))
-
- assert.NoError(t, transcript.selectHash(sha256.New))
-
- sum, err := transcript.sum()
- assert.NoError(t, err)
- assert.Equal(t, hashTranscript13(expectedClientHello, serverHello), sum)
-}
-
-func TestHandshakeTranscript13RejectsSumBeforeHashSelection(t *testing.T) {
- transcript := newHandshakeTranscript13()
-
- _, err := transcript.sum()
- assert.ErrorIs(t, err, dtlserrors.ErrHandshakeTranscriptHashNotSelected)
-}
-
-func TestHandshakeTranscript13RejectsHashReselection(t *testing.T) {
- transcript := newHandshakeTranscript13()
- assert.NoError(t, transcript.selectHash(sha256.New))
-
- err := transcript.selectHash(sha256.New)
- assert.ErrorIs(t, err, dtlserrors.ErrHandshakeTranscriptHashAlreadySelected)
-}
-
-func TestHandshakeTranscript13DuplicateHandling(t *testing.T) {
- clientHello := canonicalTranscriptHandshake13(handshake.TypeClientHello, []byte{0x01})
- changedClientHello := canonicalTranscriptHandshake13(handshake.TypeClientHello, []byte{0x02})
- serverHello := canonicalTranscriptHandshake13(handshake.TypeServerHello, []byte{0x03})
-
- transcript := newHandshakeTranscript13()
- assert.NoError(t, transcript.selectHash(sha256.New))
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13}, clientHello))
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13}, clientHello))
-
- err := transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13}, changedClientHello)
- assert.ErrorIs(t, err, dtlserrors.ErrHandshakeTranscriptMessageChanged)
-
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptServer13}, serverHello))
-
- sum, err := transcript.sum()
- assert.NoError(t, err)
- assert.Equal(t, hashTranscript13(clientHello, serverHello), sum)
-}
-
-func TestHandshakeTranscript13RejectsInvalidCanonicalMessage(t *testing.T) {
- transcript := newHandshakeTranscript13()
-
- err := transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13}, []byte{
- byte(handshake.TypeClientHello), 0x00, 0x00, 0x02, 0x01,
- })
- assert.ErrorIs(t, err, dtlserrors.ErrInvalidHandshakeTranscriptMessage)
-}
-
-func TestHandshakeTranscript13HelloRetryRequest(t *testing.T) {
- clientHello1 := canonicalTranscriptHandshake13(handshake.TypeClientHello, []byte{0x01})
- helloRetryRequest := canonicalTranscriptHandshake13(handshake.TypeServerHello, []byte{0x02})
- clientHello2 := canonicalTranscriptHandshake13(handshake.TypeClientHello, []byte{0x03})
- serverHello := canonicalTranscriptHandshake13(handshake.TypeServerHello, []byte{0x04})
-
- transcript := newHandshakeTranscript13()
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13}, clientHello1))
- assert.NoError(t, transcript.selectHash(sha256.New))
- assert.NoError(t, transcript.applyHelloRetryRequest())
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptServer13}, helloRetryRequest))
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13, seq: 1}, clientHello2))
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptServer13, seq: 1}, serverHello))
-
- clientHello1Hash := hashTranscript13(clientHello1)
- messageHash := canonicalTranscriptHandshake13(handshake.TypeMessageHash, clientHello1Hash)
- expected := hashTranscript13(messageHash, helloRetryRequest, clientHello2, serverHello)
-
- sum, err := transcript.sum()
- assert.NoError(t, err)
- assert.Equal(t, expected, sum)
- assert.Equal(t, "MessageHash", handshake.TypeMessageHash.String())
-}
-
-func TestHandshakeTranscript13HelloRetryRequestBinderFork(t *testing.T) {
- clientHello1 := canonicalTranscriptHandshake13(handshake.TypeClientHello, []byte{0x01})
- helloRetryRequest := canonicalTranscriptHandshake13(handshake.TypeServerHello, []byte{0x02})
- placeholderBinder := make([]byte, sha256.Size)
- _, truncatedClientHello2 := pskClientHelloTranscript13(t, placeholderBinder)
-
- transcript := newHandshakeTranscript13()
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13}, clientHello1))
- assert.NoError(t, transcript.selectHash(sha256.New))
- assert.NoError(t, transcript.applyHelloRetryRequest())
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptServer13}, helloRetryRequest))
-
- mainSumBefore, err := transcript.sum()
- assert.NoError(t, err)
- assert.ErrorIs(t, validateCanonicalHandshake13(truncatedClientHello2), dtlserrors.ErrInvalidHandshakeTranscriptMessage)
-
- binderTranscriptHash, err := transcript.sumWithSuffix(truncatedClientHello2)
- assert.NoError(t, err)
-
- clientHello1Hash := hashTranscript13(clientHello1)
- messageHash := canonicalTranscriptHandshake13(handshake.TypeMessageHash, clientHello1Hash)
- expectedBinderTranscriptHash := hashTranscript13(messageHash, helloRetryRequest, truncatedClientHello2)
- assert.Equal(t, expectedBinderTranscriptHash, binderTranscriptHash)
-
- binderKey := []byte("binder key")
- binder := hmacSHA25613(binderKey, binderTranscriptHash)
- expectedBinder := hmacSHA25613(binderKey, expectedBinderTranscriptHash)
- assert.Equal(t, expectedBinder, binder)
-
- mainSumAfter, err := transcript.sum()
- assert.NoError(t, err)
- assert.Equal(t, mainSumBefore, mainSumAfter)
-
- clientHello2, truncatedClientHello2WithBinder := pskClientHelloTranscript13(t, binder)
- assert.Equal(t, truncatedClientHello2, truncatedClientHello2WithBinder)
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13, seq: 1}, clientHello2))
-
- sum, err := transcript.sum()
- assert.NoError(t, err)
- assert.Equal(t, hashTranscript13(messageHash, helloRetryRequest, clientHello2), sum)
-}
-
-func TestHandshakeTranscript13HelloRetryRequestErrors(t *testing.T) {
- clientHello := canonicalTranscriptHandshake13(handshake.TypeClientHello, []byte{0x01})
- serverHello := canonicalTranscriptHandshake13(handshake.TypeServerHello, []byte{0x02})
-
- t.Run("hash not selected", func(t *testing.T) {
- transcript := newHandshakeTranscript13()
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13}, clientHello))
-
- err := transcript.applyHelloRetryRequest()
- assert.ErrorIs(t, err, dtlserrors.ErrHandshakeTranscriptHashNotSelected)
- })
-
- t.Run("not first client hello only", func(t *testing.T) {
- transcript := newHandshakeTranscript13()
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13}, clientHello))
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptServer13}, serverHello))
- assert.NoError(t, transcript.selectHash(sha256.New))
-
- err := transcript.applyHelloRetryRequest()
- assert.ErrorIs(t, err, dtlserrors.ErrHandshakeTranscriptHelloRetryRequestInvalid)
- })
-
- t.Run("server message", func(t *testing.T) {
- transcript := newHandshakeTranscript13()
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptServer13}, serverHello))
- assert.NoError(t, transcript.selectHash(sha256.New))
-
- err := transcript.applyHelloRetryRequest()
- assert.ErrorIs(t, err, dtlserrors.ErrHandshakeTranscriptHelloRetryRequestInvalid)
- })
-
- t.Run("already applied", func(t *testing.T) {
- transcript := newHandshakeTranscript13()
- assert.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13}, clientHello))
- assert.NoError(t, transcript.selectHash(sha256.New))
- assert.NoError(t, transcript.applyHelloRetryRequest())
-
- err := transcript.applyHelloRetryRequest()
- assert.ErrorIs(t, err, dtlserrors.ErrHandshakeTranscriptHelloRetryRequestInvalid)
- })
-}
-
-func TestDeriveHandshakeTrafficSecrets13NoHRRAndHRR(t *testing.T) {
- preMasterSecret := bytes.Repeat([]byte{0x42}, sha256.Size)
-
- clientHello := canonicalTranscriptHandshake13(handshake.TypeClientHello, []byte{0x01})
- serverHello := canonicalTranscriptHandshake13(handshake.TypeServerHello, []byte{0x02})
- noHRRTranscriptHash := hashTranscript13(clientHello, serverHello)
-
- noHRRSecrets, err := deriveHandshakeTrafficSecrets13(sha256.New, preMasterSecret, noHRRTranscriptHash)
- require.NoError(t, err)
- require.Len(t, noHRRSecrets.Client, sha256.Size)
- require.Len(t, noHRRSecrets.Server, sha256.Size)
- assert.NotEqual(t, noHRRSecrets.Client, noHRRSecrets.Server)
-
- again, err := deriveHandshakeTrafficSecrets13(sha256.New, preMasterSecret, noHRRTranscriptHash)
- require.NoError(t, err)
- assert.Equal(t, noHRRSecrets, again)
-
- clientHello1 := canonicalTranscriptHandshake13(handshake.TypeClientHello, []byte{0x03})
- helloRetryRequest := canonicalTranscriptHandshake13(handshake.TypeServerHello, []byte{0x04})
- clientHello2 := canonicalTranscriptHandshake13(handshake.TypeClientHello, []byte{0x05})
- serverHello2 := canonicalTranscriptHandshake13(handshake.TypeServerHello, []byte{0x06})
- messageHash := canonicalTranscriptHandshake13(handshake.TypeMessageHash, hashTranscript13(clientHello1))
- hrrTranscriptHash := hashTranscript13(messageHash, helloRetryRequest, clientHello2, serverHello2)
-
- hrrSecrets, err := deriveHandshakeTrafficSecrets13(sha256.New, preMasterSecret, hrrTranscriptHash)
- require.NoError(t, err)
- assert.NotEqual(t, noHRRSecrets.Client, hrrSecrets.Client)
- assert.NotEqual(t, noHRRSecrets.Server, hrrSecrets.Server)
-
- changedSecret := append([]byte(nil), preMasterSecret...)
- changedSecret[0] ^= 0xff
- changedSecrets, err := deriveHandshakeTrafficSecrets13(sha256.New, changedSecret, noHRRTranscriptHash)
- require.NoError(t, err)
- assert.NotEqual(t, noHRRSecrets.Client, changedSecrets.Client)
- assert.NotEqual(t, noHRRSecrets.Server, changedSecrets.Server)
-}
-
-func TestDeriveAndStoreHandshakeTrafficSecrets13FromTranscript(t *testing.T) {
- cipherSuite := cipherSuiteForID(TLS_AES_128_GCM_SHA256, nil)
- state := &dtlsstate.State{
- CipherSuite: cipherSuite,
- PreMasterSecret: bytes.Repeat([]byte{0x11}, sha256.Size),
- }
-
- clientHello := canonicalTranscriptHandshake13(handshake.TypeClientHello, []byte{0x01})
- serverHello := canonicalTranscriptHandshake13(handshake.TypeServerHello, []byte{0x02})
- transcript := newHandshakeTranscript13()
- require.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13}, clientHello))
- require.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptServer13}, serverHello))
-
- require.NoError(t, deriveAndStoreHandshakeTrafficSecrets13(state, transcript))
-
- transcriptHash, err := transcript.sum()
- require.NoError(t, err)
- expected, err := deriveHandshakeTrafficSecrets13(cipherSuite.HashFunc(), state.PreMasterSecret, transcriptHash)
- require.NoError(t, err)
- assert.Equal(t, expected, state.HandshakeTrafficSecrets13)
- assert.NotEmpty(t, state.HandshakeTrafficSecrets13.Client)
- assert.NotEmpty(t, state.HandshakeTrafficSecrets13.Server)
-}
-
-func TestCertificateVerifyInput13ServerAndClient(t *testing.T) {
- transcriptHash := bytes.Repeat([]byte{0xa5}, sha256.Size)
-
- serverInput := certificateVerifyInput13(false, transcriptHash)
- clientInput := certificateVerifyInput13(true, transcriptHash)
-
- require.Len(t, serverInput, certificateVerifyPaddingLen13+len(serverCertificateVerifyContext13)+sha256.Size)
- assert.Equal(t, bytes.Repeat([]byte{0x20}, certificateVerifyPaddingLen13),
- serverInput[:certificateVerifyPaddingLen13])
- serverContextEnd := certificateVerifyPaddingLen13 + len(serverCertificateVerifyContext13)
- serverContext := serverInput[certificateVerifyPaddingLen13:serverContextEnd]
- assert.Equal(t, serverCertificateVerifyContext13,
- string(serverContext))
- assert.Equal(t, transcriptHash, serverInput[len(serverInput)-sha256.Size:])
-
- require.Len(t, clientInput, certificateVerifyPaddingLen13+len(clientCertificateVerifyContext13)+sha256.Size)
- clientContextEnd := certificateVerifyPaddingLen13 + len(clientCertificateVerifyContext13)
- clientContext := clientInput[certificateVerifyPaddingLen13:clientContextEnd]
- assert.Equal(t, clientCertificateVerifyContext13,
- string(clientContext))
- assert.Equal(t, transcriptHash, clientInput[len(clientInput)-sha256.Size:])
- assert.NotEqual(t, serverInput, clientInput)
-}
-
-func TestFinishedVerifyData13(t *testing.T) {
- baseKey := bytes.Repeat([]byte{0x11}, sha256.Size)
- transcriptHash := bytes.Repeat([]byte{0x22}, sha256.Size)
-
- finishedKey, err := finishedKey13(sha256.New, baseKey)
- require.NoError(t, err)
-
- verifyData, err := finishedVerifyData13(sha256.New, baseKey, transcriptHash)
- require.NoError(t, err)
- require.Len(t, verifyData, sha256.Size)
-
- expectedMAC := hmac.New(sha256.New, finishedKey)
- _, err = expectedMAC.Write(transcriptHash)
- require.NoError(t, err)
- assert.Equal(t, expectedMAC.Sum(nil), verifyData)
- assert.NoError(t, verifyFinishedData13(sha256.New, baseKey, transcriptHash, verifyData))
-
- changedTranscript := append([]byte(nil), transcriptHash...)
- changedTranscript[0] ^= 0xff
- changedVerifyData, err := finishedVerifyData13(sha256.New, baseKey, changedTranscript)
- require.NoError(t, err)
- assert.NotEqual(t, verifyData, changedVerifyData)
-
- changedKey := append([]byte(nil), baseKey...)
- changedKey[0] ^= 0xff
- changedKeyVerifyData, err := finishedVerifyData13(sha256.New, changedKey, transcriptHash)
- require.NoError(t, err)
- assert.NotEqual(t, verifyData, changedKeyVerifyData)
-
- badVerifyData := append([]byte(nil), verifyData...)
- badVerifyData[0] ^= 0xff
- assert.ErrorIs(t, verifyFinishedData13(sha256.New, baseKey, transcriptHash, badVerifyData),
- dtlserrors.ErrVerifyDataMismatch)
-}
-
-func TestDTLS13TranscriptAuthenticatedHandshakeInputs(t *testing.T) {
- cipherSuite := cipherSuiteForID(TLS_AES_128_GCM_SHA256, nil)
- state := &dtlsstate.State{
- CipherSuite: cipherSuite,
- PreMasterSecret: bytes.Repeat([]byte{0x77}, sha256.Size),
- }
- transcript := newHandshakeTranscript13()
-
- clientHello := canonicalTranscriptHandshake13(handshake.TypeClientHello, []byte{0x01})
- serverHello := canonicalTranscriptHandshake13(handshake.TypeServerHello, []byte{0x02})
- require.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptClient13}, clientHello))
- require.NoError(t, transcript.appendCanonical(transcriptMessageID13{sender: transcriptServer13}, serverHello))
-
- require.NoError(t, deriveAndStoreHandshakeTrafficSecrets13(state, transcript))
- require.NotEmpty(t, state.HandshakeTrafficSecrets13.Client)
- require.NotEmpty(t, state.HandshakeTrafficSecrets13.Server)
-
- certificate := canonicalTranscriptHandshake13(handshake.TypeCertificate, []byte{0x03})
- require.NoError(t, transcript.appendCanonical(transcriptMessageID13{
- sender: transcriptServer13,
- seq: 1,
- }, certificate))
-
- certVerifyInput, err := certificateVerifyInputFromTranscript13(false, transcript)
- require.NoError(t, err)
- certVerifyTranscriptHash, err := transcript.sum()
- require.NoError(t, err)
- assert.Equal(t, certVerifyTranscriptHash, certVerifyInput[len(certVerifyInput)-sha256.Size:])
-
- certVerify := canonicalTranscriptHandshake13(handshake.TypeCertificateVerify, []byte{0x04})
- require.NoError(t, transcript.appendCanonical(transcriptMessageID13{
- sender: transcriptServer13,
- seq: 2,
- }, certVerify))
-
- verifyData, err := finishedVerifyDataFromTranscript13(
- sha256.New,
- state.HandshakeTrafficSecrets13.Server,
- transcript,
- )
- require.NoError(t, err)
- finishedTranscriptHash, err := transcript.sum()
- require.NoError(t, err)
- assert.NoError(t, verifyFinishedData13(
- sha256.New,
- state.HandshakeTrafficSecrets13.Server,
- finishedTranscriptHash,
- verifyData,
- ))
-}
-
-func FuzzCanonicalHandshake13(f *testing.F) {
- f.Add(makeRawHandshake13(f, handshake.Header{
- Type: handshake.TypeClientHello,
- Length: 2,
- FragmentLength: 2,
- }, []byte{0x01, 0x02}))
- f.Add([]byte{byte(handshake.TypeClientHello), 0x00, 0x00, 0x01})
-
- f.Fuzz(func(t *testing.T, data []byte) {
- canonical, err := canonicalHandshake13(data)
- if err != nil {
- return
- }
- if !assert.GreaterOrEqual(t, len(canonical), tlsHandshakeHeaderLength13) {
- return
- }
- assert.Equal(t, len(canonical)-tlsHandshakeHeaderLength13, int(util.BigEndianUint24(canonical[1:])))
- assert.Equal(t, data[0], canonical[0])
- assert.Equal(t, data[handshake.HeaderLength:], canonical[tlsHandshakeHeaderLength13:])
- })
-}
-
-func makeRawHandshake13(tb testing.TB, header handshake.Header, body []byte) []byte {
- tb.Helper()
-
- rawHeader, err := header.Marshal()
- assert.NoError(tb, err)
-
- return append(rawHeader, body...)
-}
-
-func canonicalTranscriptHandshake13(typ handshake.Type, body []byte) []byte {
- out := make([]byte, tlsHandshakeHeaderLength13+len(body))
- out[0] = byte(typ)
- util.PutBigEndianUint24(out[1:], uint32(len(body))) //nolint:gosec // G115
- copy(out[tlsHandshakeHeaderLength13:], body)
-
- return out
-}
-
-func hashTranscript13(messages ...[]byte) []byte {
- hash := sha256.New()
- for _, message := range messages {
- _, _ = hash.Write(message)
- }
-
- return hash.Sum(nil)
-}
-
-func pskClientHelloTranscript13(tb testing.TB, binder []byte) ([]byte, []byte) {
- tb.Helper()
-
- msg := &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- CipherSuiteIDs: []uint16{0x1301},
- CompressionMethods: []*protocol.CompressionMethod{{}},
- Extensions: []extension.Extension{
- &extension.PreSharedKey{
- Identities: []extension.PskIdentity{
- {
- Identity: []byte("psk-identity"),
- ObfuscatedTicketAge: 0x01020304,
- },
- },
- Binders: []extension.PskBinderEntry{binder},
- },
- },
- }
-
- body, err := msg.Marshal()
- assert.NoError(tb, err)
-
- full := canonicalTranscriptHandshake13(handshake.TypeClientHello, body)
- truncatedLen := len(full) - (2 + 1 + len(binder))
- assert.Greater(tb, truncatedLen, tlsHandshakeHeaderLength13)
-
- return full, append([]byte(nil), full[:truncatedLen]...)
-}
-
-func hmacSHA25613(key, data []byte) []byte {
- mac := hmac.New(sha256.New, key)
- _, _ = mac.Write(data)
-
- return mac.Sum(nil)
-}
diff --git a/handshaker.go b/handshaker.go
deleted file mode 100644
index bd1816994..000000000
--- a/handshaker.go
+++ /dev/null
@@ -1,381 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
- "crypto/tls"
- "crypto/x509"
- "fmt"
- "io"
- "sync"
- "time"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/elliptic"
- "github.com/pion/dtls/v3/pkg/crypto/signaturehash"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/logging"
-)
-
-// [RFC6347 Section-4.2.4]
-// +-----------+
-// +---> | PREPARING | <--------------------+
-// | +-----------+ |
-// | | |
-// | | Buffer next flight |
-// | | |
-// | \|/ |
-// | +-----------+ |
-// | | SENDING |<------------------+ | Send
-// | +-----------+ | | HelloRequest
-// Receive | | | |
-// next | | Send flight | | or
-// flight | +--------+ | |
-// | | | Set retransmit timer | | Receive
-// | | \|/ | | HelloRequest
-// | | +-----------+ | | Send
-// +--)--| WAITING |-------------------+ | ClientHello
-// | | +-----------+ Timer expires | |
-// | | | | |
-// | | +------------------------+ |
-// Receive | | Send Read retransmit |
-// last | | last |
-// flight | | flight |
-// | | |
-// \|/\|/ |
-// +-----------+ |
-// | FINISHED | -------------------------------+
-// +-----------+
-// | /|\
-// | |
-// +---+
-// Read retransmit
-// Retransmit last flight
-
-type handshakeState uint8
-
-const (
- handshakeErrored handshakeState = iota
- handshakePreparing
- handshakeSending
- handshakeWaiting
- handshakeFinished
-)
-
-func (s handshakeState) String() string {
- switch s {
- case handshakeErrored:
- return "Errored"
- case handshakePreparing:
- return "Preparing"
- case handshakeSending:
- return "Sending"
- case handshakeWaiting:
- return "Waiting"
- case handshakeFinished:
- return "Finished"
- default:
- return "Unknown"
- }
-}
-
-type handshakeFSM12 struct {
- currentFlight flightVal
- flights []*packet
- retransmit bool
- retransmitInterval time.Duration
- state *dtlsstate.State
- cache *handshakeCache
- cfg *handshakeConfig
- closed chan struct{}
-}
-
-type handshakeConfig struct {
- localPSKCallback PSKCallback
- localPSKIdentityHint []byte
- localCipherSuites []CipherSuite // Available CipherSuites
- localSignatureSchemes []signaturehash.Algorithm // Available signature schemes
- localCertSignatureSchemes []signaturehash.Algorithm // Available signature schemes for certificates
- extendedMasterSecret ExtendedMasterSecretType // Policy for the Extended Master Support extension
- localSRTPProtectionProfiles []SRTPProtectionProfile // Available SRTPProtectionProfiles, if empty no SRTP support
- localSRTPMasterKeyIdentifier []byte
- serverName string
- supportedProtocols []string
- clientAuth ClientAuthType // If we are a client should we request a client certificate
- localCertificates []tls.Certificate
- nameToCertificate map[string]*tls.Certificate
- insecureSkipVerify bool
- verifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
- verifyConnection func(*State) error
- sessionStore SessionStore
- rootCAs *x509.CertPool
- clientCAs *x509.CertPool
- initialRetransmitInterval time.Duration
- disableRetransmitBackoff bool
- customCipherSuites func() []CipherSuite
- ellipticCurves []elliptic.Curve
- insecureSkipHelloVerify bool
- connectionIDGenerator func() []byte
- helloRandomBytesGenerator func() [handshake.RandomBytesLength]byte
-
- onFlightState func(flightVal, handshakeState)
- log logging.LeveledLogger
- keyLogWriter io.Writer
-
- localGetCertificate func(*ClientHelloInfo) (*tls.Certificate, error)
- localGetClientCertificate func(*CertificateRequestInfo) (*tls.Certificate, error)
-
- initialEpoch uint16
-
- mu sync.Mutex
-
- clientHelloMessageHook func(handshake.MessageClientHello) handshake.Message
- serverHelloMessageHook func(handshake.MessageServerHello) handshake.Message
- certificateRequestMessageHook func(handshake.MessageCertificateRequest) handshake.Message
-
- resumeState *dtlsstate.State
-
- minVersion protocol.Version
- maxVersion protocol.Version
-
- onFlightState13 func(flightVal13, handshakeState)
-}
-
-type flightConn interface {
- notify(ctx context.Context, level alert.Level, desc alert.Description) error
- writePackets(context.Context, []*packet) error
- recvHandshake() <-chan recvHandshakeState
- setLocalEpoch(epoch uint16)
- handleQueuedPackets(context.Context) error
- sessionKey() []byte
-}
-
-func (c *handshakeConfig) writeKeyLog(label string, clientRandom, secret []byte) {
- if c.keyLogWriter == nil {
- return
- }
- c.mu.Lock()
- defer c.mu.Unlock()
- _, err := fmt.Fprintf(c.keyLogWriter, "%s %x %x\n", label, clientRandom, secret)
- if err != nil {
- c.log.Debugf("failed to write key log file: %s", err)
- }
-}
-
-func srvCliStr(isClient bool) string {
- if isClient {
- return "client"
- }
-
- return "server"
-}
-
-func newHandshakeFSM12(
- s *dtlsstate.State, cache *handshakeCache, cfg *handshakeConfig,
- initialFlight flightVal,
-) *handshakeFSM12 {
- return &handshakeFSM12{
- currentFlight: initialFlight,
- state: s,
- cache: cache,
- cfg: cfg,
- retransmitInterval: cfg.initialRetransmitInterval,
- closed: make(chan struct{}),
- }
-}
-
-type handshakeFSM interface {
- Done() <-chan struct{}
- Run(ctx context.Context, conn flightConn, initialState handshakeState) error
- finish(ctx context.Context, c flightConn) (handshakeState, error)
- prepare(ctx context.Context, conn flightConn) (handshakeState, error)
- send(ctx context.Context, c flightConn) (handshakeState, error)
- wait(ctx context.Context, conn flightConn) (handshakeState, error)
-}
-
-//nolint:dupl
-func (s *handshakeFSM12) Run(ctx context.Context, conn flightConn, initialState handshakeState) error {
- state := initialState
- defer func() {
- close(s.closed)
- }()
- for {
- s.cfg.log.Tracef("[handshake:%s] %s: %s", srvCliStr(s.state.IsClient), s.currentFlight.String(), state.String())
- if s.cfg.onFlightState != nil {
- s.cfg.onFlightState(s.currentFlight, state)
- }
- var err error
- switch state {
- case handshakePreparing:
- state, err = s.prepare(ctx, conn)
- case handshakeSending:
- state, err = s.send(ctx, conn)
- case handshakeWaiting:
- state, err = s.wait(ctx, conn)
- case handshakeFinished:
- state, err = s.finish(ctx, conn)
- default:
- return dtlserrors.ErrInvalidFSMTransition
- }
- if err != nil {
- return err
- }
- }
-}
-
-func (s *handshakeFSM12) Done() <-chan struct{} {
- return s.closed
-}
-
-//nolint:dupl
-func (s *handshakeFSM12) prepare(ctx context.Context, conn flightConn) (handshakeState, error) {
- s.flights = nil
- // Prepare flights
- var (
- dtlsAlert *alert.Alert
- err error
- pkts []*packet
- )
- gen, retransmit, errFlight := s.currentFlight.getFlightGenerator()
- if errFlight != nil {
- err = errFlight
- dtlsAlert = &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}
- } else {
- pkts, dtlsAlert, err = gen(conn, s.state, s.cache, s.cfg)
- s.retransmit = retransmit
- }
- if dtlsAlert != nil {
- if alertErr := conn.notify(ctx, dtlsAlert.Level, dtlsAlert.Description); alertErr != nil {
- if err != nil {
- err = alertErr
- }
- }
- }
- if err != nil {
- return handshakeErrored, err
- }
-
- s.flights = pkts
- epoch := s.cfg.initialEpoch
- nextEpoch := epoch
- for _, p := range s.flights {
- p.record.Header.Epoch += epoch
- if p.record.Header.Epoch > nextEpoch {
- nextEpoch = p.record.Header.Epoch
- }
- if h, ok := p.record.Content.(*handshake.Handshake); ok {
- h.Header.MessageSequence = uint16(s.state.HandshakeSendSequence) //nolint:gosec // G115
- s.state.HandshakeSendSequence++
- }
- }
- if epoch != nextEpoch {
- s.cfg.log.Tracef("[handshake:%s] -> changeCipherSpec (epoch: %d)", srvCliStr(s.state.IsClient), nextEpoch)
- conn.setLocalEpoch(nextEpoch)
- }
-
- return handshakeSending, nil
-}
-
-func (s *handshakeFSM12) send(ctx context.Context, c flightConn) (handshakeState, error) {
- // Send flights
- if err := c.writePackets(ctx, s.flights); err != nil {
- return handshakeErrored, err
- }
-
- if s.currentFlight.isLastSendFlight() {
- return handshakeFinished, nil
- }
-
- return handshakeWaiting, nil
-}
-
-func (s *handshakeFSM12) wait(ctx context.Context, conn flightConn) (handshakeState, error) { //nolint:gocognit,cyclop
- parse, errFlight := s.currentFlight.getFlightParser()
- if errFlight != nil {
- if alertErr := conn.notify(ctx, alert.Fatal, alert.InternalError); alertErr != nil {
- return handshakeErrored, alertErr
- }
-
- return handshakeErrored, errFlight
- }
-
- retransmitTimer := time.NewTimer(s.retransmitInterval)
- for {
- select {
- case state := <-conn.recvHandshake():
- if !state.isRetransmit {
- // only reset retransmit interval on non-retransmit state
- // https://github.com/pion/dtls/issues/758
- s.retransmitInterval = s.cfg.initialRetransmitInterval
- }
-
- nextFlight, alert, err := parse(ctx, conn, s.state, s.cache, s.cfg)
- close(state.done)
- if alert != nil {
- if alertErr := conn.notify(ctx, alert.Level, alert.Description); alertErr != nil {
- if err != nil {
- err = alertErr
- }
- }
- }
- if err != nil {
- return handshakeErrored, err
- }
- if nextFlight == 0 {
- break
- }
- s.cfg.log.Tracef(
- "[handshake:%s] %s -> %s",
- srvCliStr(s.state.IsClient),
- s.currentFlight.String(),
- nextFlight.String(),
- )
- if nextFlight.isLastRecvFlight() && s.currentFlight == nextFlight {
- return handshakeFinished, nil
- }
- s.currentFlight = nextFlight
-
- return handshakePreparing, nil
-
- case <-retransmitTimer.C:
- if !s.retransmit {
- return handshakeWaiting, nil
- }
-
- // RFC 4347 4.2.4.1:
- // Implementations SHOULD use an initial timer value of 1 second (the minimum defined in RFC 2988 [RFC2988])
- // and double the value at each retransmission, up to no less than the RFC 2988 maximum of 60 seconds.
- if !s.cfg.disableRetransmitBackoff {
- s.retransmitInterval *= 2
- }
- if s.retransmitInterval > time.Second*60 {
- s.retransmitInterval = time.Second * 60
- }
-
- return handshakeSending, nil
- case <-ctx.Done():
- s.retransmitInterval = s.cfg.initialRetransmitInterval
-
- return handshakeErrored, ctx.Err()
- }
- }
-}
-
-func (s *handshakeFSM12) finish(ctx context.Context, c flightConn) (handshakeState, error) {
- select {
- case state := <-c.recvHandshake():
- close(state.done)
- if s.state.IsClient {
- return handshakeFinished, nil
- } else {
- return handshakeSending, nil
- }
- case <-ctx.Done():
- return handshakeErrored, ctx.Err()
- }
-}
diff --git a/handshaker_13.go b/handshaker_13.go
deleted file mode 100644
index 7f68c9e7d..000000000
--- a/handshaker_13.go
+++ /dev/null
@@ -1,463 +0,0 @@
-// SPDX-FileCopyrightText: 2023 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
- "crypto/sha256"
- "errors"
- "time"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
-)
-
-// [RFC9147 Section-5.8.1]
-// +-----------+
-// | PREPARING |
-// +----------> | |
-// | | |
-// | +-----------+
-// | |
-// | | Buffer next flight
-// | |
-// | \|/
-// | +-----------+
-// | | |
-// | | SENDING |<------------------+
-// | | | |
-// | +-----------+ |
-// Receive | | |
-// next | | Send flight or partial |
-// flight | | flight |
-// | | |
-// | | Set retransmit timer |
-// | \|/ |
-// | +-----------+ |
-// | | | |
-// +------------| WAITING |-------------------+
-// | +----->| | Timer expires |
-// | | +-----------+ |
-// | | | | | |
-// | | | | | |
-// | +----------+ | +--------------------+
-// | Receive record | Read retransmit or ACK
-// Receive | (Maybe Send ACK) |
-// last | |
-// flight | | Receive ACK
-// | | for last flight
-// \|/ |
-// |
-// +-----------+ |
-// | | <---------+
-// | FINISHED |
-// | |
-// +-----------+
-// | /|\
-// | |
-// | |
-// +---+
-//
-// Server read retransmit
-// Retransmit ACK
-
-type handshakeFSM13 struct {
- currentFlight flightVal13
- flights []*packet //nolint:unused
- retransmit bool //nolint:unused
- retransmitInterval time.Duration
- state *dtlsstate.State
- cache *handshakeCache
- cfg *handshakeConfig
- transcript *handshakeTranscript13
- closed chan struct{}
-}
-
-func newHandshakeFSM13(
- state *dtlsstate.State,
- cache *handshakeCache,
- cfg *handshakeConfig,
- initialFlight flightVal13,
- initialFlights []*packet,
- initialTranscript *handshakeTranscript13,
-) (*handshakeFSM13, error) {
- if initialTranscript == nil {
- initialTranscript = newHandshakeTranscript13()
- }
-
- fsm := &handshakeFSM13{
- currentFlight: initialFlight,
- flights: initialFlights,
- retransmit: initialFlights != nil,
- state: state,
- cache: cache,
- cfg: cfg,
- transcript: initialTranscript,
- retransmitInterval: cfg.initialRetransmitInterval,
- closed: make(chan struct{}),
- }
- if err := fsm.seedTranscriptFromInitialFlights(); err != nil {
- return nil, err
- }
-
- return fsm, nil
-}
-
-func (s *handshakeFSM13) flightContext() *handshakeContext13 {
- return &handshakeContext13{
- state: s.state,
- cache: s.cache,
- cfg: s.cfg,
- transcript: s.transcript,
- }
-}
-
-// seedTranscriptFromInitialFlights handles the dual-stack ClientHello generated
-// before the DTLS 1.3 FSM exists.
-func (s *handshakeFSM13) seedTranscriptFromInitialFlights() error {
- if !s.state.IsClient {
- return nil
- }
-
- appended, err := appendClientHelloInitialFlights13(s.transcript, s.flights)
- if err != nil {
- return err
- }
- if s.retransmit && !appended {
- return dtlserrors.ErrHandshakeTranscriptMissingClientHello
- }
-
- return nil
-}
-
-func appendClientHelloInitialFlights13(transcript *handshakeTranscript13, flights []*packet) (bool, error) {
- if transcript == nil {
- return false, dtlserrors.ErrHandshakeTranscriptMissingClientHello
- }
-
- appended := false
- for _, p := range flights {
- seq, canonical, ok, err := canonicalClientHelloInitialFlight13(p)
- if err != nil {
- return false, err
- }
- if !ok {
- continue
- }
- if err := transcript.appendCanonical(transcriptMessageID13{
- sender: transcriptClient13,
- seq: seq,
- }, canonical); err != nil {
- return false, err
- }
- appended = true
- }
-
- return appended, nil
-}
-
-func canonicalClientHelloInitialFlight13(p *packet) (uint16, []byte, bool, error) {
- if p == nil || p.record == nil {
- return 0, nil, false, nil
- }
- hand, ok := p.record.Content.(*handshake.Handshake)
- if !ok {
- return 0, nil, false, nil
- }
- if hand.Message == nil || hand.Message.Type() != handshake.TypeClientHello {
- return 0, nil, false, nil
- }
-
- raw, err := hand.Marshal()
- if err != nil {
- return 0, nil, false, err
- }
- canonical, err := canonicalHandshake13(raw)
- if err != nil {
- return 0, nil, false, err
- }
-
- return hand.Header.MessageSequence, canonical, true, nil
-}
-
-//nolint:dupl
-func (s *handshakeFSM13) Run(ctx context.Context, conn flightConn, initialState handshakeState) error {
- state := initialState
- defer func() {
- close(s.closed)
- }()
- for {
- s.cfg.log.Tracef("[handshake13:%s] %s: %s", srvCliStr(s.state.IsClient), s.currentFlight.String(), state.String())
- // nolint:godox
- // TODO:: refactor callback, see discussion in https://github.com/pion/dtls/pull/738#discussion_r3131501159
- if s.cfg.onFlightState13 != nil {
- s.cfg.onFlightState13(s.currentFlight, state)
- }
- var err error
- switch state {
- case handshakePreparing:
- state, err = s.prepare(ctx, conn)
- case handshakeSending:
- state, err = s.send(ctx, conn)
- case handshakeWaiting:
- state, err = s.wait(ctx, conn)
- case handshakeFinished:
- state, err = s.finish(ctx, conn)
- default:
- return dtlserrors.ErrInvalidFSMTransition
- }
- if err != nil {
- return err
- }
- }
-}
-
-func (s *handshakeFSM13) Done() <-chan struct{} {
- return s.closed
-}
-
-//nolint:dupl
-func (s *handshakeFSM13) prepare(ctx context.Context, conn flightConn) (handshakeState, error) {
- s.flights = nil
- // Prepare flights
- var (
- dtlsAlert *alert.Alert
- err error
- pkts []*packet
- )
- gen, retransmit, errFlight := s.currentFlight.getFlightGenerator13()
- if errFlight != nil {
- err = errFlight
- dtlsAlert = &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}
- } else {
- pkts, dtlsAlert, err = gen(conn, s.flightContext())
- s.retransmit = retransmit
- }
- if dtlsAlert != nil {
- if alertErr := conn.notify(ctx, dtlsAlert.Level, dtlsAlert.Description); alertErr != nil {
- if err != nil {
- err = alertErr
- }
- }
- }
- if err != nil {
- return handshakeErrored, err
- }
-
- s.flights = pkts
- if err := s.commitPreparedFlights(conn); err != nil {
- return handshakeErrored, err
- }
-
- return handshakeSending, nil
-}
-
-func (s *handshakeFSM13) commitPreparedFlights(conn flightConn) error {
- epoch := s.cfg.initialEpoch
- nextEpoch := epoch
- for _, p := range s.flights {
- p.record.Header.Epoch += epoch
- if p.record.Header.Epoch > nextEpoch {
- nextEpoch = p.record.Header.Epoch
- }
- if h, ok := p.record.Content.(*handshake.Handshake); ok {
- h.Header.MessageSequence = uint16(s.state.HandshakeSendSequence) //nolint:gosec // G115
- s.state.HandshakeSendSequence++
- }
- }
- if err := appendOutboundHandshakeFlight13(
- s.transcript,
- s.state.IsClient,
- s.state.CipherSuite,
- s.flights,
- ); err != nil {
- return err
- }
- if epoch != nextEpoch {
- s.cfg.log.Tracef("[handshake13:%s] -> changeCipherSpec (epoch: %d)", srvCliStr(s.state.IsClient), nextEpoch)
- conn.setLocalEpoch(nextEpoch)
- }
-
- return nil
-}
-
-func (s *handshakeFSM13) send(ctx context.Context, c flightConn) (handshakeState, error) {
- return handshakeErrored, dtlserrors.ErrStateUnimplemented13
-}
-
-func (s *handshakeFSM13) wait(ctx context.Context, conn flightConn) (handshakeState, error) {
- return handshakeErrored, dtlserrors.ErrStateUnimplemented13
-}
-
-func (s *handshakeFSM13) finish(ctx context.Context, c flightConn) (handshakeState, error) {
- return handshakeErrored, dtlserrors.ErrStateUnimplemented13
-}
-
-func transcriptSenderForSide13(isClient bool) transcriptSender13 {
- if isClient {
- return transcriptClient13
- }
-
- return transcriptServer13
-}
-
-func selectTranscriptHashIfReady13(t *handshakeTranscript13, cipherSuite CipherSuite) error {
- if t == nil || cipherSuite == nil {
- return nil
- }
-
- err := t.selectHash(cipherSuite.HashFunc())
- if errors.Is(err, dtlserrors.ErrHandshakeTranscriptHashAlreadySelected) {
- return nil
- }
-
- return err
-}
-
-func appendOutboundHandshakeFlight13(
- transcript *handshakeTranscript13,
- isClient bool,
- cipherSuite CipherSuite,
- pkts []*packet,
-) error {
- if transcript == nil {
- return nil
- }
-
- sender := transcriptSenderForSide13(isClient)
- for _, p := range pkts {
- h, canonical, ok, err := canonicalOutboundHandshake13(p)
- if err != nil {
- return err
- }
- if !ok {
- continue
- }
-
- if err := appendOutboundHandshake13(transcript, sender, cipherSuite, h, canonical); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func canonicalOutboundHandshake13(p *packet) (*handshake.Handshake, []byte, bool, error) {
- if p == nil || p.record == nil {
- return nil, nil, false, nil
- }
-
- hs, ok := p.record.Content.(*handshake.Handshake)
- if !ok || hs.Message == nil {
- return nil, nil, false, nil
- }
-
- raw, err := hs.Marshal()
- if err != nil {
- return nil, nil, false, err
- }
- canonical, err := canonicalHandshake13(raw)
- if err != nil {
- return nil, nil, false, err
- }
-
- return hs, canonical, true, nil
-}
-
-func appendOutboundHandshake13(
- transcript *handshakeTranscript13,
- sender transcriptSender13,
- cipherSuite CipherSuite,
- h *handshake.Handshake,
- canonical []byte,
-) error {
- return appendHandshake13(transcript, sender, cipherSuite, h.Header.MessageSequence, h.Message, canonical)
-}
-
-func appendInboundHandshakeCacheItems13(
- transcript *handshakeTranscript13,
- cipherSuite CipherSuite,
- items []*handshakeCacheItem,
-) error {
- if transcript == nil {
- return nil
- }
-
- keyExchangeAlgorithm := keyExchangeAlgorithmForCipherSuite(cipherSuite)
- for _, item := range items {
- canonical, err := canonicalHandshake13(item.data)
- if err != nil {
- return err
- }
-
- h := &handshake.Handshake{
- KeyExchangeAlgorithm: keyExchangeAlgorithm,
- }
- if err := h.Unmarshal(item.data); err != nil {
- return err
- }
-
- if err := appendHandshake13(
- transcript,
- transcriptSenderForSide13(item.isClient),
- cipherSuite,
- h.Header.MessageSequence,
- h.Message,
- canonical,
- ); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-func appendHandshake13(
- transcript *handshakeTranscript13,
- sender transcriptSender13,
- cipherSuite CipherSuite,
- seq uint16,
- message handshake.Message,
- canonical []byte,
-) error {
- id := transcriptMessageID13{
- sender: sender,
- seq: seq,
- }
- if sh, ok := message.(*handshake.MessageServerHello); ok && isHelloRetryRequest(sh) {
- duplicate, err := transcript.hasCanonical13(id, canonical)
- if err != nil || duplicate {
- return err
- }
-
- if err := selectTranscriptHashIfReady13(transcript, cipherSuite); err != nil {
- return err
- }
- if err := transcript.applyHelloRetryRequest(); err != nil {
- return err
- }
- }
-
- return transcript.appendCanonical(id, canonical)
-}
-
-func (t *handshakeTranscript13) hasCanonical13(id transcriptMessageID13, message []byte) (bool, error) {
- if err := validateCanonicalHandshake13(message); err != nil {
- return false, err
- }
-
- seen, ok := t.seen[id]
- if !ok {
- return false, nil
- }
-
- fingerprint := sha256.Sum256(message)
- if seen.length == len(message) && seen.fingerprint == fingerprint {
- return true, nil
- }
-
- return false, dtlserrors.ErrHandshakeTranscriptMessageChanged
-}
diff --git a/handshaker_13_test.go b/handshaker_13_test.go
deleted file mode 100644
index c018af585..000000000
--- a/handshaker_13_test.go
+++ /dev/null
@@ -1,364 +0,0 @@
-// SPDX-FileCopyrightText: 2026 The Pion community
-// SPDX-License-Identifier: MIT
-
-package dtls
-
-import (
- "context"
- "testing"
- "time"
-
- dtlserrors "github.com/pion/dtls/v3/internal/errors"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/signaturehash"
- "github.com/pion/dtls/v3/pkg/protocol"
- "github.com/pion/dtls/v3/pkg/protocol/extension"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
- "github.com/pion/logging"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestHandshakeFSM13OwnsTranscriptAndPropagatesContext(t *testing.T) {
- state := &dtlsstate.State{IsClient: true, LocalVersion: protocol.Version1_3}
- cache := newHandshakeCache()
- cfg := testHandshakeConfig13(t)
-
- fsm, err := newHandshakeFSM13(state, cache, cfg, flight13_1, nil, nil)
- require.NoError(t, err)
- require.NotNil(t, fsm.transcript)
-
- flightCtx := fsm.flightContext()
- assert.Same(t, state, flightCtx.state)
- assert.Same(t, cache, flightCtx.cache)
- assert.Same(t, cfg, flightCtx.cfg)
- assert.Same(t, fsm.transcript, flightCtx.transcript)
-}
-
-func TestHandshakeFSM13DualStackClientHelloSeedsTranscript(t *testing.T) {
- state := &dtlsstate.State{IsClient: true, LocalVersion: protocol.Version1_3}
- cache := newHandshakeCache()
- cfg := testHandshakeConfig13(t)
- cfg.clientHelloMessageHook = func(ch handshake.MessageClientHello) handshake.Message {
- ch.SessionID = []byte{0xaa, 0xbb}
-
- return &ch
- }
-
- transcript := newHandshakeTranscript13()
- pkts, dtlsAlert, err := flight13_1Generate(nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- transcript: transcript,
- })
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
- require.Len(t, pkts, 1)
-
- const messageSequence = 7
- content, ok := pkts[0].record.Content.(*handshake.Handshake)
- require.True(t, ok)
- content.Header.MessageSequence = messageSequence
-
- expected := canonicalPacketHandshake13(t, pkts[0])
- appended, err := appendClientHelloInitialFlights13(transcript, pkts)
- require.NoError(t, err)
- require.True(t, appended)
-
- fsm, err := newHandshakeFSM13(state, cache, cfg, flight13_1, pkts, transcript)
- require.NoError(t, err)
- require.NotNil(t, fsm.transcript)
- require.Same(t, transcript, fsm.transcript)
- require.Len(t, fsm.transcript.pending, 1)
- require.Len(t, fsm.transcript.order, 1)
-
- assert.Equal(t, expected, fsm.transcript.pending[0])
- assert.Equal(t, expected, fsm.transcript.transcript)
- assert.Equal(t, transcriptMessageID13{
- sender: transcriptClient13,
- seq: messageSequence,
- }, fsm.transcript.order[0].id)
- assert.Equal(t, handshake.TypeClientHello, fsm.transcript.order[0].typ)
-}
-
-func TestHandshakeFSM13TranscriptSurvivesStateChangesAndRetransmitSeed(t *testing.T) {
- state := &dtlsstate.State{IsClient: true, LocalVersion: protocol.Version1_3}
- cache := newHandshakeCache()
- cfg := testHandshakeConfig13(t)
- transcript := newHandshakeTranscript13()
-
- pkts, dtlsAlert, err := flight13_1Generate(nil, &handshakeContext13{
- state: state,
- cache: cache,
- cfg: cfg,
- transcript: transcript,
- })
- require.NoError(t, err)
- require.Nil(t, dtlsAlert)
-
- fsm, err := newHandshakeFSM13(state, cache, cfg, flight13_1, pkts, transcript)
- require.NoError(t, err)
-
- transcript = fsm.transcript
- before := append([]byte(nil), transcript.transcript...)
- require.Len(t, transcript.pending, 1)
-
- fsm.currentFlight = flight13_2
- fsm.retransmit = true
- fsm.retransmitInterval *= 2
-
- assert.Same(t, transcript, fsm.transcript)
- assert.Equal(t, before, fsm.transcript.transcript)
- assert.Same(t, transcript, fsm.flightContext().transcript)
-
- require.NoError(t, fsm.seedTranscriptFromInitialFlights())
- assert.Same(t, transcript, fsm.transcript)
- assert.Equal(t, before, fsm.transcript.transcript)
- assert.Len(t, fsm.transcript.pending, 1)
-}
-
-func TestHandshakeFSM13DualStackClientHelloRequired(t *testing.T) {
- state := &dtlsstate.State{IsClient: true, LocalVersion: protocol.Version1_3}
- cache := newHandshakeCache()
- cfg := testHandshakeConfig13(t)
-
- fsm, err := newHandshakeFSM13(state, cache, cfg, flight13_1, []*packet{}, newHandshakeTranscript13())
- require.Nil(t, fsm)
- require.ErrorIs(t, err, dtlserrors.ErrHandshakeTranscriptMissingClientHello)
-}
-
-func TestHandshakeFSM13PrepareHelloRetryRequestRequiresSeededTranscript(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{
- LocalVersion: protocol.Version1_3,
- CipherSuite: cfg.localCipherSuites[0],
- }
- cache := newHandshakeCache()
-
- fsm, err := newHandshakeFSM13(state, cache, cfg, flight13_2, nil, nil)
- require.NoError(t, err)
-
- nextState, err := fsm.prepare(context.Background(), nil)
- require.ErrorIs(t, err, dtlserrors.ErrHandshakeTranscriptHelloRetryRequestInvalid)
- assert.Equal(t, handshakeErrored, nextState)
- require.Len(t, fsm.flights, 1)
- assert.Empty(t, fsm.transcript.order)
- assert.Empty(t, fsm.transcript.transcript)
- assert.Equal(t, 1, state.HandshakeSendSequence)
-}
-
-func TestHandshakeFSM13PrepareCommitsOutboundClientHello(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{IsClient: true, LocalVersion: protocol.Version1_3}
- cache := newHandshakeCache()
-
- fsm, err := newHandshakeFSM13(state, cache, cfg, flight13_1, nil, nil)
- require.NoError(t, err)
-
- nextState, err := fsm.prepare(context.Background(), nil)
- require.NoError(t, err)
- assert.Equal(t, handshakeSending, nextState)
- require.Len(t, fsm.flights, 1)
-
- expected := canonicalPacketHandshake13(t, fsm.flights[0])
- require.Len(t, fsm.transcript.order, 1)
- assert.Equal(t, transcriptMessageID13{sender: transcriptClient13, seq: 0}, fsm.transcript.order[0].id)
- assert.Equal(t, handshake.TypeClientHello, fsm.transcript.order[0].typ)
- assert.Equal(t, expected, fsm.transcript.transcript)
- assert.Equal(t, 1, state.HandshakeSendSequence)
-}
-
-func TestHandshakeFSM13PrepareCommitsOutboundHelloRetryRequestWithSeededTranscript(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- state := &dtlsstate.State{
- LocalVersion: protocol.Version1_3,
- CipherSuite: cfg.localCipherSuites[0],
- }
- cache := newHandshakeCache()
- transcript := newHandshakeTranscript13()
- clientHello := transcriptTestClientHelloPacket13([]byte{0x01}, 0)
- clientHelloCanonical := canonicalPacketHandshake13(t, clientHello)
- require.NoError(t, appendOutboundHandshakeFlight13(transcript, true, nil, []*packet{clientHello}))
-
- fsm, err := newHandshakeFSM13(state, cache, cfg, flight13_2, nil, transcript)
- require.NoError(t, err)
-
- nextState, err := fsm.prepare(context.Background(), nil)
- require.NoError(t, err)
- assert.Equal(t, handshakeSending, nextState)
- require.Len(t, fsm.flights, 1)
-
- helloRetryRequestCanonical := canonicalPacketHandshake13(t, fsm.flights[0])
- messageHash := canonicalTranscriptHandshake13(handshake.TypeMessageHash, hashTranscript13(clientHelloCanonical))
- expectedTranscript := append(append([]byte(nil), messageHash...), helloRetryRequestCanonical...)
-
- assert.Equal(t, expectedTranscript, fsm.transcript.transcript)
- require.Len(t, fsm.transcript.order, 2)
- assert.Equal(t, transcriptMessageID13{sender: transcriptServer13, seq: 0}, fsm.transcript.order[1].id)
- assert.Equal(t, handshake.TypeServerHello, fsm.transcript.order[1].typ)
- assert.Equal(t, 1, state.HandshakeSendSequence)
-}
-
-func canonicalPacketHandshake13(t *testing.T, p *packet) []byte {
- t.Helper()
-
- content, ok := p.record.Content.(*handshake.Handshake)
- require.True(t, ok)
- raw, err := content.Marshal()
- require.NoError(t, err)
- canonical, err := canonicalHandshake13(raw)
- require.NoError(t, err)
-
- return canonical
-}
-
-func testHandshakeConfig13(t *testing.T) *handshakeConfig {
- t.Helper()
-
- cipherSuites, err := parseCipherSuitesForVersions(
- nil,
- nil,
- true,
- false,
- protocol.Version1_3,
- protocol.Version1_3,
- )
- require.NoError(t, err)
-
- loggerFactory := logging.NewDefaultLoggerFactory()
-
- return &handshakeConfig{
- localCipherSuites: cipherSuites,
- ellipticCurves: defaultCurves,
- initialRetransmitInterval: time.Second,
- extendedMasterSecret: RequestExtendedMasterSecret,
- log: loggerFactory.NewLogger("dtls"),
- minVersion: protocol.Version1_3,
- maxVersion: protocol.Version1_3,
- localSignatureSchemes: signaturehash.Algorithms13(),
- localCertSignatureSchemes: nil,
- localSRTPProtectionProfiles: nil,
- }
-}
-
-func TestAppendOutboundHandshakeFlight13ClientHello(t *testing.T) {
- transcript := newHandshakeTranscript13()
- pkt := transcriptTestClientHelloPacket13([]byte{0x01}, 3)
- expected := canonicalPacketHandshake13(t, pkt)
-
- err := appendOutboundHandshakeFlight13(transcript, true, nil, []*packet{pkt})
- require.NoError(t, err)
- require.Len(t, transcript.order, 1)
- require.Len(t, transcript.pending, 1)
-
- assert.Equal(t, transcriptMessageID13{sender: transcriptClient13, seq: 3}, transcript.order[0].id)
- assert.Equal(t, handshake.TypeClientHello, transcript.order[0].typ)
- assert.Equal(t, expected, transcript.pending[0])
- assert.Equal(t, expected, transcript.transcript)
-}
-
-func TestAppendOutboundHandshakeFlight13DuplicateNoop(t *testing.T) {
- transcript := newHandshakeTranscript13()
- pkt := transcriptTestClientHelloPacket13([]byte{0x01}, 0)
-
- require.NoError(t, appendOutboundHandshakeFlight13(transcript, true, nil, []*packet{pkt}))
- before := append([]byte(nil), transcript.transcript...)
-
- require.NoError(t, appendOutboundHandshakeFlight13(transcript, true, nil, []*packet{pkt}))
- assert.Equal(t, before, transcript.transcript)
- assert.Len(t, transcript.order, 1)
-}
-
-func TestAppendOutboundHandshakeFlight13ChangedSameSequenceFails(t *testing.T) {
- transcript := newHandshakeTranscript13()
- pkt := transcriptTestClientHelloPacket13([]byte{0x01}, 0)
- changedPkt := transcriptTestClientHelloPacket13([]byte{0x02}, 0)
-
- require.NoError(t, appendOutboundHandshakeFlight13(transcript, true, nil, []*packet{pkt}))
- err := appendOutboundHandshakeFlight13(transcript, true, nil, []*packet{changedPkt})
-
- assert.ErrorIs(t, err, dtlserrors.ErrHandshakeTranscriptMessageChanged)
-}
-
-func TestAppendOutboundHandshakeFlight13HelloRetryRequest(t *testing.T) {
- cfg := testHandshakeConfig13(t)
- cipherSuite := cfg.localCipherSuites[0]
- transcript := newHandshakeTranscript13()
- clientHello := transcriptTestClientHelloPacket13([]byte{0x01}, 0)
- helloRetryRequest := transcriptTestHelloRetryRequestPacket13(t, cipherSuite, 0)
-
- clientHelloCanonical := canonicalPacketHandshake13(t, clientHello)
- helloRetryRequestCanonical := canonicalPacketHandshake13(t, helloRetryRequest)
-
- require.NoError(t, appendOutboundHandshakeFlight13(transcript, true, cipherSuite, []*packet{clientHello}))
- require.NoError(t, appendOutboundHandshakeFlight13(transcript, false, cipherSuite, []*packet{helloRetryRequest}))
-
- clientHelloHash := hashTranscript13(clientHelloCanonical)
- messageHash := canonicalTranscriptHandshake13(handshake.TypeMessageHash, clientHelloHash)
- expectedTranscript := append(append([]byte(nil), messageHash...), helloRetryRequestCanonical...)
- assert.Equal(t, expectedTranscript, transcript.transcript)
-
- sum, err := transcript.sum()
- require.NoError(t, err)
- assert.Equal(t, hashTranscript13(messageHash, helloRetryRequestCanonical), sum)
- require.Len(t, transcript.order, 2)
- assert.Equal(t, handshake.TypeClientHello, transcript.order[0].typ)
- assert.Equal(t, handshake.TypeServerHello, transcript.order[1].typ)
-
- before := append([]byte(nil), transcript.transcript...)
- require.NoError(t, appendOutboundHandshakeFlight13(transcript, false, cipherSuite, []*packet{helloRetryRequest}))
- assert.Equal(t, before, transcript.transcript)
- assert.Len(t, transcript.order, 2)
-}
-
-func transcriptTestClientHelloPacket13(sessionID []byte, seq uint16) *packet {
- return &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Header: handshake.Header{MessageSequence: seq},
- Message: &handshake.MessageClientHello{
- Version: protocol.Version1_2,
- SessionID: sessionID,
- CipherSuiteIDs: []uint16{uint16(TLS_AES_128_GCM_SHA256)},
- CompressionMethods: defaultCompressionMethods(),
- },
- },
- },
- }
-}
-
-func transcriptTestHelloRetryRequestPacket13(tb testing.TB, cipherSuite CipherSuite, seq uint16) *packet {
- tb.Helper()
-
- random := handshake.Random{}
- random.UnmarshalFixed([32]byte(handshake.HelloRetryRequestRandom()))
- cipherSuiteID := uint16(cipherSuite.ID())
-
- return &packet{
- record: &recordlayer.RecordLayer{
- Header: recordlayer.Header{
- Version: protocol.Version1_2,
- },
- Content: &handshake.Handshake{
- Header: handshake.Header{MessageSequence: seq},
- Message: &handshake.MessageServerHello{
- Version: protocol.Version1_2,
- Random: random,
- CipherSuiteID: &cipherSuiteID,
- CompressionMethod: defaultCompressionMethods()[0],
- Extensions: []extension.Extension{
- &extension.SupportedVersions{
- Versions: []protocol.Version{protocol.Version1_3},
- SelectedVersion: true,
- },
- },
- },
- },
- },
- }
-}
diff --git a/handshaker_test.go b/handshaker_test.go
index 98b7505cf..f6b0f7048 100644
--- a/handshaker_test.go
+++ b/handshaker_test.go
@@ -8,19 +8,29 @@ import (
"context"
"crypto/tls"
"errors"
+ "fmt"
+ "net"
+ "strings"
"sync"
+ "sync/atomic"
"testing"
"time"
- dtlsstate "github.com/pion/dtls/v3/internal/state"
- "github.com/pion/dtls/v3/pkg/crypto/selfsign"
- "github.com/pion/dtls/v3/pkg/crypto/signaturehash"
- "github.com/pion/dtls/v3/pkg/protocol/alert"
- "github.com/pion/dtls/v3/pkg/protocol/handshake"
- "github.com/pion/dtls/v3/pkg/protocol/recordlayer"
+ dtlsconfig "github.com/pion/dtls/v4/internal/config"
+ dtlsflight "github.com/pion/dtls/v4/internal/flight"
+ dtlsflight12 "github.com/pion/dtls/v4/internal/flight/flight12"
+ dtlshandshake "github.com/pion/dtls/v4/internal/handshake"
+ dtlsstate "github.com/pion/dtls/v4/internal/state"
+ cryptosuite "github.com/pion/dtls/v4/pkg/crypto/ciphersuite"
+ "github.com/pion/dtls/v4/pkg/crypto/selfsign"
+ "github.com/pion/dtls/v4/pkg/crypto/signaturehash"
+ "github.com/pion/dtls/v4/pkg/protocol"
+ "github.com/pion/dtls/v4/pkg/protocol/alert"
+ "github.com/pion/dtls/v4/pkg/protocol/handshake"
"github.com/pion/logging"
- "github.com/pion/transport/v4/test"
+ "github.com/pion/transport/v5/test"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
)
const nonZeroRetransmitInterval = 100 * time.Millisecond
@@ -29,10 +39,10 @@ const nonZeroRetransmitInterval = 100 * time.Millisecond
// when a key log writer is given.
func TestWriteKeyLog(t *testing.T) {
var buf bytes.Buffer
- cfg := handshakeConfig{
- keyLogWriter: &buf,
+ cfg := dtlsconfig.HandshakeConfig{
+ KeyLogWriter: &buf,
}
- cfg.writeKeyLog("LABEL", []byte{0xAA, 0xBB, 0xCC}, []byte{0xDD, 0xEE, 0xFF})
+ cfg.WriteKeyLog("LABEL", []byte{0xAA, 0xBB, 0xCC}, []byte{0xDD, 0xEE, 0xFF})
// Secrets follow the format