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 @@ join us on Discord Follow us on Bluesky
GitHub Workflow Status - Go Reference + Go Reference Coverage Status - Go Report Card + Go Report Card License: MIT


-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