diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index fcd2ed5e..2b114480 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -2,9 +2,9 @@ name: cargo on: push: - branches: [master] + branches: [ master, v0.20.x, v0.21.x ] pull_request: - branches: [master] + branches: [ master, v0.20.x, v0.21.x ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -18,7 +18,7 @@ jobs: name: Build strategy: matrix: - os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] + os: [ 'ubuntu-latest', 'macos-latest', 'windows-latest' ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v3 @@ -26,7 +26,7 @@ jobs: run: cargo build --verbose test: - name: Test + name: Test (default backend) strategy: matrix: os: [ 'ubuntu-latest' ] #, 'macos-latest', 'windows-latest' ] @@ -36,6 +36,184 @@ jobs: - name: Run tests run: cargo test --verbose + crypto_backend_baseline: + name: Crypto baseline (${{ matrix.backend }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + backend: [ crypto-ring, crypto-aws-lc-rs ] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Test workspace + run: cargo test --workspace --no-fail-fast --lib --bins --examples --tests --no-default-features --features rtc/${{ matrix.backend }} --verbose + - name: Test workspace documentation + run: cargo test --workspace --doc --no-default-features --features rtc/${{ matrix.backend }} --verbose + - name: Build standalone crypto consumers + shell: bash + run: | + cargo check --package rtc-dtls --no-default-features --features ${{ matrix.backend }} --verbose + cargo check --package rtc-srtp --no-default-features --features ${{ matrix.backend }} --verbose + cargo check --package rtc-stun --no-default-features --features ${{ matrix.backend }} --verbose + + rtc_crypto_provider_matrix: + name: rtc-crypto (${{ matrix.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: default + args: --features test-support + - name: ring + args: --no-default-features --features crypto-ring,test-support + - name: aws-lc-rs + args: --no-default-features --features crypto-aws-lc-rs,test-support + - name: ring + aws-lc-rs + args: --no-default-features --features crypto-ring,crypto-aws-lc-rs,test-support + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Test provider configuration + run: cargo test --package rtc-crypto ${{ matrix.args }} --verbose + + rtc_crypto_custom_provider: + name: rtc-crypto (no built-in, custom provider) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Test downstream-style provider + run: cargo test --package rtc-crypto --no-default-features --features test-support --test custom_provider --verbose + - name: Verify publishable package + run: cargo package --package rtc-crypto --no-default-features + + workspace_provider_matrix: + name: Workspace (${{ matrix.name }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: default + args: "" + - name: ring only + args: --no-default-features --features crypto-ring + - name: aws-lc-rs only + args: --no-default-features --features crypto-aws-lc-rs + # Both backends in one graph. This was a `compile_error!` before G3; it is now the + # regression guard for Cargo feature additivity, since feature unification can enable + # both from unrelated dependencies. + - name: ring + aws-lc-rs + args: --features crypto-ring,crypto-aws-lc-rs + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + # Compiles the `assert_dyn_compatible` guard in rtc-crypto/src/lib.rs under this feature + # set. `Arc` is load-bearing for the whole design, and object + # safety is lost silently by adding one generic method — possibly behind a feature flag. + - name: Build workspace and dyn-compatibility guard + run: cargo build --workspace --all-targets ${{ matrix.args }} --verbose + - name: Test workspace + run: cargo test --workspace --no-fail-fast ${{ matrix.args }} --verbose + - name: Doctests + run: cargo test --workspace --doc ${{ matrix.args }} --verbose + + standalone_crate_checks: + name: Standalone crates (${{ matrix.backend }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + backend: [ crypto-ring, crypto-aws-lc-rs ] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + # Crates that forward provider features must build on their own, not only as part of the + # workspace, since standalone protocol users are a supported audience. + - name: Check provider-forwarding crates + run: | + cargo check --package rtc-crypto --no-default-features --features ${{ matrix.backend }} --all-targets --verbose + cargo check --package rtc-dtls --no-default-features --features ${{ matrix.backend }} --all-targets --verbose + cargo check --package rtc-srtp --no-default-features --features ${{ matrix.backend }} --all-targets --verbose + cargo check --package rtc-stun --no-default-features --features ${{ matrix.backend }} --all-targets --verbose + cargo check --package rtc-ice --no-default-features --features ${{ matrix.backend }} --all-targets --verbose + cargo check --package rtc-turn --no-default-features --features ${{ matrix.backend }} --all-targets --verbose + # These perform no cryptography and must not have acquired a provider dependency. + - name: Check crates that must stay crypto-free + run: | + cargo check --package rtc-shared --all-targets --verbose + cargo check --package rtc-sctp --all-targets --verbose + cargo check --package rtc-rtp --all-targets --verbose + cargo check --package rtc-rtcp --all-targets --verbose + cargo check --package rtc-sdp --all-targets --verbose + cargo check --package rtc-media --all-targets --verbose + + crypto_dependency_audit: + name: Crypto dependency boundary + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + # Enforces the G3 invariant: rtc-crypto is the only crate naming a crypto implementation. + # The script parses dependency sections so that provider-feature forwarding + # (`ring = ["crypto/ring"]`) and the root `[workspace.dependencies]` table are not + # mistaken for real dependencies. + - name: No crypto implementation outside rtc-crypto + run: python3 scripts/check-crypto-boundary.py + - name: No mutually exclusive backend guards + run: | + if grep -rn 'At most one of the features' --include='*.rs' . ; then + echo "A mutually exclusive backend compile_error! guard is still present." + exit 1 + fi + - name: No backend-name aliasing + run: | + if grep -rn 'extern crate aws_lc_rs as ring' --include='*.rs' . ; then + echo "The aws-lc-rs-as-ring alias is still present." + exit 1 + fi + + package_publication_order: + name: cargo package (publication order) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + # rtc-crypto is new in this release and has no intra-workspace dependencies, so it is the + # first crate in publication order and the only one whose packaging can be verified from + # a clean checkout today. + - name: Package rtc-crypto first + run: cargo package --package rtc-crypto --allow-dirty + - name: Package crates with no rtc-crypto dependency + run: cargo package --package rtc-shared --allow-dirty + # Crates depending on rtc-crypto cannot be packaged until rtc-crypto 0.21.0 exists on + # crates.io: `cargo package` resolves the dependency graph against the registry and the + # path dependency is stripped from the published manifest. This is expected for a newly + # introduced crate, not a regression. This step asserts the failure is exactly that + # missing-version resolution error and nothing else, so a different packaging break is + # still caught. + - name: Confirm dependents block only on rtc-crypto publication + run: | + if cargo package --package rtc-stun --allow-dirty 2>package.log; then + echo "rtc-stun packaged; rtc-crypto appears to be published." + echo "Replace this step with a normal packaging check." + exit 0 + fi + if grep -q 'failed to select a version for the requirement `rtc-crypto' package.log; then + echo "Expected: rtc-stun awaits the first rtc-crypto publish." + exit 0 + fi + echo "rtc-stun packaging failed for an unexpected reason:" + cat package.log + exit 1 + rustfmt_and_clippy: name: Check rustfmt style && run clippy runs-on: ubuntu-latest diff --git a/.github/workflows/grcov.yml b/.github/workflows/grcov.yml index 44ae7c6f..3f32a5aa 100644 --- a/.github/workflows/grcov.yml +++ b/.github/workflows/grcov.yml @@ -2,9 +2,9 @@ name: coverage on: push: - branches: [ master ] + branches: [ master, v0.20.x, v0.21.x ] pull_request: - branches: [ master ] + branches: [ master, v0.20.x, v0.21.x ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -26,13 +26,9 @@ jobs: - nightly crypto: - name: ring - features: rtc/ring - packages: --workspace - test_args: "" + features: crypto-ring - name: aws-lc-rs - features: aws-lc-rs - packages: --package rtc - test_args: --lib + features: crypto-aws-lc-rs steps: - name: Checkout source code uses: actions/checkout@v4 @@ -49,7 +45,7 @@ jobs: tool: grcov - name: Test - run: cargo test ${{ matrix.crypto.packages }} --no-fail-fast --no-default-features --features ${{ matrix.crypto.features }} ${{ matrix.crypto.test_args }} + run: cargo test --workspace --no-fail-fast --no-default-features --features ${{ matrix.crypto.features }} env: CARGO_INCREMENTAL: "0" RUSTFLAGS: "-Cinstrument-coverage" diff --git a/.github/workflows/semver.yml b/.github/workflows/semver.yml new file mode 100644 index 00000000..b337e439 --- /dev/null +++ b/.github/workflows/semver.yml @@ -0,0 +1,27 @@ +name: SemVer Check + +on: + push: + branches: [ master, v0.20.x, v0.21.x ] + pull_request: + branches: [ master, v0.20.x, v0.21.x ] + +jobs: + semver: + name: Check SemVer + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry and build artifacts + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-semver-checks + run: cargo install cargo-semver-checks --locked + + - name: Run cargo-semver-checks + run: cargo semver-checks --workspace --default-features diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..96d46660 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,165 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- `RTCCrypto::new_hmac` returns a keyed `Mac` whose key schedule is derived once, mirroring the + existing keyed cipher factories. It replaces the removed one-shot `hmac`/`verify_hmac`. +- **New `rtc-crypto` crate: a provider-neutral cryptographic API** + ([webrtc#839](https://github.com/webrtc-rs/webrtc/issues/839), + [rtc#128](https://github.com/webrtc-rs/rtc/issues/128)). `RTCCryptoProvider` bundles an + `RTCCrypto` operations trait and an `RTCRandom` CSPRNG. Two built-in providers ship, + `RingProvider` and `AwsLcRsProvider`, and applications can supply their own — for OpenSSL, a + FIPS-validated module, an HSM, or a platform backend — by implementing the public traits. A + reusable conformance suite (`rtc_crypto::conformance::assert_provider`, behind the + `test-support` feature) validates any implementation against the same RFC vectors the built-ins + pass. +- `SettingEngine::set_crypto_provider` selects the provider per peer connection. Two peer + connections in one process may use different providers. There is no process-global provider to + install. +- Explicit provider constructors on the standalone protocol crates: + `rtc_dtls::ConfigBuilder::with_crypto_provider`, `rtc_srtp::Context::new_with_provider`, and + `MessageIntegrity::{new_raw,new_short_term,new_long_term}_integrity_with_provider`. +- Provider-neutral certificate construction: `RTCCertificate::generate`, + `RTCCertificate::generate_from_signing_key`, and `RTCCertificate::from_pkcs8`. + `rcgen::CertificateParams` is re-exported from `rtc::peer_connection::certificate`, so callers + no longer need a direct `rcgen` dependency to name it. +- DTLS record-protection benchmarks (`cargo bench --package rtc-dtls --bench record_protection`) + and SRTP AEAD plus context-construction benchmarks, all reporting each enabled provider under + identical inputs and separating one-time key-schedule cost from per-packet cost. + +### Changed + +- **No library code resolves a default crypto provider.** `crypto::default_provider()` is now + called in exactly one place — peer-connection construction — where the application either + supplied one via `SettingEngine::set_crypto_provider` or gets the feature-selected built-in. + Every constructor below that takes an `Arc` from its caller, so a + `--no-default-features` build fails at configuration time instead of deep inside a handshake. + The `*_with_provider` constructor pairs are collapsed into single provider-taking constructors + (`Context::new`, `Client::new`, `Agent::new`, `Certificate::generate_self_signed`, + `Certificate::from_pem`, `RTCCertificate::from_pem`, `RTCCertificate::get_fingerprints`, the + `MessageIntegrity` constructors). `Default` impls that resolved a provider — `HandshakeConfig`, + `rtc_dtls::State`, `Agent`, `RTCDtlsTransport` — are removed in favour of those constructors, + and `ConfigBuilder::build` errors when no provider was configured rather than inventing one. +- Each protocol crate re-exports the crypto API (`rtc_srtp::crypto`, `rtc_stun::crypto`, + `rtc_ice::crypto`, `rtc_turn::crypto`, `rtc_dtls::crypto_provider`) so standalone users can name + `Arc` without a direct `rtc-crypto` dependency. +- **`crypto-ring` and `crypto-aws-lc-rs` Cargo features are now additive.** Enabling both builds successfully + and is covered in CI. Previously each of `rtc`, `rtc-dtls`, `rtc-srtp`, and `rtc-stun` carried a + `compile_error!` rejecting the combination, which made an otherwise valid build fail whenever + Cargo feature unification pulled in both. `rtc_crypto::default_provider()` still prefers `ring` + when both are enabled, so default behaviour is unchanged. +- DTLS, SRTP, STUN, ICE, TURN, and the top-level `rtc` crate perform all cryptography through the + configured provider. Protocol composition is unchanged and stays in its own crate: the TLS 1.2 + PRF in DTLS, SRTP key derivation and packet layout in SRTP, STUN integrity framing in STUN. + Default algorithm selection and wire behaviour are unchanged. +- `rtc_dtls::State::export_keying_material` is now an inherent method returning `SecretVec`, and + `rtc_srtp::Config::set_session_keys_from_keying_material` consumes the exported bytes. The + top-level crate performs the handoff, so `rtc-srtp` and `rtc-dtls` remain independent of each + other. `rtc_srtp::config::LABEL_EXTRACTOR_DTLS_SRTP` is now public for standalone callers. +- `rtc_stun::MessageIntegrity` changed from a public tuple struct to named fields and now holds + its provider, because `Setter::add_to` and `check` have no parameter through which to receive + one. Its key is stored as a `SecretVec`. +- **`MediaEngine::register_default_codecs` no longer registers `video/ulpfec`** + ([#837](https://github.com/webrtc-rs/webrtc/issues/837)). The receive path does not recover + media from ULPFEC packets, so offering the codec invited peers to send repair packets that + could not be used. Applications that want it can still register it explicitly with + `MIME_TYPE_ULP_FEC`, which remains public. **This changes the default offer's SDP**: payload + type 116 is no longer present. ULPFEC will return to the defaults once receive-side recovery + is implemented. + +### Deprecated + +- + +### Removed + +These are deliberate pre-1.0 API removals, not behaviour changes. Every entry has a replacement; +see `docs/crypto-provider-migration.md` for before/after examples. + +- Removed the partial `openssl` and `vendored-openssl` Cargo features from `rtc-srtp` and `rtc`; SRTP cryptography now uses the selected `rtc-crypto` provider for every protection profile. They selected only an alternate AES-CTR path and never implemented the full provider contract. An OpenSSL backend can return as a complete downstream `RTCCryptoProvider`. +- Removed `rtc_shared::crypto::KeyingMaterialExporter`. `rtc-shared` performs no cryptography and + no longer carries a crypto bridge. +- Removed `rtc_srtp::Config::extract_session_keys_from_dtls`; use + `set_session_keys_from_keying_material` with material exported from the DTLS session. +- Removed the default-resolving `MessageIntegrity::{new_raw_integrity, new_short_term_integrity, + new_long_term_integrity}` constructors and the derived `Default` impl. `Default` produced a + credential with an empty key, which is never valid; use the `_with_provider` constructors. +- Removed `rtc_dtls::crypto::CustomSigner` and `CryptoPrivateKey::from_custom_signer`. Implement + `rtc_crypto::SigningKey` and use `CryptoPrivateKey::from_signing_key`. This is a superset of the + old capability: non-exportable HSM and KMS keys are supported, and `to_pkcs8_der()` returns + `Ok(None)` for them instead of fabricating key bytes. +- Removed `CryptoPrivateKey::from_key_pair`, its `TryFrom<&rcgen::KeyPair>` impl, and the + `serialized_der` field. +- Removed `RTCCertificate::from_key_pair` and `from_key_pair_with_provider`; use + `RTCCertificate::generate` or `generate_from_signing_key`. +- Removed the `Sec1`, `P256`, `RcGen`, `AesGcm`, and `Aes` variants from `rtc_shared::Error`, + along with the `sec1`, `p256`, `rcgen`, `aes`, and `aes-gcm` dependencies of `rtc-shared`. These + put crypto-crate types in the public API of every crate in the workspace and pinned their major + versions there. Crypto failures now cross crate boundaries as `Error::Crypto(String)`. +- Removed `RTCCrypto::hmac` and `RTCCrypto::verify_hmac`. They are exactly + `new_hmac(..)?.sign(..)` and `new_hmac(..)?.verify(..)`, and retaining them preserved a path + that re-derives the HMAC key schedule on every call. +- Removed the four duplicated `compile_error!` backend guards and the four + `extern crate aws_lc_rs as ring;` aliases. + +### Fixed + +- `rtc-crypto`'s AES-CTR keystream now uses a batched implementation instead of one + `encrypt_block` call per 16-byte block, which defeated AES-NI / ARMv8 instruction pipelining. + Roughly 9-10% faster on a 1200-byte SRTP payload. +- **SRTP no longer derives the HMAC key schedule on every packet.** `RTCCrypto::new_hmac` returns + a keyed `Mac`, and `rtc-srtp` keys its SRTP and SRTCP MACs once per context instead of passing + raw key bytes per packet. Roughly 40% faster per RTCP packet and 7% per 1200-byte RTP packet. + A counting provider in `rtc-srtp/tests/provider_profiles.rs` guards the invariant. +- **DTLS CBC no longer derives its record MAC key on every record.** `CryptoCbc` holds two keyed + `Mac` objects instead of passing raw key bytes to `prf_mac` per record: ~4% faster on encrypt and + ~7% on decrypt, with the key schedule moving to epoch setup. +- STUN `MESSAGE-INTEGRITY` was measured against its pre-migration baseline and shows no regression + (see `rtc-stun/benches/README.md`); it already used `ring`'s HMAC-SHA1 before G3. +- **The built-in `RTCRandom` implementations no longer read the operating system on every call.** + DTLS generates a GCM explicit nonce and a CBC record IV per record; routing those through the + backend's `SystemRandom` cost ~829 ns on `ring` and ~2196 ns on `aws-lc-rs`, against ~8 ns for + the thread-local CSPRNG the pre-provider code used. They now use an OS-seeded, periodically + reseeded thread-local CSPRNG, as BoringSSL and OpenSSL do internally. `SystemRandom` is still + used where the backend owns the operation — keypair generation and signing. DTLS GCM encryption + went from 1.015 µs to 262 ns per record. A deployment needing validated entropy everywhere + supplies its own `RTCRandom`. +- **The `ring` provider composes RustCrypto's HMAC-SHA1.** `ring` exposes SHA-1 only as + `HMAC_SHA1_FOR_LEGACY_USE_ONLY` and does not use the ARMv8 SHA-1 instructions: 4469 ns against + RustCrypto's 1373 ns over a 1212-byte message. The built-in providers are already composite — + AES-CTR, CCM, CBC and MD5 come from RustCrypto — so HMAC-SHA1 is composed the same way. This + closes the gap without making `aws-lc-rs` the default, which would impose the `aws-lc-sys` C + toolchain on every downstream build. SHA-256 stays on `ring`; `aws-lc-rs` keeps its own SHA-1. + +**Net effect: every per-packet and per-record benchmark is at parity with or faster than the +pre-migration baseline on the default provider.** SRTP AES-CM/HMAC RTP encryption 1.725 µs before +and 1.723 µs after; DTLS GCM record encryption 270.5 ns before and 270.1 ns after; DTLS CBC 6-8% +faster than before. Context and epoch setup costs rose, which is the intended trade — key +schedules moved off the per-packet path. See the three `benches/README.md` files for the full +tables and methodology. + +- Add RSA as an allowed private key kind in rtc_dtls::ConfigBuilder:: + validate- [PR #141](https://github.com/webrtc-rs/rtc/pull/141) + +### Security + +- + +## [0.20.0] - 2026-07-31 + +### Added + +- The `rtc` v0.20.0 is Sans-I/O protocol core with complete WebRTC stack (95%+ W3C API compliance) + +[Unreleased]: https://github.com/webrtc-rs/rtc/compare/0.20.0...HEAD + +[0.20.1]: https://github.com/webrtc-rs/rtc/compare/0.20.0...0.20.1 + +[0.20.0]: https://github.com/webrtc-rs/rtc/releases/tag/0.20.0 diff --git a/Cargo.toml b/Cargo.toml index 663fa51a..3dc39d60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "rtc-crypto", "rtc-datachannel", "rtc-dtls", "rtc-ice", @@ -22,7 +23,7 @@ resolver = "2" opt-level = 0 [workspace.package] -version = "0.20.0-rc.4" +version = "0.21.0" authors = ["Rain Liu "] edition = "2024" license = "MIT/Apache-2.0" @@ -32,21 +33,23 @@ keywords = ["sansio", "networking", "protocols"] categories = ["network-programming"] [workspace.dependencies] -datachannel = { version = "0.20.0-rc.4", path = "rtc-datachannel", package = "rtc-datachannel" } -dtls = { version = "0.20.0-rc.4", path = "rtc-dtls", package = "rtc-dtls", default-features = false } -ice = { version = "0.20.0-rc.4", path = "rtc-ice", package = "rtc-ice", default-features = false } -interceptor = { version = "0.20.0-rc.4", path = "rtc-interceptor", package = "rtc-interceptor" } -interceptor-derive = { version = "0.20.0-rc.4", path = "rtc-interceptor-derive", package = "rtc-interceptor-derive" } -mdns = { version = "0.20.0-rc.4", path = "rtc-mdns", package = "rtc-mdns" } -media = { version = "0.20.0-rc.4", path = "rtc-media", package = "rtc-media" } -rtcp = { version = "0.20.0-rc.4", path = "rtc-rtcp", package = "rtc-rtcp" } -rtp = { version = "0.20.0-rc.4", path = "rtc-rtp", package = "rtc-rtp" } -sctp = { version = "0.20.0-rc.4", path = "rtc-sctp", package = "rtc-sctp" } -sdp = { version = "0.20.0-rc.4", path = "rtc-sdp", package = "rtc-sdp" } -shared = { version = "0.20.0-rc.4", path = "rtc-shared", package = "rtc-shared", default-features = false } -srtp = { version = "0.20.0-rc.4", path = "rtc-srtp", package = "rtc-srtp", default-features = false } -stun = { version = "0.20.0-rc.4", path = "rtc-stun", package = "rtc-stun", default-features = false } -turn = { version = "0.20.0-rc.4", path = "rtc-turn", package = "rtc-turn", default-features = false } +crypto = { version = "0.21.0", path = "rtc-crypto", package = "rtc-crypto", default-features = false } +datachannel = { version = "0.21.0", path = "rtc-datachannel", package = "rtc-datachannel" } +dtls = { version = "0.21.0", path = "rtc-dtls", package = "rtc-dtls", default-features = false } +ice = { version = "0.21.0", path = "rtc-ice", package = "rtc-ice", default-features = false } +interceptor = { version = "0.21.0", path = "rtc-interceptor", package = "rtc-interceptor" } +interceptor-derive = { version = "0.21.0", path = "rtc-interceptor-derive", package = "rtc-interceptor-derive" } +mdns = { version = "0.21.0", path = "rtc-mdns", package = "rtc-mdns" } +media = { version = "0.21.0", path = "rtc-media", package = "rtc-media" } +rtcp = { version = "0.21.0", path = "rtc-rtcp", package = "rtc-rtcp" } +rtp = { version = "0.21.0", path = "rtc-rtp", package = "rtc-rtp" } +sctp = { version = "0.21.0", path = "rtc-sctp", package = "rtc-sctp" } +sdp = { version = "0.21.0", path = "rtc-sdp", package = "rtc-sdp" } +shared = { version = "0.21.0", path = "rtc-shared", package = "rtc-shared", default-features = false } +srtp = { version = "0.21.0", path = "rtc-srtp", package = "rtc-srtp", default-features = false } +stun = { version = "0.21.0", path = "rtc-stun", package = "rtc-stun", default-features = false } +turn = { version = "0.21.0", path = "rtc-turn", package = "rtc-turn", default-features = false } +signal = { version = "0.21.0", path = "examples/signal", package = "rtc-signal" } # common dependencies sansio = "1" @@ -58,7 +61,36 @@ ring = "0.17.14" aws-lc-rs = { version = "1.17.3", features = ["aws-lc-sys"], default-features = false } rand = "0.10.1" serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.114" thiserror = "2.0.18" +zeroize = "1.8.2" +pem = "3.0.3" +aes = "0.8.4" +ctr = "0.9.2" +ccm = "0.5.0" +md-5 = "0.10.6" +hmac = "0.12.1" +sha1 = "0.10.6" +subtle = "2.6.1" +x509-parser = "0.16.0" +der-parser = "9.0.0" +rustls = { version = "0.23.27", default-features = false, features = ["std"] } +rkyv = "0.8.17" +bytecheck = "0.8" +url = "2.5.0" +base64 = "0.22.1" +crc = "3.0.1" +slab = "0.4.9" +crc32c = "0.6" +rustc-hash = "2" +memchr = "2.1.1" +socket2 = { version = "0.6", features = ["all"] } +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full", "parsing", "extra-traits"] } +uuid = { version = "1", features = ["v4"] } +unicase = "2.8" +substring = "1.4.5" # dev dependencies env_logger = "0.11.11" @@ -67,6 +99,22 @@ clap = { version = "4.6.1", features = ["derive"] } ctrlc = "3.5.2" criterion = "0.8.2" tokio = { version = "1.52.3", features = ["full"] } +tokio-util = { version = "0.7.17", features = ["codec"] } +tokio-tungstenite = "0.28.0" +hyper = { version = "0.14.28", features = ["full"] } +lazy_static = "1.4.0" +hex = "0.4.3" +crossbeam-channel = "0.5" +assert_matches = "1.5.0" +nearly_eq = "0.2" +regex = "1.10.3" +ipnet = "2.9.0" +waitgroup = "0.1.2" +futures = "0.3.30" +futures-util = "0.3" +local-sync = "0.1.1" +core_affinity = "0.8.1" +anyhow = "1.0.80" [package] name = "rtc" @@ -83,21 +131,19 @@ categories.workspace = true readme = "README.md" [features] -default = ["ring"] -pem = ["dep:pem", "dtls/pem"] -openssl = ["srtp/openssl"] -vendored-openssl = ["srtp/vendored-openssl"] -ring = ["dep:ring", "dtls/ring", "rustls/ring", "rcgen/ring", "ice/ring", "stun/ring", "srtp/ring", "turn/ring"] -aws-lc-rs = ["dep:aws-lc-rs", "dtls/aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs", "ice/aws-lc-rs", "stun/aws-lc-rs", "srtp/aws-lc-rs", "turn/aws-lc-rs"] -__testing = [] +default = ["crypto-ring"] +crypto-ring = ["crypto/crypto-ring", "dtls/crypto-ring", "rustls/ring", "rcgen/ring", "ice/crypto-ring", "stun/crypto-ring", "srtp/crypto-ring", "turn/crypto-ring"] +crypto-aws-lc-rs = ["crypto/crypto-aws-lc-rs", "dtls/crypto-aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs", "ice/crypto-aws-lc-rs", "stun/crypto-aws-lc-rs", "srtp/crypto-aws-lc-rs", "turn/crypto-aws-lc-rs"] [dependencies] -shared = { workspace = true, default-features = false, features = ["crypto", "marshal", "replay"] } +shared = { workspace = true, default-features = false, features = ["marshal", "replay"] } +crypto.workspace = true sansio.workspace = true datachannel.workspace = true dtls.workspace = true ice.workspace = true interceptor.workspace = true +interceptor-derive.workspace = true mdns.workspace = true media.workspace = true rtcp.workspace = true @@ -110,41 +156,37 @@ turn.workspace = true bytes.workspace = true log.workspace = true -serde = "1" -serde_json = { version = "1", features = [] } +serde.workspace = true +serde_json.workspace = true rcgen.workspace = true -ring = { workspace = true, optional = true } -aws-lc-rs = { workspace = true, optional = true } -sha2 = "0.10" -rustls = { version = "0.23.35", default-features = false, features = ["std"] } -url = { version = "2", features = [] } -hex = { version = "0.4", features = [] } -pem = { version = "3", optional = true } -unicase = "2.8" +rustls.workspace = true +url.workspace = true +hex.workspace = true +pem.workspace = true +x509-parser.workspace = true +unicase.workspace = true rand.workspace = true [dev-dependencies] sansio.workspace = true shared.workspace = true ice.workspace = true -webrtc = "0.14.0" -signal = { version = "0.20.0-rc.4", path = "examples/signal", package = "rtc-signal" } +signal.workspace = true tokio.workspace = true env_logger.workspace = true -anyhow = "1" +anyhow.workspace = true clap.workspace = true -hyper = { version = "0.14.32", features = ["full"] } -tokio-util = { version = "0.7.17", features = ["codec"] } +hyper.workspace = true +tokio-util.workspace = true chrono.workspace = true log.workspace = true -serde.workspace = true -serde_json = "1.0.145" -bytes.workspace = true -rand.workspace = true ctrlc.workspace = true -tokio-tungstenite = "0.28.0" -futures-util = "0.3" +tokio-tungstenite.workspace = true +futures-util.workspace = true + +# integration test with old webrtc crate +webrtc = "0.14.0" [[example]] name = "broadcast" @@ -296,6 +338,11 @@ name = "rtcp-processing" path = "examples/rtcp-processing/rtcp-processing.rs" bench = false +[[example]] +name = "rtcp-processing-boxed" +path = "examples/rtcp-processing-boxed/rtcp-processing-boxed.rs" +bench = false + [[example]] name = "data-channels-simple" path = "examples/data-channels-simple/data-channels-simple.rs" diff --git a/README.md b/README.md index f3e1f1ea..166f2176 100644 --- a/README.md +++ b/README.md @@ -276,6 +276,7 @@ RTC is built from composable crates, each implementing a specific protocol: SCTP
DTLS + Crypto
mDNS STUN diff --git a/docs/benchmarking-crypto-migration.md b/docs/benchmarking-crypto-migration.md new file mode 100644 index 00000000..c6537767 --- /dev/null +++ b/docs/benchmarking-crypto-migration.md @@ -0,0 +1,267 @@ +# Benchmarking against a pre-migration baseline + +How the before/after figures in `rtc-srtp/benches/README.md`, `rtc-dtls/benches/README.md`, and +`rtc-stun/benches/README.md` were produced, and how to reproduce or extend them. + +The G3 crypto migration moved every cryptographic operation behind `rtc-crypto`. That is a +performance-sensitive change on paths that run per packet, so each affected crate was measured +against the last commit before *its own* migration. Three real regressions were found this way and +none of them were visible from code review. + +## Contents + +- [The rule that makes or breaks this](#the-rule-that-makes-or-breaks-this) +- [Baseline commits](#baseline-commits) +- [Procedure A — a benchmark already exists at the baseline](#procedure-a--a-benchmark-already-exists-at-the-baseline) +- [Procedure B — no benchmark exists at the baseline](#procedure-b--no-benchmark-exists-at-the-baseline) +- [Procedure C — micro-benchmarks for diagnosis](#procedure-c--micro-benchmarks-for-diagnosis) +- [Interpreting the results](#interpreting-the-results) +- [Pitfalls hit during G3](#pitfalls-hit-during-g3) +- [Reporting](#reporting) + +## The rule that makes or breaks this + +**Both sides must be measured on the same machine, in the same session, with the same criterion +settings.** Everything else in this document is detail. + +This is not a formality. `rtc-srtp/benches/README.md` previously carried results from a MacBook +Air M3 showing `Encrypt/RTP` at 5.69 µs. The post-migration figure on an M1 Max was 4.95 µs. +Comparing those two numbers says performance *improved*. Measuring the actual baseline on the same +M1 Max gave 1.73 µs — a 2.9x regression. The stale cross-machine figure did not merely add noise, +it inverted the conclusion. + +Delete or clearly quarantine historical numbers from other machines before comparing. + +## Baseline commits + +Use the last commit **before the crate in question was migrated**, not a single global baseline — +each crate moved in a different phase, and an earlier commit drags in unrelated changes. + +| Crate | Migrated in | Baseline commit | Baseline is | +|---|---|---|---| +| `rtc-stun` | P2 `f8298ce` | `b8bb313` | P1 — rtc-crypto exists, STUN not yet using it | +| `rtc-srtp` | P4 `fd81f68` | `425494c` | P3 | +| `rtc-dtls` | P5 `219788a` | `fd81f68` | P4 | + +Verify a baseline before trusting it: + +```bash +# The crate must not yet depend on rtc-crypto at the baseline. +for c in 425494c HEAD; do + echo "${c}: $(git show "${c}:rtc-srtp/Cargo.toml" | grep -cE '^crypto[ .=]')" +done +# 425494c: 0 <- baseline, not yet migrated +# HEAD: 1 +``` + +Match the dependency line, not the word. A bare `grep -c crypto` returns 1 even at the baseline, +because `rtc-shared` is pulled in with `features = ["crypto", …]` — that is `rtc-shared`'s own +feature gate, not the `rtc-crypto` crate. Anchoring to `^crypto[ .=]` matches only the dependency +entry. + +In zsh, write `"${c}:path"` and not `"$c:path"`: `$c:r` is parsed as a history modifier and +silently mangles the revision. + +## Procedure A — a benchmark already exists at the baseline + +This is the strong case: the same benchmark source runs on both sides, so the only variable is the +code under test. Used for **SRTP** and **STUN**. + +**1. Confirm the benchmark exists and is unchanged.** + +```bash +git cat-file -e 425494c:rtc-srtp/benches/bench.rs && echo present + +# Byte-compare the region you intend to compare. For SRTP the first 151 lines — the four +# original Encrypt/Decrypt RTP/RTCP benchmarks and their key constants — are identical between +# the baseline and today; everything added later sits after them. +git show 425494c:rtc-srtp/benches/bench.rs > /tmp/base_bench.rs +diff <(sed -n '1,151p' /tmp/base_bench.rs) <(sed -n '1,151p' rtc-srtp/benches/bench.rs) +``` + +If that diff is empty, the comparison is sound. If the benchmark changed for reasons unrelated to +the migration, compare only the benchmarks that did not, or fall back to Procedure B. + +Where the benchmark *had* to change because the API under test changed — STUN's +`new_short_term_integrity` becoming `new_short_term_integrity_with_provider` — that is +unavoidable and is exactly the migration being measured. Keep every other input identical. + +**2. Create a worktree and run it.** + +```bash +git worktree add /tmp/rtc-srtp-base 425494c +cd /tmp/rtc-srtp-base +cargo bench -p rtc-srtp --bench bench -- --warm-up-time 2 --measurement-time 5 +``` + +A worktree gets its own `target/`, so the two builds do not share artifacts. Do not set a shared +`CARGO_TARGET_DIR`. + +**3. Run the current tree with identical arguments.** + +```bash +cd /path/to/rtc +cargo bench -p rtc-srtp --bench bench -- --warm-up-time 2 --measurement-time 5 +``` + +**4. Repeat the baseline and quote a representative run, not the fastest.** Three independent runs +of the SRTP baseline gave 1.725, 1.766, and 1.780 µs for `Encrypt/RTP` — a spread of about ±3%, +which is the noise floor on this machine. Reporting the minimum from one side and a typical value +from the other manufactures a difference of that size out of nothing. Take the median, or quote +the range, and treat anything inside ±3% here as parity. + +**5. Remove the worktree.** + +```bash +cd /path/to/rtc && git worktree remove /tmp/rtc-srtp-base --force +``` + +## Procedure B — no benchmark exists at the baseline + +The weaker case: you write the baseline harness yourself, so a mistake in the port silently +corrupts the comparison. Used for **DTLS**. Say so explicitly when reporting. + +**1. Write the benchmark for the current tree first**, and get it passing. That file is the +specification the baseline port must match. + +**2. Port it back to the pre-migration API.** For DTLS the deltas were: + +| | Baseline (`fd81f68`) | Current | +|---|---|---| +| `CryptoGcm::new` | `(local_key, local_iv, remote_key, remote_iv) -> Self` | `(provider, …) -> Result` | +| `CryptoCbc::new` | `(local_key, local_mac, remote_key, remote_mac) -> Result` | `(provider, …) -> Result` | +| `encrypt` / `decrypt` | `&self` | `&mut self` | + +So the port drops the provider argument, drops `?`/`.unwrap()` where the old constructor returned +`Self`, and drops `mut` on the cipher bindings. Nothing else may change. + +**3. Hold every input identical** — payload length, key and IV constants, record construction, +criterion settings. Copy them literally rather than retyping. In the DTLS bench the header must be +marshalled rather than zero-filled, because `decrypt` re-parses it from the ciphertext buffer; +getting that wrong produces `ErrUnsupportedProtocolVersion` rather than a wrong number, which is +the good failure mode. + +**4. Add the harness plumbing to the worktree**, since it will not be there: + +```toml +# rtc-dtls/Cargo.toml in the worktree +[dev-dependencies] +criterion.workspace = true + +[[bench]] +name = "record_protection" +harness = false +``` + +**5. Sanity-check the port before trusting it.** A ported harness that produces plausible-looking +numbers can still be measuring the wrong thing. Two cheap checks: + +- Does an unrelated benchmark in the same file match between the two sides? If the port is sound, + operations the migration did not touch should be within noise. +- Does the result *predict* something? The DTLS port showed encryption regressing while decryption + did not. That asymmetry pointed at randomness, an independent micro-benchmark (Procedure C) + confirmed the RNG cost, and removing the RNG moved the figure to 270.1 ns against a predicted + 270.5 ns. A broken harness is unlikely to survive that. + +## Procedure C — micro-benchmarks for diagnosis + +Once a regression is confirmed, a throwaway crate isolates the cause far faster than profiling the +whole path. These are **diagnostic only** — never quote them as headline results, and never commit +them. + +```bash +mkdir -p /tmp/rngbench/src && cd /tmp/rngbench +cat > Cargo.toml <<'EOF' +[package] +name = "rngbench" +version = "0.0.0" +edition = "2021" +[dependencies] +ring = "0.17.14" +rand = "0.10.1" +EOF +cat > src/main.rs <<'EOF' +use ring::rand::{SecureRandom, SystemRandom}; +use std::time::Instant; + +fn main() { + const N: u32 = 200_000; + let mut buf = [0u8; 8]; + + let t = Instant::now(); + for _ in 0..N { SystemRandom::new().fill(&mut buf).unwrap(); } + println!("ring SystemRandom : {:>7.1} ns", t.elapsed().as_nanos() as f64 / N as f64); + + let t = Instant::now(); + for _ in 0..N { rand::fill(&mut buf); } + println!("rand thread-local : {:>7.1} ns", t.elapsed().as_nanos() as f64 / N as f64); +} +EOF +cargo run --release -q +``` + +Rules that keep these honest: build with `--release`, use `std::hint::black_box` around anything +the optimiser could elide, run enough iterations that per-call cost dwarfs timer overhead, and +compare candidates **in the same binary** so the environment is shared. + +This is how the two dominant causes were found — an 8-byte OS read at 829 ns versus 8.3 ns, and +`ring`'s HMAC-SHA1 at 4469 ns versus RustCrypto's 1373 ns over the same 1212 bytes. + +## Interpreting the results + +**Separate setup from per-operation cost.** Benchmark them as different groups. The migration +deliberately moves work from the per-packet path to context construction — keyed cipher and MAC +objects are built once. A `Setup/*` figure rising while `Encrypt/*` falls is the design working, +not a regression. Reporting only a combined number hides both directions. + +**Measure every enabled provider under identical inputs.** The benchmarks loop over the built-in +providers, so `--features crypto-ring,crypto-aws-lc-rs` reports both side by side. This is what showed that +`aws-lc-rs` was *worse* on DTLS encrypt while faster everywhere else, which localised the cause to +the encrypt-only RNG call. + +**Asymmetries are the most useful signal.** Encrypt regressed and decrypt did not; large payloads +improved and small ones did not. Each asymmetry excludes whole classes of explanation before any +profiling. + +**A backend difference is not an architecture problem.** `ring`'s slow SHA-1 was a property of the +backend, not of the provider abstraction. It was fixed by composing RustCrypto's HMAC-SHA1 into +the `ring` provider — the built-ins are already composite — rather than by changing the default +provider, which would have imposed a C toolchain on every downstream build. + +## Pitfalls hit during G3 + +**Cross-machine comparison inverted a conclusion.** Covered above. The single most dangerous item +here. + +**Sequential runs of the same command are cached.** Running `cargo clippy` twice and grepping each +output separately gives real diagnostics from the first and an empty second. Capture one run to a +file and derive every count from that file. + +**zsh eats `:r` in a revision string.** `git show "$c:rtc-srtp/Cargo.toml"` parses `$c:r` as a +history modifier and looks up `425494ctc-srtp/Cargo.toml`. Use `"${c}:path"`. + +**zsh does not word-split unquoted variables.** `F="--no-default-features --features crypto-ring"; cargo build $F` passes one bogus argument. This produced four false "failures" in a feature-matrix loop. +Write the invocations out, or use an array. + +**Bisecting can misattribute.** An early bisect blamed the AES-CTR rewrite for a set of DTLS test +failures. The real cause was a text-slice deletion that had removed a provider method along with +its neighbours; the CTR change was innocent. Confirm a bisect result by reverting *only* the +suspected hunk, not a whole file. + +**Build-only verification is not verification.** After the last code edit before a commit, only +build and clippy were re-run — both clean — while the tests were not. Nine DTLS tests were broken +at that commit. Re-run the full suite after the final edit, not the second-to-last. + +## Reporting + +Each crate's `benches/README.md` carries its own table. State, every time: + +- the machine and OS; +- the baseline commit and what it precedes; +- the exact criterion arguments, identical on both sides; +- whether the baseline benchmark was pre-existing (Procedure A) or reconstructed (Procedure B); +- for reconstructed harnesses, that the comparison rests on the port being faithful. + +Quarantine or delete numbers from other machines. If a table mixes measurements taken at different +points — for example a "before fix / after fix" column pair captured as work progressed rather +than in one sweep — label it, or re-run everything at HEAD in a single session. diff --git a/docs/crypto-provider-baseline.md b/docs/crypto-provider-baseline.md new file mode 100644 index 00000000..f64d5088 --- /dev/null +++ b/docs/crypto-provider-baseline.md @@ -0,0 +1,120 @@ +# Crypto provider baseline + +This document records the crypto behavior and compatibility surface before the `rtc-crypto` provider migration. It is the baseline for G3 and must be updated when a migration intentionally changes an algorithm, preference, public adapter, or test expectation. + +## DTLS + +### Cipher suites + +All current suites use SHA-256 for the TLS 1.2 PRF and Finished calculation. CBC suites additionally use HMAC-SHA1 for record authentication. + +| Preference | Cipher suite | Code point | Record protection | Authentication | +|---:|---|---:|---|---| +| 1 | `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` | `0xc02b` | AES-128-GCM | ECDSA certificate | +| 2 | `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA` | `0xc00a` | AES-256-CBC plus HMAC-SHA1 | ECDSA certificate | +| 3 | `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` | `0xc02f` | AES-128-GCM | RSA certificate | +| 4 | `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA` | `0xc014` | AES-256-CBC plus HMAC-SHA1 | RSA certificate | +| 5 | `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` | `0xcca9` | ChaCha20-Poly1305 | ECDSA certificate | +| Explicit only | `TLS_ECDHE_ECDSA_WITH_AES_128_CCM` | `0xc0ac` | AES-128-CCM, 16-byte tag | ECDSA certificate | +| Explicit only | `TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8` | `0xc0ae` | AES-128-CCM, 8-byte tag | ECDSA certificate | +| Explicit only | `TLS_PSK_WITH_AES_128_CCM` | `0xc0a4` | AES-128-CCM, 16-byte tag | PSK | +| Explicit only | `TLS_PSK_WITH_AES_128_CCM_8` | `0xc0a8` | AES-128-CCM, 8-byte tag | PSK | +| Explicit only | `TLS_PSK_WITH_AES_128_GCM_SHA256` | `0x00a8` | AES-128-GCM | PSK | +| Explicit only | `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` | `0xcca8` | ChaCha20-Poly1305 | RSA certificate | + +The first five rows are the order returned by `default_cipher_suites()`. G3 must preserve this order unless a separate compatibility change explicitly changes it. + +### Named groups + +| Group | Code point | Current use | +|---|---:|---| +| P-256 | `0x0017` | Advertised first | +| P-384 | `0x0018` | Advertised third | +| X25519 | `0x001d` | Advertised second and defined as `DEFAULT_NAMED_CURVE` | + +The provider API therefore needs P-256, P-384, and X25519 key agreement. The current concrete `NamedCurvePrivateKey` remains crate-private. + +### Signatures and key encodings + +The default advertised signature/hash list is ECDSA with SHA-256, SHA-384, and SHA-512; RSA PKCS#1 with SHA-256, SHA-384, and SHA-512; and Ed25519. Current signing supports Ed25519, ECDSA P-256/SHA-256, RSA PKCS#1/SHA-256, and `CustomSigner`. Current verification additionally accepts ECDSA P-384/SHA-384 and RSA PKCS#1/SHA-1, SHA-384, and SHA-512. + +The `SignatureScheme` enum names RSA-PSS and SHA-1 combinations beyond that set, but they are not complete negotiated implementations. They are not part of the initial provider contract. + +| Signature operation | Current verifier input | +|---|---| +| Ed25519 | Raw 32-byte public key | +| ECDSA P-256/SHA-256 | Uncompressed SEC1 point and ASN.1 DER signature | +| ECDSA P-384/SHA-384 | Uncompressed SEC1 point and ASN.1 DER signature | +| RSA PKCS#1/SHA-1, SHA-256, SHA-384, or SHA-512 | PKCS#1 DER `RSAPublicKey` and PKCS#1 v1.5 signature | + +Certificate parsing currently extracts the subject-public-key bit string from X.509. The provider-neutral certificate boundary will instead use complete SPKI DER, with built-in adapters converting it to the operation-specific encodings above. + +## SRTP + +| Protection profile | Master key | Master salt | RTP auth tag | RTCP auth tag | AEAD tag | Auth key | +|---|---:|---:|---:|---:|---:|---:| +| `SRTP_AES128_CM_HMAC_SHA1_80` | 16 | 14 | 10 | 10 | 0 | 20 | +| `SRTP_AES128_CM_HMAC_SHA1_32` | 16 | 14 | 4 | 10 | 0 | 20 | +| `SRTP_AES256_CM_HMAC_SHA1_80` | 32 | 14 | 10 | 10 | 0 | 20 | +| `SRTP_AES256_CM_HMAC_SHA1_32` | 32 | 14 | 4 | 10 | 0 | 20 | +| `SRTP_AEAD_AES_128_GCM` | 16 | 12 | 0 | 0 | 16 | 0 | +| `SRTP_AEAD_AES_256_GCM` | 32 | 12 | 0 | 0 | 16 | 0 | + +Lengths are bytes. DTLS-SRTP negotiation currently exposes AES-128-CM HMAC-SHA1 80/32 and AEAD AES-128/256-GCM; the two AES-256-CM profiles are implemented by `rtc-srtp` but are not currently represented in the DTLS `use_srtp` extension. + +## STUN + +| Operation | Current call site and behavior | +|---|---| +| MD5 | `rtc-stun/src/integrity.rs` derives a long-term credential key from `username:realm:password`. | +| HMAC-SHA1 | `rtc-stun/src/integrity.rs` calculates and verifies `MESSAGE-INTEGRITY`. | +| Constant-time equality | `rtc-stun/src/checks.rs` compares complete integrity values with `subtle::ConstantTimeEq`. | +| Transaction randomness | `TransactionId::new()` and `Message::new_transaction_id()` use `rand::rng().fill` for the 96-bit ID. | + +`ATTR_MESSAGE_INTEGRITY_SHA256` is defined, but the current implementation only implements HMAC-SHA1 `MESSAGE-INTEGRITY`. Adding STUN SHA-256 integrity is not implicit G3 work. `MessageIntegrity` publicly wraps a `Vec`, and its current `Display` implementation exposes the key; migration must replace this with a redacted secret type without changing wire behavior. + +## Public backend-bound compatibility surface + +| Surface | Current dependency | G3 disposition | +|---|---|---| +| `rtc_dtls::crypto::CryptoPrivateKeyKind` | Ring-compatible Ed25519/ECDSA key pairs, RSA key pair, or `CustomSigner` | Compatibility adapter during migration; remove before 1.0. | +| `rtc_dtls::crypto::CryptoPrivateKey` and `Certificate` | Concrete key variants and X.509 parsing | Replace internals with provider-neutral signing keys and SPKI DER. | +| `rtc_dtls::crypto::CustomSigner` | DTLS-specific signing extension | Adapt temporarily to `SigningKey`; remove before 1.0. | +| DTLS `Config` certificate verification | rustls `RootCertStore`, verifier traits, and certificate types | Keep behind an explicit certificate-verification adapter; do not put X.509 policy into `RTCCrypto`. | +| Top-level `RTCCertificate::from_key_pair` | `rcgen::KeyPair` | Keep as a deprecated migration adapter until provider-neutral import/generation lands; remove before 1.0. | +| `rtc_shared::crypto::KeyingMaterialExporter` | DTLS-to-SRTP trait coupling | Replace with byte-oriented keying material and remove. | +| `ring` and `aws-lc-rs` feature guards and aliases | DTLS, SRTP, STUN, ICE, TURN, and top-level RTC | Replace with additive provider selection in P1-09 and remove per-crate direct dependencies after migration. | +| `openssl` and `vendored-openssl` | Partial SRTP AES-CTR implementation only | Deprecate and remove before 1.0; no partial provider will remain. | +| Shared backend error variants | Ring, AWS-LC-RS, and OpenSSL errors in `rtc-shared` | Map provider failures at protocol boundaries, then remove backend-specific variants. | + +## Initial provider operation-to-caller map + +Only operations with a current caller belong in the initial trait. Algorithm identifiers used inside a signature or HMAC operation do not require an equivalent standalone hash operation. + +| Provider operation | Initial algorithms | Current callers | +|---|---|---| +| Hash | MD5, SHA-256 | STUN long-term credentials; RTC fingerprints and DTLS transcript/PRF composition. | +| HMAC | SHA-1, SHA-256 | STUN and SRTP authentication and DTLS CBC record MAC; DTLS TLS 1.2 PRF. | +| Constant-time equality | Byte slices | STUN, SRTP, and DTLS authentication checks. | +| Random bytes | CSPRNG bytes | DTLS protocol randoms and key generation; optional STUN transaction-ID generation where provider propagation is practical. | +| AES block encryption | AES-128, AES-256 | SRTP key derivation. | +| Stream cipher | AES-128-CTR, AES-256-CTR | SRTP AES-CM profiles. | +| AEAD | AES-128-GCM, AES-256-GCM, AES-128-CCM, AES-128-CCM-8, ChaCha20-Poly1305 | DTLS record protection and SRTP AEAD profiles. | +| CBC | AES-256-CBC | DTLS CBC record protection. | +| Key agreement | P-256, P-384, X25519 | DTLS ECDHE. | +| Signature verification | Ed25519; ECDSA P-256/SHA-256 and P-384/SHA-384; RSA PKCS#1/SHA-1, SHA-256, SHA-384, SHA-512 | DTLS certificate authentication. | +| Signing | Ed25519, ECDSA P-256/SHA-256, RSA PKCS#1/SHA-256 | DTLS CertificateVerify. | +| Signing-key generation | Ed25519, ECDSA P-256 | Current certificate generation paths. | +| Signing-key import | Ed25519, ECDSA P-256, RSA | Current PKCS#8/key-pair import paths. | + +Standalone SHA-1, SHA-384, and SHA-512 hashing has no current caller and is excluded from the initial `HashAlgorithm`. RSA-PSS is also excluded until an independently tested protocol requirement exists. + +## Existing and missing validation + +| Area | Existing baseline | Explicit follow-up gap | +|---|---|---| +| DTLS | Cipher-suite unit tests, handshake tests, key-exchange/signature tests, and repository interoperability coverage | Provider-level vectors for every primitive and cross-provider DTLS handshakes belong to P1-08 and P6-06. | +| SRTP AES-CM | RFC 3711 Appendix B.3 AES-128 derivation vector and RFC 6188 section 7.2 AES-256 derivation vector; RTP/RTCP round trips, replay, rollover, and bad-auth tests | Cross-provider packets for every profile belong to P1-08/P4; retain exact ciphertext and tag fixtures during migration. | +| SRTP AEAD | AES-128-GCM and AES-256-GCM tests, including RFC 7714-derived layouts and RTP/RTCP round trips | Consolidated published known-answer fixtures for every AEAD packet shape belong to P1-08/P4. | +| STUN | Long-term MD5 key expectations, HMAC behavior, tamper rejection, fingerprint ordering, and message round trips | Add RFC 5769 full-message vectors and cross-provider equality in P1-08/P2. | +| Browser/interoperability | Existing repository integration workflows | Add a provider-by-provider DTLS-SRTP browser/interoperability matrix in P7-03/P8-02. | diff --git a/docs/crypto-provider-decisions.md b/docs/crypto-provider-decisions.md new file mode 100644 index 00000000..dc77164c --- /dev/null +++ b/docs/crypto-provider-decisions.md @@ -0,0 +1,44 @@ +# Crypto provider migration decisions + +These decisions freeze the boundaries needed to start G3 implementation. Reopening one requires an explicit design change because provider implementations and protocol migrations depend on them. + +## SRTP OpenSSL features + +The `openssl` and `vendored-openssl` features were partial SRTP implementation choices, not complete RTC crypto providers, and were removed when SRTP migrated to `rtc-crypto`. A future OpenSSL provider is possible only as a complete downstream implementation of the public provider traits that passes the same conformance suite as the built-in providers; partial protocol-specific backend features will not return. + +The removal is recorded in the changelog and the crypto-provider migration guide. This pre-1.0 cleanup is intentional: retaining the feature names would falsely imply that OpenSSL provided the complete algorithm surface now required by SRTP and the rest of the RTC stack. + +## Crypto errors and protocol boundaries + +`rtc-crypto` owns a backend-neutral, non-exhaustive `CryptoError`. The initial variants are `NoDefaultProvider`, `UnsupportedAlgorithm(CryptoAlgorithm)`, `InvalidKeyLength { expected, actual }`, `InvalidNonceLength { expected, actual }`, `InvalidTagLength { expected, actual }`, `InvalidPublicKey`, `InvalidPrivateKey`, `AuthenticationFailed`, `InvalidSignature`, `RandomnessFailed`, `OutputTooSmall { required, actual }`, and `Provider(String)`. Authentication, tag, and padding failures from decryption converge on `AuthenticationFailed`; signature verification uses `InvalidSignature`. These failures must not expose backend-specific distinctions that could become an oracle. + +Protocol crates convert `CryptoError` at their own boundary. `rtc-shared` must not depend on `rtc-crypto`, and G3 will not introduce that dependency merely to obtain an automatic `From` conversion. During migration, each call site maps a provider failure to the protocol's existing semantic error where one exists; otherwise it maps to a backend-neutral shared crypto error without retaining the backend error type. This mapping is explicit rather than a blanket conversion. + +The string in `Provider(String)` is sanitized local diagnostic context, is not a stable matching surface, and may be logged at trace level. It must not be serialized into packets, alerts, or peer-visible protocol messages. All other variants use stable provider-neutral wording. Secret material, keys, plaintext, nonces, tags, and complete signatures are never included in `Debug`, `Display`, or error sources. + +The existing shared public error type remains available during G3 to avoid mixing a workspace-wide error redesign into provider migration. Backend-specific shared variants are removed in P7-01. Moving protocols to crate-local error types can be considered separately and is not required to finish G3. + +## Public keys and certificate adapters + +The initial non-exhaustive `PublicKeyEncoding` distinguishes these encodings: + +| Variant | Encoding | +|---|---| +| `SubjectPublicKeyInfoDer` | Complete DER-encoded SubjectPublicKeyInfo | +| `EcUncompressedPoint` | SEC1 uncompressed P-256 or P-384 point; the signature scheme determines the curve | +| `Ed25519Raw` | Raw 32-byte Ed25519 public key | +| `RsaPkcs1Der` | PKCS#1 DER `RSAPublicKey` | + +The canonical certificate-facing public-key boundary is complete DER-encoded SubjectPublicKeyInfo. Certificate parsing and policy remain outside `RTCCrypto`; adapters parse SPKI and construct the operation-specific `PublicKey` passed to signature verification. This preserves an unambiguous public boundary while matching the encodings accepted by current providers. + +A `SigningKey` may be non-exportable. Public-key access and signing are mandatory; `to_pkcs8_der()` returns `Ok(None)` for a non-exportable key. PEM/private-key serialization built on that operation returns an explicit adapter error rather than fabricating bytes. Key equality or certificate identity must use public material or a stable provider-neutral identifier, never private-key export. + +The current `rcgen::KeyPair`, `CryptoPrivateKeyKind`, and `CustomSigner` APIs remain only as migration adapters. Provider-neutral key generation/import and signing land first; the old adapters are then deprecated and removed before 1.0 in P7-02. Internal DTLS named-curve private-key types are replaced by provider-owned active key-exchange objects and do not become public compatibility APIs. + +rustls/webpki certificate-chain verification remains an explicitly separate adapter and policy layer. `RTCCrypto` performs cryptographic primitives and signing-key operations; it does not own trust stores, certificate path building, hostname validation, revocation policy, or application identity policy. + +## Feature and provider selection invariants + +`ring` remains the default built-in provider. `ring` and `aws-lc-rs` become additive in P1-09: enabling both compiles both and does not silently select AWS-LC-RS. Construction resolves the chosen provider once and protocol objects store an `Arc`; packet paths do not read a mutable global or repeatedly resolve defaults. + +The protocol crates still intentionally reject both built-ins together and cannot yet build the full RTC stack without one while their direct backend calls remain. P1-09 makes these configurations pass for `rtc-crypto`; P2 through P6 migrate the consumers, and P7 adds the corresponding full-workspace matrix. This sequencing keeps the new provider contract independently testable without claiming that unmigrated protocol crates are already additive. diff --git a/docs/crypto-provider-migration.md b/docs/crypto-provider-migration.md new file mode 100644 index 00000000..3768ea35 --- /dev/null +++ b/docs/crypto-provider-migration.md @@ -0,0 +1,425 @@ +# Crypto provider migration + +`rtc` 0.21 routes every cryptographic operation through `rtc-crypto`. Protocol crates no longer +depend on `ring`, `aws-lc-rs`, or RustCrypto primitive crates; they take an +`Arc` and call provider traits. + +This guide covers what changed for callers. For *why*, see `docs/crypto-provider-decisions.md`. + +## Contents + +- [Choosing a provider](#choosing-a-provider) +- [Cargo features are now additive](#cargo-features-are-now-additive) +- [Removed and changed public items](#removed-and-changed-public-items) +- [Top-level `rtc`](#top-level-rtc) +- [Certificates](#certificates) +- [Standalone DTLS](#standalone-dtls) +- [Standalone SRTP](#standalone-srtp) +- [Standalone STUN, ICE, and TURN](#standalone-stun-ice-and-turn) +- [Writing your own provider](#writing-your-own-provider) +- [OpenSSL](#openssl) + +## Choosing a provider + +```rust +use std::sync::Arc; +use rtc_crypto::{RTCCryptoProvider, default_provider}; +use rtc_crypto::providers::{AwsLcRsProvider, RingProvider}; + +// The feature-selected default: ring when enabled, otherwise aws-lc-rs. +// Returns Err(CryptoError::NoDefaultProvider) when neither feature is on. +let provider: Arc = default_provider()?; + +// Or name one explicitly. Both can be compiled in at once. +let ring: Arc = Arc::new(RingProvider::new()); +let aws: Arc = Arc::new(AwsLcRsProvider::new()); +``` + +There is no process-global provider and nothing to install at startup. A provider is an ordinary +value passed through configuration, so two peer connections in one process can use different +providers. + +## The provider is always passed in + +**No library code resolves a default provider.** `crypto::default_provider()` is called in exactly +one place in the workspace — peer-connection construction, where the application either supplied +one via `SettingEngine::set_crypto_provider` or gets the feature-selected built-in. Everything +below that receives an `Arc` from its caller. + +This is why the `*_with_provider` constructor pairs are gone. Each had a default-resolving sibling +that hid the choice — and, in a `--no-default-features` build, panicked or failed deep inside a +call rather than at configuration time. There is now one constructor per type and it takes the +provider: + +| Before | Now | +|---|---| +| `Context::new(key, salt, profile, a, b)` | `Context::new(key, salt, profile, a, b, provider)` | +| `Context::new_with_provider(…)` | (folded into `new`) | +| `Client::new(config)` | `Client::new(config, provider)` | +| `Agent::new(config)` | `Agent::new(config, provider)` | +| `Certificate::generate_self_signed(names)` | `Certificate::generate_self_signed(names, provider)` | +| `Certificate::from_pem(pem)` | `Certificate::from_pem(pem, provider)` | +| `RTCCertificate::from_pem(pem)` | `RTCCertificate::from_pem(pem, provider)` | +| `RTCCertificate::get_fingerprints()` | `get_fingerprints(provider) -> Result<…>` | +| `MessageIntegrity::new_short_term_integrity(pw)` | `…(pw, provider)` | + +`Default` impls that resolved a provider are removed rather than reworked, because `Default` has +nowhere to accept one. `HandshakeConfig`, `rtc_dtls::State`, `Agent`, and `RTCDtlsTransport` are +now built through constructors that take the provider. `ConfigBuilder::build` returns an error +instead of inventing a provider when none was configured. + +Each protocol crate re-exports the crypto API — `rtc_srtp::crypto`, `rtc_stun::crypto`, +`rtc_ice::crypto`, `rtc_turn::crypto`, and `rtc_dtls::crypto_provider` (named differently because +`rtc-dtls` already has its own `crypto` module) — so a standalone user can name +`Arc` without adding and version-matching a direct `rtc-crypto` dependency. + +Tests, examples, and benchmarks are the outside caller and may call `default_provider()` directly. + +## Cargo features are now additive + +Before 0.21, `rtc`, `rtc-dtls`, `rtc-srtp`, and `rtc-stun` each carried: + +```rust +#[cfg(all(feature = "aws-lc-rs", feature = "ring"))] +compile_error!("At most one of the features \"aws-lc-rs\" and \"ring\" can be enabled."); +``` + +Enabling both is now supported and tested in CI. This matters because Cargo unifies features: a +transitive dependency enabling the other backend used to break an otherwise valid build, with no +fix available to the person hitting it. + +| Build | Result | +|---|---| +| `--features crypto-ring` | ring only | +| `--features crypto-aws-lc-rs` | aws-lc-rs only | +| `--features crypto-ring,crypto-aws-lc-rs` | both compiled; `default_provider()` returns ring | +| `--no-default-features` | no built-in provider; supply your own | + +`default_provider()` prefers ring when both are enabled, matching the previous precedence. +Enabling `crypto-aws-lc-rs` alongside the default never silently switches the default. + +## Removed and changed public items + +| Item | Status | Replacement | +|---|---|---| +| `rtc_shared::crypto::KeyingMaterialExporter` | removed | inherent `rtc_dtls::State::export_keying_material` | +| `rtc_srtp::Config::extract_session_keys_from_dtls` | removed | `set_session_keys_from_keying_material` | +| `rtc_stun::MessageIntegrity` (tuple struct) | changed | named fields; `key` is a `SecretVec` | +| `MessageIntegrity::new_short_term_integrity` | changed | `new_short_term_integrity(password, provider)` | +| `MessageIntegrity::new_long_term_integrity` | changed | `new_long_term_integrity(user, realm, pass, provider)` | +| `MessageIntegrity::new_raw_integrity` | changed | `new_raw_integrity(key, provider)` | +| `MessageIntegrity::default()` | removed | construct with an explicit key and provider | +| `rtc_dtls::crypto::CustomSigner` | removed | implement `rtc_crypto::SigningKey` | +| `CryptoPrivateKey::from_custom_signer` | removed | `CryptoPrivateKey::from_signing_key` | +| `CryptoPrivateKey::from_key_pair(&KeyPair)` | changed | now takes a provider: `from_key_pair(&KeyPair, provider)` | +| `CryptoPrivateKey::serialized_der` field | removed | `SigningKey::to_pkcs8_der()` | +| `rtc_dtls::crypto::CryptoPrivateKeyKind` | removed | opaque `Arc` | +| `RTCCertificate::from_key_pair` | removed | `RTCCertificate::generate` | +| `RTCCertificate::from_key_pair_with_provider` | removed | `RTCCertificate::generate_from_signing_key` | +| `rtc_shared::Error::{Sec1, P256, RcGen, AesGcm, Aes}` | removed | `Error::Crypto(String)` | +| `RTCCrypto::hmac` | removed | `new_hmac(alg, key)?.sign(input, out)` | +| `RTCCrypto::verify_hmac` | removed | `new_hmac(alg, key)?.verify(input, tag)` | +| `rtc_srtp` `openssl` / `vendored-openssl` features | removed | see [OpenSSL](#openssl) | + +`MessageIntegrity::default()` produced a credential with an empty key, which was never useful. No +in-tree caller existed; downstream code that relied on it must supply a real key. + +### Keyed MACs + +`RTCCrypto::hmac` and `verify_hmac` are gone. They were exactly `new_hmac(..)?.sign(..)` and +`new_hmac(..)?.verify(..)`, and keeping them preserved a path that derives the HMAC key schedule +on every call — which cost SRTP roughly 40% of its per-RTCP-packet time. There is now one way to +compute an HMAC, and it makes the keying cost visible at the call site. + +Before: + +```rust +let mut tag = [0u8; 20]; +crypto.hmac(HmacAlgorithm::Sha1, &key, &[header, payload], &mut tag)?; +crypto.verify_hmac(HmacAlgorithm::Sha1, &key, &[header, payload], &tag)?; +``` + +After — one-shot: + +```rust +let mut tag = [0u8; 20]; +let mut mac = crypto.new_hmac(HmacAlgorithm::Sha1, &key)?; +mac.sign(&[header, payload], &mut tag)?; +mac.verify(&[header, payload], &tag)?; +``` + +After — repeated authentication with one key, which is the point: hold the `Mac` in your state and +key it once. + +```rust +struct MyContext { + auth: Box, // keyed once at construction +} + +impl MyContext { + fn new(crypto: &dyn RTCCrypto, key: &[u8]) -> Result { + Ok(Self { auth: crypto.new_hmac(HmacAlgorithm::Sha1, key)? }) + } + + // `&mut self`, because Mac methods take &mut self like the cipher traits. + fn tag(&mut self, message: &[&[u8]]) -> Result<[u8; 20], CryptoError> { + let mut tag = [0u8; 20]; + self.auth.sign(message, &mut tag)?; + Ok(tag) + } +} +``` + +`Mac` is `Send` and its methods take `&mut self`, matching `StreamCipher`, `AeadCipher`, and +`CbcCipher`. A caller whose own signature is fixed to `&self` can still build a local `Mac` per +message and use it mutably. + +## Top-level `rtc` + +`SettingEngine` carries the provider. It is resolved once during peer-connection construction and +cloned into DTLS, SRTP, STUN, certificate, and fingerprint state. + +```rust +use std::sync::Arc; +use rtc::peer_connection::configuration::setting_engine::SettingEngine; +use rtc_crypto::providers::AwsLcRsProvider; + +let mut setting_engine = SettingEngine::default(); +setting_engine.set_crypto_provider(Arc::new(AwsLcRsProvider::new())); +``` + +Omitting the call keeps the feature-selected default, so existing code needs no change. + +## Certificates + +`rcgen::KeyPair`-based construction is gone. Certificate *formatting* still uses `rcgen` — +X.509 is deliberately not a provider concern — but key generation and signing go through the +provider. `CertificateParams` is re-exported so callers need no direct `rcgen` dependency. + +Before: + +```rust +use rcgen::KeyPair; + +let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; +let certificate = RTCCertificate::from_key_pair(key_pair)?; +``` + +After: + +```rust +use rtc::crypto::{self, SignatureScheme}; +use rtc::peer_connection::certificate::{CertificateParams, RTCCertificate}; + +let certificate = RTCCertificate::generate( + crypto::default_provider()?, + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["localhost".to_owned()])?, +)?; +``` + +Three construction paths are available: + +| Need | Use | +|---|---| +| Provider generates the key | `RTCCertificate::generate(provider, scheme, params)` | +| Key already exists (imported PKCS#8, HSM, KMS) | `RTCCertificate::generate_from_signing_key(params, scheme, signing_key)` | +| Certificate chain already exists | `RTCCertificate::from_pkcs8(provider, scheme, chain, der, expires)` | + +### External and non-exportable signing keys + +`CustomSigner` is replaced by `rtc_crypto::SigningKey`, which is also what the built-in providers +implement. An HSM or KMS key returns `Ok(None)` from `to_pkcs8_der()`; PEM serialization of such a +key returns an explicit error rather than fabricating bytes. + +```rust +use std::sync::Arc; +use rtc_crypto::{CryptoError, PublicKey, PublicKeyEncoding, SignatureScheme, SigningKey}; + +#[derive(Debug)] +struct KmsKey { /* handle, cached SPKI */ } + +impl SigningKey for KmsKey { + fn supports(&self, scheme: SignatureScheme) -> bool { + scheme == SignatureScheme::EcdsaP256Sha256 + } + + fn public_key(&self) -> PublicKey<'_> { + PublicKey { + encoding: PublicKeyEncoding::SubjectPublicKeyInfoDer, + bytes: &[], // cached SPKI DER + } + } + + fn sign(&self, scheme: SignatureScheme, message: &[u8]) -> Result, CryptoError> { + let _ = (scheme, message); + todo!("call the KMS") + } +} + +let key: Arc = Arc::new(KmsKey { /* … */ }); +``` + +Pass it to `RTCCertificate::generate_from_signing_key`, or to +`rtc_dtls::crypto::CryptoPrivateKey::from_signing_key` for standalone DTLS. + +## Standalone DTLS + +```rust +use rtc_dtls::config::ConfigBuilder; + +let config = ConfigBuilder::default() + .with_crypto_provider(provider.clone()) + .with_certificates(vec![certificate]) + .build(true, None)?; +``` + +`Certificate::generate_self_signed`, `generate_self_signed_with_alg`, and `from_pem` all take the +provider as their last argument. The default-resolving overloads are gone, as is +`ConfigBuilder::build`'s silent fallback — it now returns an error if no provider was configured. + +### DTLS-SRTP keying material + +`KeyingMaterialExporter` is gone. Export is an inherent method on the DTLS session, and SRTP takes +the resulting bytes, so `rtc-srtp` and `rtc-dtls` stay independent of each other. + +Before: + +```rust +srtp_config.extract_session_keys_from_dtls(dtls_state, is_client)?; +``` + +After: + +```rust +use rtc_srtp::config::LABEL_EXTRACTOR_DTLS_SRTP; + +let material = dtls_state.export_keying_material( + LABEL_EXTRACTOR_DTLS_SRTP, + &[], + srtp_config.keying_material_len(), +)?; +srtp_config.set_session_keys_from_keying_material(&material, dtls_state.is_client())?; +``` + +## Standalone SRTP + +`Context::new` takes the provider. It validates the protection +profile against `RTCCrypto::supports`, derives session material through the provider, and builds +keyed cipher objects **once per one-way context**. Packet indexes, rollover counters, replay +windows, IVs, AAD, tag truncation, and wire layout stay in `rtc-srtp`. + +```rust +let context = Context::new( + &master_key, + &master_salt, + ProtectionProfile::Aes128CmHmacSha1_80, + None, + None, + provider.clone(), +)?; +``` + +There is no default-resolving overload. `rtc-srtp` never calls `default_provider()`; an +application that wants the feature-selected built-in passes `crypto::default_provider()?`. + +## Standalone STUN, ICE, and TURN + +`MessageIntegrity` now holds its provider, because `Setter::add_to` and `check` have no place to +receive one. The struct changed from a public tuple to named fields, and the default-resolving +constructors were removed. + +Before: + +```rust +let integrity = MessageIntegrity::new_short_term_integrity(password); +``` + +After: + +```rust +let integrity = MessageIntegrity::new_short_term_integrity(password, provider.clone()); + +let integrity = MessageIntegrity::new_long_term_integrity( + username, realm, password, provider.clone(), +)?; // returns Result: MD5 must be available +``` + +ICE and TURN reach crypto only through these values; they hold one provider handle and clone it +into the integrity attributes they build. + +## Writing your own provider + +Implement `RTCCryptoProvider` plus the component traits. Nothing needs to be registered — pass the +value in through configuration. + +```rust +use std::sync::Arc; +use rtc_crypto::{RTCCrypto, RTCCryptoProvider, RTCRandom}; + +#[derive(Debug)] +struct MyProvider { crypto: MyCrypto, random: MyRandom } + +impl RTCCryptoProvider for MyProvider { + fn name(&self) -> &'static str { "my-provider" } + fn crypto(&self) -> &dyn RTCCrypto { &self.crypto } + fn random(&self) -> &dyn RTCRandom { &self.random } +} + +setting_engine.set_crypto_provider(Arc::new(MyProvider { /* … */ })); +``` + +Validate it against the same conformance suite the built-ins use: + +```rust +// Cargo.toml: rtc-crypto = { version = "0.21", features = ["test-support"] } +#[test] +fn my_provider_conforms() { + rtc_crypto::conformance::assert_provider(&MyProvider::new()); +} +``` + +`assert_provider` runs the whole suite. Individual sections are also public +(`assert_hashes_and_hmac`, `assert_aead`, `assert_cbc`, `assert_block_and_stream_ciphers`, +`assert_key_exchange`, `assert_signatures`, `assert_random`) for a provider that implements only +part of the surface. + +The suite covers RFC known-answer vectors, round trips, tag-length and nonce-length validation, +and unsupported-algorithm reporting. `cargo test --package rtc-crypto --no-default-features +--features test-support --test custom_provider` shows a complete downstream-style provider built +with no built-in enabled. + +Report unsupported algorithms honestly through `RTCCrypto::supports` — negotiation intersects +protocol support, provider capability, and application configuration before advertising anything, +so an accurate answer turns an unusable combination into a construction-time error instead of a +stalled handshake. + +## Provider performance + +The built-in providers are **composite**: each uses its primary backend where that backend is +strong and RustCrypto elsewhere. `ring` supplies SHA-256, AEAD, key exchange and signatures; +AES-CTR, CCM, CBC, MD5 and HMAC-SHA1 come from RustCrypto because `ring`'s SHA-1 does not use the +ARMv8 instructions. `aws-lc-rs` keeps its own SHA-1, which is faster than both. + +Randomness for protocol values — DTLS randoms and record IVs, cookies, transaction IDs — comes +from an OS-seeded, periodically reseeded thread-local CSPRNG rather than a per-call read of the +operating system, which measured 100-250x slower and showed up directly on the DTLS record path. +Keypair generation and signing still use the backend's own RNG. + +After these choices, SRTP and DTLS per-packet throughput is at parity with, or better than, +pre-`rtc-crypto` releases on the default provider. `aws-lc-rs` is faster on the SRTP AES-CM/HMAC +and DTLS CBC paths if you can accept the `aws-lc-sys` C toolchain in your build. Measurements and +methodology are in `rtc-srtp/benches/README.md`, `rtc-dtls/benches/README.md`, and +`rtc-stun/benches/README.md`. + +A custom provider is free to make different choices; the conformance suite checks correctness, not +speed. + +## OpenSSL + +The `openssl` and `vendored-openssl` features on `rtc-srtp` were removed. They selected an +alternate AES-CTR path inside SRTP only and never implemented the full provider contract, so +keeping the names would imply a completeness that did not exist. + +An OpenSSL backend can return as a complete `RTCCryptoProvider` — in an application or a separate +crate — that passes the public conformance suite. No `rtc` change is required to enable that. diff --git a/docs/randomness-audit.md b/docs/randomness-audit.md new file mode 100644 index 00000000..1f2352a5 --- /dev/null +++ b/docs/randomness-audit.md @@ -0,0 +1,33 @@ +# ICE and SCTP randomness audit + +## Scope + +`RTCRandom` is the workspace boundary for cryptographically secure randomness when a component naturally owns an `RTCCryptoProvider`. It is not a replacement for every random-looking value in the workspace. Standalone crates may retain a direct CSPRNG when adding provider ownership would distort their public API, while deterministic protocol fields and test fixtures should remain deterministic. + +## ICE + +| Value | Security role | Source and decision | +| --- | --- | --- | +| Local username fragment | Short-term ICE credential | `Agent` generates it through `RTCCryptoProvider::random()`. The standalone compatibility helper retains `rand`'s thread-local CSPRNG. An application-supplied value remains unchanged. | +| Local password | Short-term ICE credential | `Agent` generates it through `RTCCryptoProvider::random()`. The standalone compatibility helper retains `rand`'s thread-local CSPRNG. An application-supplied value remains unchanged. | +| Controlling/controlled tie breaker | Unpredictable role-conflict input | Generated through `RTCCryptoProvider::random()` because `Agent` already owns the provider. | +| STUN transaction ID | Request correlation with an unpredictability requirement | `rtc-stun` retains its documented direct CSPRNG path because a `Message` can be constructed without provider ownership. This is separate from STUN integrity and fingerprint operations, which use the configured provider. | +| Candidate ID | Local uniqueness and diagnostics | Retains the direct CSPRNG helper. It is neither a credential nor a cryptographic identity, and passing a provider solely for this field would add unnecessary coupling. | +| Candidate foundation | Candidate grouping and redundancy elimination | Deterministic CRC32C over candidate properties. It is intentionally not random or secret. | +| mDNS host name | Privacy-preserving local alias | Retains UUID v4 generation through the UUID crate's secure random source. The mDNS subsystem does not naturally own the provider. | +| Explicit configuration and test values | Application policy and deterministic fixtures | Preserved exactly as supplied. Tests may use deterministic `RTCRandom` implementations to verify provider routing and error propagation. | + +## SCTP + +| Value | Security role | Source and decision | +| --- | --- | --- | +| Verification tag / association ID | Unpredictable nonzero SCTP verification tag | Generated with `rand`'s thread-local CSPRNG and rejected if zero. `rtc-sctp` remains independently usable and does not otherwise need an RTC crypto provider. | +| Initial TSN | Unpredictable starting sequence number | Generated with `rand`'s thread-local CSPRNG and adjusted to be nonzero. Provider propagation would add ownership solely for this field. | +| State-cookie bytes | Unpredictable 256-bit challenge echoed by the peer | Generated with `rand`'s thread-local CSPRNG. Received cookies are compared in constant time. The current in-memory cookie design does not encrypt, authenticate, or serialize server state into the value. | +| Retransmission counters, sequence arithmetic, and timers | Derived protocol state | Deterministic values derived from negotiated state and elapsed time; no random source is appropriate. | +| `ParamRandom` | Peer-provided extension negotiation input | Parsed from the peer rather than generated locally. It is not an application randomness boundary. | +| Unit-test packets and fixtures | Reproducible protocol inputs | Fixed constants remain intentional. Tests for generated values check invariants rather than depending on exact output. | + +## Provider boundary decision + +ICE already owns `Arc` for STUN operations, so routing credential and tie-breaker generation through the same provider is a natural extension of that ownership. SCTP has no cryptographic operation requiring `RTCCrypto`, and its few security-sensitive random values are safely served by its existing direct CSPRNG. Therefore this phase does not add an `rtc-crypto` dependency or provider parameter to `rtc-sctp`. diff --git a/docs/semver.md b/docs/semver.md new file mode 100644 index 00000000..a6fc09d5 --- /dev/null +++ b/docs/semver.md @@ -0,0 +1,107 @@ +# API stability policy + +This document records how `rtc` and the protocol subcrates keep their public API extensible, +and the specific decisions taken before 1.0. It exists because `#[non_exhaustive]` and trait +sealing **cannot be introduced after 1.0** — adding either is itself a breaking change, so +the window closes when the major version lands. + +The async wrapper keeps its own copy of this policy plus its trait decisions in +`webrtc/docs/semver.md`. + +## The rule + +> Mark a public enum `#[non_exhaustive]` when the set of variants is defined by something +> outside this codebase and can grow — an IANA registry, a protocol state machine, an error +> taxonomy, an event stream. Leave it exhaustive when the set is closed by construction — a +> fixed-width wire field, a binary role, a mathematically complete set — and where losing +> exhaustiveness checking would cost callers more than a future variant would. + +`#[non_exhaustive]` does **not** affect matches inside the defining crate, but it does affect +that crate's examples, integration tests, and benchmarks, which are separate compilation +units. It also blocks downstream struct-literal construction of a non-exhaustive variant, and +makes an irrefutable `let` destructure of a single-variant enum illegal. + +Because `rtc` re-exports all 15 protocol subcrates wholesale — + +```rust +pub use {datachannel, dtls, ice, interceptor, mdns, media, rtcp, rtp, + sansio, sctp, sdp, shared, srtp, stun, turn}; +``` + +— their entire public surface is part of `rtc`'s public API and is frozen at 1.0 alongside it. +The subcrates are in scope for this policy, not an afterthought. + +## Inventory + +122 public enums, all with an explicit decision: + +| Area | `#[non_exhaustive]` | Kept exhaustive | +|---|---:|---:| +| `rtc/src` | 34 | 0 | +| protocol subcrates | 69 | 19 | +| **total** | **103** | **19** | + +### `rtc/src` — all 34 marked + +Every directly declared enum in the core is `#[non_exhaustive]`: the W3C-facing state, +configuration, transport, statistics, event, and message enums. All of these track external +specifications (W3C WebRTC, IANA registries, RFC state machines) that add values over time. + +### Protocol subcrates — kept exhaustive (19) + +These are the deliberate exceptions. Each is closed by construction, and exhaustive matching +is worth more to callers than room for a variant we have no way to add. + +| Crate | Enum | Why it cannot grow | +|---|---|---| +| `rtc-dtls` | `ExtendedMasterSecretType` | Request / Require / Disable — a complete policy triple. | +| `rtc-dtls` | `CryptoCcmTagLen` | CCM tag is 8 or 16 bytes. | +| `rtc-dtls` | `DtlsPadding` | Padding scheme marker. | +| `rtc-ice` | `Role` | Controlling / Controlled (RFC 8445). | +| `rtc-shared` | `EcnCodepoint` | Exactly four values in a two-bit field (RFC 3168). | +| `rtc-rtcp` | `ChunkType` | One-bit RLE discriminator. | +| `rtc-rtcp` | `TTLorHopLimitType` | Two-bit field. | +| `rtc-rtcp` | `StatusChunkTypeTcc` | One-bit field. | +| `rtc-rtcp` | `SymbolTypeTcc` | Two-bit field. | +| `rtc-rtcp` | `SymbolSizeTypeTcc` | One-bit field. | +| `rtc-rtcp` | `PacketStatusChunk` | Closed by its one-bit discriminator. | +| `rtc-media` | `Deinterleaved`, `Interleaved` | Buffer-layout markers. | +| `rtc-sctp` | `Side` | Client / Server. | +| `rtc-datachannel` | `DataChannelThreshold` | Low / High. | +| `rtc-rtp` | `CameraDirection` | Front / Back. | +| `rtc-rtp` | `VideoRotation` | 0° / 90° / 180° / 270°. | +| `rtc-sdp` | `Direction` | sendrecv / sendonly / recvonly / inactive (RFC 4566). | +| `rtc-sdp` | `ConnectionRole` | active / passive / actpass / holdconn (RFC 4145). | + +### Protocol subcrates — marked (69) + +Everything else, falling into four families: + +- **IANA / protocol registries**, which grow by design: `CipherSuiteId`, `NamedCurve`, + `SignatureScheme`, `HashAlgorithm`, `SignatureAlgorithm`, `SrtpProtectionProfile`, + `ExtensionValue`, `HandshakeType`, `ContentType`, `ClientCertificateType`, + `CompressionMethodId`, `EllipticCurveType`, `PacketType`, `SdesType`, `BlockType`, + `PayloadProtocolIdentifier`, `MessageType`, `ChannelType`, `ProtectionProfile`, + `H264NalUnitType`, `H265NalUnitType`, `UnitType`, `CandidateType`, `NetworkType`, + `TcpType`, `SchemeType`, `ProtoType`, … +- **State machines**: `ConnectionState`, `GatheringState`, `CandidatePairState`, + `RecvSendState`, `ReliabilityType`, … +- **Event streams**: `Event` (ice, turn, sctp), `EndpointEvent`, `DatagramEvent`, + `StunEvent`, `ClientAgent`, `MdnsEvent`, `StreamEvent`, … +- **Error taxonomies**: `Error` (shared, media), `AssociationError`, `ConnectError`. + +Also marked: wire sum types that gain arms when the protocol does — `Content`, +`HandshakeMessage`, `Extension`, `Message`, `Payload`, `H265Payload`, `HeaderExtension`, +`H26xNAL`, `Packet`, `CryptoPrivateKeyKind`, `NextHop`, `Kind`, `TransportProtocol`, +`IvfCodec`, `OggHeaderType`, `MulticastDnsMode`, `ClientAuthType`, `CipherSuiteHash`, and the +Windows interface-info enums in `rtc-shared::ifaces`. + +## When adding a new public item + +- **New public enum**: decide exhaustive-or-not *at the point of introduction* and record it + here. After 1.0 the decision is frozen. +- **New variant on a `#[non_exhaustive]` enum**: minor release, no breakage. +- **New variant on an exhaustive enum**: major release. Reconsider the classification first. +- Matching a `#[non_exhaustive]` enum from another crate in this workspace — including from + `rtc` onto a subcrate enum — requires a `_` arm. Prefer a fallback that degrades safely + (skip the packet, return `Unspecified`) over `unreachable!()`. diff --git a/examples/README.md b/examples/README.md index 318f5228..f01a736f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -50,6 +50,10 @@ check [Pion Examples](https://github.com/pion/webrtc/tree/master/examples#readme track. - ✅ [RTCP Processing](rtcp-processing): The rtcp-processing example demonstrates how to create a custom RtcpForwarderInterceptor using the derive macros. This allows access to media statistics and control information. +- ✅ [RTCP Processing Boxed](rtcp-processing-boxed): The rtcp-processing-boxed example is the type-erased variant of + rtcp-processing. It holds the peer connection as `RTCPeerConnection`, so an interceptor chain + assembled at runtime (here, switched by a `--no-rtcp-forwarding` flag) does not leak its type into the application: + a plain, non-generic struct can own the peer connection, and peers built with different chains share one type. - ✅ [Save to Disk AV1](save-to-disk-av1): The save-to-disk-av1 is a simple application that shows how to save a video to disk using AV1. - ✅ [Play from Disk Playlist Control](play-from-disk-playlist-control): Streams Opus pages from multi or single track diff --git a/examples/broadcast/broadcast.rs b/examples/broadcast/broadcast.rs index b016164b..1d52b053 100644 --- a/examples/broadcast/broadcast.rs +++ b/examples/broadcast/broadcast.rs @@ -326,6 +326,7 @@ async fn run_broadcaster( trace!("[Receiver] Received RTCP packets"); } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/data-channels-close/data-channels-close.rs b/examples/data-channels-close/data-channels-close.rs index 2527054a..88ff645b 100644 --- a/examples/data-channels-close/data-channels-close.rs +++ b/examples/data-channels-close/data-channels-close.rs @@ -280,6 +280,7 @@ async fn run( let msg_str = String::from_utf8(data_channel_message.data.to_vec())?; println!("Message from DataChannel '{}': '{}'", dc.label(), msg_str); } + _ => {} } } diff --git a/examples/data-channels-create/data-channels-create.rs b/examples/data-channels-create/data-channels-create.rs index 70921544..ccec57a0 100644 --- a/examples/data-channels-create/data-channels-create.rs +++ b/examples/data-channels-create/data-channels-create.rs @@ -271,6 +271,7 @@ async fn run( let msg_str = String::from_utf8(data_channel_message.data.to_vec())?; println!("Message from DataChannel '{}': '{}'", dc.label(), msg_str); } + _ => {} } } diff --git a/examples/data-channels-flow-control/data-channels-flow-control.rs b/examples/data-channels-flow-control/data-channels-flow-control.rs index 25370bc3..59e3144b 100644 --- a/examples/data-channels-flow-control/data-channels-flow-control.rs +++ b/examples/data-channels-flow-control/data-channels-flow-control.rs @@ -200,11 +200,11 @@ async fn run_requester( // Poll requester events while let Some(event) = requester.poll_event() { match event { - RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { - if state == RTCPeerConnectionState::Failed { - eprintln!("Requester peer connection failed"); - break 'EventLoop; - } + RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) + if state == RTCPeerConnectionState::Failed => + { + eprintln!("Requester peer connection failed"); + break 'EventLoop; } RTCPeerConnectionEvent::OnDataChannel(data_channel_event) => { match data_channel_event { @@ -358,21 +358,16 @@ async fn run_responder( // Poll responder events while let Some(event) = responder.poll_event() { match event { - RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { - if state == RTCPeerConnectionState::Failed { - eprintln!("Responder peer connection failed"); - break 'EventLoop; - } + RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) + if state == RTCPeerConnectionState::Failed => + { + eprintln!("Responder peer connection failed"); + break 'EventLoop; } - RTCPeerConnectionEvent::OnDataChannel(data_channel_event) => { - match data_channel_event { - RTCDataChannelEvent::OnOpen(_channel_id) => { - println!("Responder: Data channel opened"); - resp_data_channel_opened = true; - throughput_start = Instant::now(); - } - _ => {} - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(_channel_id)) => { + println!("Responder: Data channel opened"); + resp_data_channel_opened = true; + throughput_start = Instant::now(); } _ => {} } @@ -385,6 +380,7 @@ async fn run_responder( RTCMessage::DataChannelMessage(_channel_id, data_channel_message) => { total_bytes_received += data_channel_message.data.len(); } + _ => {} } } diff --git a/examples/data-channels-offer-answer/data-channels-answer.rs b/examples/data-channels-offer-answer/data-channels-answer.rs index 2cca7596..4a8b20ce 100644 --- a/examples/data-channels-offer-answer/data-channels-answer.rs +++ b/examples/data-channels-offer-answer/data-channels-answer.rs @@ -233,20 +233,17 @@ async fn main() -> Result<()> { break 'EventLoop; } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = peer_connection.data_channel(channel_id) { - println!( - "Data channel '{}'-'{}' open. Random messages will now be sent every 5 seconds", - dc.label(), - dc.id() - ); - data_channel_opened = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { + if let Some(dc) = peer_connection.data_channel(channel_id) { + println!( + "Data channel '{}'-'{}' open. Random messages will now be sent every 5 seconds", + dc.label(), + dc.id() + ); + data_channel_opened = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -260,6 +257,7 @@ async fn main() -> Result<()> { String::from_utf8(data_channel_message.data.to_vec()).unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/data-channels-offer-answer/data-channels-offer.rs b/examples/data-channels-offer-answer/data-channels-offer.rs index b66034a3..32b04627 100644 --- a/examples/data-channels-offer-answer/data-channels-offer.rs +++ b/examples/data-channels-offer-answer/data-channels-offer.rs @@ -257,20 +257,17 @@ async fn main() -> Result<()> { break 'EventLoop; } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = peer_connection.data_channel(channel_id) { - println!( - "Data channel '{}'-'{}' open. Random messages will now be sent every 5 seconds", - dc.label(), - dc.id() - ); - data_channel_opened = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { + if let Some(dc) = peer_connection.data_channel(channel_id) { + println!( + "Data channel '{}'-'{}' open. Random messages will now be sent every 5 seconds", + dc.label(), + dc.id() + ); + data_channel_opened = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -284,6 +281,7 @@ async fn main() -> Result<()> { String::from_utf8(data_channel_message.data.to_vec()).unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/data-channels/data-channels.rs b/examples/data-channels/data-channels.rs index 10d7af37..894bce8d 100644 --- a/examples/data-channels/data-channels.rs +++ b/examples/data-channels/data-channels.rs @@ -263,6 +263,7 @@ async fn run( ); dc.send_text(msg_str)?; } + _ => {} } } diff --git a/examples/ice-restart/ice-restart.rs b/examples/ice-restart/ice-restart.rs index 89b5a585..96dfeeff 100644 --- a/examples/ice-restart/ice-restart.rs +++ b/examples/ice-restart/ice-restart.rs @@ -249,16 +249,15 @@ async fn main() -> Result<()> { println!("Peer Connection connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!("Data channel '{}'-'{}' open", dc.label(), dc.id()); - data_channel_opened = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!("Data channel '{}'-'{}' open", dc.label(), dc.id()); + data_channel_opened = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -272,6 +271,7 @@ async fn main() -> Result<()> { .unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/ice-tcp-active-passive/ice-tcp-active-offer.rs b/examples/ice-tcp-active-passive/ice-tcp-active-offer.rs index e771a306..f330fcb8 100644 --- a/examples/ice-tcp-active-passive/ice-tcp-active-offer.rs +++ b/examples/ice-tcp-active-passive/ice-tcp-active-offer.rs @@ -319,16 +319,13 @@ async fn main() -> Result<()> { println!("[Offer] Connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = peer_connection.data_channel(channel_id) { - println!("[Offer] Data channel '{}'-'{}' open", dc.label(), dc.id()); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { + if let Some(dc) = peer_connection.data_channel(channel_id) { + println!("[Offer] Data channel '{}'-'{}' open", dc.label(), dc.id()); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -343,6 +340,7 @@ async fn main() -> Result<()> { String::from_utf8(data_channel_message.data.to_vec()).unwrap_or_default(); println!("[Offer] Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/ice-tcp-active-passive/ice-tcp-passive-answer.rs b/examples/ice-tcp-active-passive/ice-tcp-passive-answer.rs index 94bd158a..0eb580e9 100644 --- a/examples/ice-tcp-active-passive/ice-tcp-passive-answer.rs +++ b/examples/ice-tcp-active-passive/ice-tcp-passive-answer.rs @@ -238,20 +238,15 @@ async fn main() -> Result<()> { println!("[Answer] Connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!( - "[Answer] Data channel '{}'-'{}' open", - dc.label(), - dc.id() - ); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!("[Answer] Data channel '{}'-'{}' open", dc.label(), dc.id()); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -266,6 +261,7 @@ async fn main() -> Result<()> { .unwrap_or_default(); println!("[Answer] Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/ice-tcp/ice-tcp.rs b/examples/ice-tcp/ice-tcp.rs index 1c0970d4..3cbde756 100644 --- a/examples/ice-tcp/ice-tcp.rs +++ b/examples/ice-tcp/ice-tcp.rs @@ -231,21 +231,20 @@ async fn run_main_loop( println!("Peer Connection connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!( - "{} - Data channel '{}'-'{}' open", - chrono::Local::now().format("%H:%M:%S"), - dc.label(), - dc.id() - ); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!( + "{} - Data channel '{}'-'{}' open", + chrono::Local::now().format("%H:%M:%S"), + dc.label(), + dc.id() + ); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -260,6 +259,7 @@ async fn run_main_loop( .unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/mdns-query-and-gather/mdns-query-and-gather.rs b/examples/mdns-query-and-gather/mdns-query-and-gather.rs index dbb113f1..55052cc4 100644 --- a/examples/mdns-query-and-gather/mdns-query-and-gather.rs +++ b/examples/mdns-query-and-gather/mdns-query-and-gather.rs @@ -311,6 +311,7 @@ async fn run( ); dc.send_text(msg_str)?; } + _ => {} } } diff --git a/examples/play-from-disk-playlist-control/play-from-disk-playlist-control.rs b/examples/play-from-disk-playlist-control/play-from-disk-playlist-control.rs index 4f99100a..a98fa482 100644 --- a/examples/play-from-disk-playlist-control/play-from-disk-playlist-control.rs +++ b/examples/play-from-disk-playlist-control/play-from-disk-playlist-control.rs @@ -286,6 +286,7 @@ fn parse_playlist(path: &str) -> Result> { } continue; } + _ => {} } } diff --git a/examples/reflect/reflect.rs b/examples/reflect/reflect.rs index 57596b4c..c1eeeacf 100644 --- a/examples/reflect/reflect.rs +++ b/examples/reflect/reflect.rs @@ -426,6 +426,7 @@ async fn run( // like NACK this needs to be called. } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/rtcp-processing-boxed/README.md b/examples/rtcp-processing-boxed/README.md new file mode 100644 index 00000000..3fec1764 --- /dev/null +++ b/examples/rtcp-processing-boxed/README.md @@ -0,0 +1,168 @@ +# rtcp-processing-boxed + +The type-erased counterpart of the [`rtcp-processing`](../rtcp-processing) example. + +It does exactly the same thing — installs a custom `RtcpForwarderInterceptor` so RTCP packets surface through +`poll_read()`, then prints them — but it stores the peer connection as `RTCPeerConnection` instead of +letting the interceptor chain's type leak into the application. Read `rtcp-processing` first for the RTCP concepts and +the forwarder interceptor itself; this README only covers what is different. + +## The problem: the chain's type is enormous, and infectious + +`RTCPeerConnection` is generic over its interceptor chain `I`. A chain built by `register_default_interceptors` +plus one custom layer has a type like: + +```text +RtcpForwarderInterceptor>>>>> +``` + +As long as the chain flows straight into a local variable, `impl Interceptor` hides this — that is what +`rtcp-processing` does: + +```rust +fn create_rtc_peer() -> Result> { /* ... */ } +``` + +That breaks down as soon as an application does anything more than hold the peer connection in a `let`: + +| You want to… | With `impl Interceptor` | +|-------------------------------------------------------------------------|-------------------------| +| store the peer connection in your own struct | the struct must become `MyStruct`, and `I` then spreads to every impl block and helper function | +| keep peers in a `Vec` / `HashMap` where chains differ per peer | impossible — different chains are different types | +| choose the chain in an `if` / `match` at runtime | impossible — the arms have different types | +| write a trait-object-free API boundary (`fn handle(pc: &mut ...)`) | must be generic | + +The usual workaround is to hand-write an application-level `trait PeerConnection { … }` that mirrors the whole +`RTCPeerConnection` API and store a `Box` — hundreds of lines of pure forwarding boilerplate. + +## The fix: erase the chain, not the peer connection + +`Interceptor` is object safe, so the chain itself can be boxed. [`Registry::boxed`] does that, and +`BoxedInterceptor` is the alias for `Box`: + +```rust +use rtc::interceptor::{BoxedInterceptor, Registry}; + +fn build_peer_connection( + forward_rtcp: bool, + mut media_engine: MediaEngine, +) -> Result> { + let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; + + // Two different chain types, unified by `.boxed()`. + let registry = if forward_rtcp { + registry.with(RtcpForwarderBuilder::new().build()).boxed() + } else { + registry.boxed() + }; + + Ok(RTCPeerConnectionBuilder::new() + .with_configuration(config) + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .build()?) +} +``` + +Every peer connection now has the same concrete type, so an ordinary struct can own one: + +```rust +struct RtcpSession { + peer_connection: RTCPeerConnection, // no type parameter needed + socket: Arc, + local_addr: SocketAddr, + ssrc2kind: HashMap, + rtcp_count: u64, +} + +impl RtcpSession { // a plain impl block + async fn flush_writes(&mut self) { /* ... */ } + fn drain_events(&mut self) -> Result { /* ... */ } + fn drain_reads(&mut self) { /* ... */ } +} +``` + +Compare with `rtcp-processing`, where all of this logic has to live inline in `run()` because there is no nameable +type to hang it on. + +**Cost:** one virtual call per interceptor-chain entry point (`handle_read`, `poll_write`, `handle_timeout`, …). The +chain's *interior* is untouched — the layers still call each other through static dispatch and inline as before. If +your chain is fixed at compile time, keep `RTCPeerConnection`; nothing about it changed. + +## Instructions + +### Open the rtcp-processing example page + +[jsfiddle.net](https://jsfiddle.net/zurq6j7x/) — the same page the `rtcp-processing` example uses. You should see two +text-areas, a 'Start Session' button and 'Copy browser SessionDescription to clipboard'. + +### Run + +```bash +cargo run --example rtcp-processing-boxed +``` + +Paste the browser's offer, then paste the printed answer back into the browser. RTCP packets are printed as media +flows, exactly as in `rtcp-processing`. + +### Run with the forwarder omitted + +```bash +cargo run --example rtcp-processing-boxed -- --no-rtcp-forwarding +``` + +The peer connection has the **same type** (`RTCPeerConnection`) and the same code path drives it, +but the chain was built without the RTCP forwarder — so the default interceptors consume RTCP internally and nothing +is printed. This flag is the demonstration: the two chains are different Rust types, chosen at runtime, behind one +peer connection type. + +### Other flags + +```bash +cargo run --example rtcp-processing-boxed -- --debug # debug logging +cargo run --example rtcp-processing-boxed -- --input-sdp-file offer.txt # read SDP from a file +``` + +## Example Output + +``` +Interceptor chain: defaults + RTCP forwarder (boxed) +Paste your offer here: + + +Offer received: ... +RTCP Processing (boxed) listening on 127.0.0.1:54321... + +Paste this answer in your browser: +eyJ0eXBlIjoiYW5zd2VyIiwic2RwIjoi... + +Waiting for RTCP packets... +Press Ctrl-C to stop + +Connection State has changed: connected +Connection established! Waiting for RTCP packets... + +Track has started - track_id: video-track, receiver_id: 0 + Stream ID: my-stream, Track ID: video-track, Kind: video, Codec: video/VP8 + +=== RTCP Packet #1 (Track: video-track) === + [1] Type: SenderReport, Length: 12 words + SenderReport from 1234567890 + ... + +^C +Ctrl-C received, shutting down... +Total RTCP packets received: 42 +Event loop exited +``` + +With `--no-rtcp-forwarding` the first line reads +`Interceptor chain: defaults only (boxed) — no RTCP will be printed`, and no `=== RTCP Packet ===` blocks appear. + +## See also + +- [`rtcp-processing`](../rtcp-processing) — the same example without type erasure +- `tests/rtcp_processing_boxed_interop.rs` — integration tests for the boxed chain, including two peers with + *different* chains driven out of a single `Vec` diff --git a/examples/rtcp-processing-boxed/rtcp-processing-boxed.rs b/examples/rtcp-processing-boxed/rtcp-processing-boxed.rs new file mode 100644 index 00000000..0bbe66d3 --- /dev/null +++ b/examples/rtcp-processing-boxed/rtcp-processing-boxed.rs @@ -0,0 +1,588 @@ +//! rtcp-processing-boxed is the type-erased counterpart of the `rtcp-processing` example. +//! +//! It does exactly the same thing — installs a custom `RtcpForwarderInterceptor` so RTCP +//! packets surface through `poll_read()`, then prints them — but it holds the peer +//! connection as `RTCPeerConnection` instead of letting the interceptor +//! chain's type leak into the application. +//! +//! # Why erase the chain? +//! +//! An interceptor chain is a nest of generic types: `register_default_interceptors` +//! followed by `.with(RtcpForwarderBuilder::new().build())` produces something like +//! +//! ```text +//! RtcpForwarderInterceptor>>>>> +//! ``` +//! +//! `RTCPeerConnection` is generic over that type. As long as the chain flows straight +//! into a local variable, `impl Interceptor` hides it (that is what `rtcp-processing` +//! does). But the moment an application wants to *store* the peer connection, the type +//! parameter propagates: every struct that owns one, and every function that touches +//! those structs, has to carry ``. And an opaque `impl Interceptor` type +//! still cannot be produced by two different branches of an `if`, nor put in a collection +//! next to a peer built with a different chain. +//! +//! [`Registry::boxed`] erases the chain to [`BoxedInterceptor`] (`Box`), +//! so every peer connection has the same concrete type no matter how its chain was +//! assembled at runtime. Two consequences are visible below: +//! +//! * [`build_peer_connection`] picks its chain from a **command-line flag** — the two +//! branches build different chain types and unify only after `.boxed()`. +//! * [`RtcpSession`] is a plain struct with **no type parameter**, and its methods are +//! ordinary non-generic methods. +//! +//! The cost is one virtual call per chain entry point; the chain's interior stays +//! statically dispatched. + +use anyhow::Result; +use bytes::BytesMut; +use clap::Parser; +use env_logger::Target; +use log::{error, trace}; +use rtc::interceptor::{ + BoxedInterceptor, Interceptor, Packet, Registry, StreamInfo, TaggedPacket, interceptor, +}; +use rtc::peer_connection::configuration::RTCConfigurationBuilder; +use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors; +use rtc::peer_connection::configuration::media_engine::{ + MIME_TYPE_OPUS, MIME_TYPE_VP8, MediaEngine, +}; +use rtc::peer_connection::event::RTCTrackEvent; +use rtc::peer_connection::event::{RTCEvent, RTCPeerConnectionEvent}; +use rtc::peer_connection::message::RTCMessage; +use rtc::peer_connection::sdp::RTCSessionDescription; +use rtc::peer_connection::state::RTCPeerConnectionState; +use rtc::peer_connection::transport::RTCIceServer; +use rtc::peer_connection::transport::{CandidateConfig, CandidateHostConfig, RTCIceCandidate}; +use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; +use rtc::rtp_transceiver::rtp_sender::RtpCodecKind; +use rtc::rtp_transceiver::rtp_sender::{RTCRtpCodec, RTCRtpCodecParameters}; +use rtc::sansio::{self, Protocol}; // Required for #[interceptor] macro and Protocol trait methods +use rtc::shared::error::Error; +use rtc::shared::{TaggedBytesMut, TransportContext, TransportProtocol}; +use std::collections::{HashMap, VecDeque}; +use std::fs::OpenOptions; +use std::io::Write; +use std::net::SocketAddr; +use std::str::FromStr; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::net::UdpSocket; +use tokio::sync::mpsc::channel; + +const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(86400); // 1 day + +// ============================================================================ +// RTCP Forwarder Interceptor +// ============================================================================ +// +// Identical to the one in the `rtcp-processing` example: it forwards RTCP packets to the +// application via poll_read(). By default RTCP is consumed by the interceptor chain (for +// statistics, NACK, congestion control) and never reaches the application. + +/// Builder for the RtcpForwarderInterceptor. +pub struct RtcpForwarderBuilder

{ + _phantom: std::marker::PhantomData

, +} + +impl

Default for RtcpForwarderBuilder

{ + fn default() -> Self { + Self { + _phantom: std::marker::PhantomData, + } + } +} + +impl

RtcpForwarderBuilder

{ + /// Create a new builder. + pub fn new() -> Self { + Self::default() + } + + /// Build the interceptor. + pub fn build(self) -> impl FnOnce(P) -> RtcpForwarderInterceptor

{ + move |inner| RtcpForwarderInterceptor::new(inner) + } +} + +/// Interceptor that forwards RTCP packets to the application. +#[derive(Interceptor)] +pub struct RtcpForwarderInterceptor

{ + #[next] + next: P, + read_queue: VecDeque, +} + +impl

RtcpForwarderInterceptor

{ + /// Create a new RtcpForwarderInterceptor. + fn new(next: P) -> Self { + Self { + next, + read_queue: VecDeque::new(), + } + } +} + +#[interceptor] +impl RtcpForwarderInterceptor

{ + #[overrides] + fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> { + // If this is an RTCP packet, queue a copy for the application + if let Packet::Rtcp(rtcp_packets) = &msg.message { + self.read_queue.push_back(TaggedPacket { + now: msg.now, + transport: msg.transport, + message: Packet::Rtcp(rtcp_packets.clone()), + }); + } + // Always pass to next interceptor for normal processing + self.next.handle_read(msg) + } + + #[overrides] + fn poll_read(&mut self) -> Option { + // First return any queued RTCP packets + if let Some(pkt) = self.read_queue.pop_front() { + return Some(pkt); + } + // Then check next interceptor + self.next.poll_read() + } + + #[overrides] + fn close(&mut self) -> Result<(), Self::Error> { + self.read_queue.clear(); + self.next.close() + } +} + +// ============================================================================ +// Building the peer connection: the chain is chosen at runtime +// ============================================================================ + +/// Build a peer connection whose interceptor chain depends on `forward_rtcp`. +/// +/// This is the shape that the `impl Interceptor` return type cannot express. The two +/// branches produce chains of different Rust types, and `if`/`else` arms must agree on a +/// type — so without erasure this function would have to be split in two, and its two +/// return types would then infect everything downstream. `Registry::boxed()` collapses +/// both to [`BoxedInterceptor`], leaving one ordinary return type. +fn build_peer_connection( + forward_rtcp: bool, + mut media_engine: MediaEngine, +) -> Result> { + // Default interceptors (NACK, RTCP reports, TWCC receiver) in both cases. + let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; + + // The RTCP forwarder must be the *outermost* layer, so it sees RTCP before the rest + // of the chain consumes it. + let registry = if forward_rtcp { + registry.with(RtcpForwarderBuilder::new().build()).boxed() + } else { + registry.boxed() + }; + + let config = RTCConfigurationBuilder::new() + .with_ice_servers(vec![RTCIceServer { + urls: vec!["stun:stun.l.google.com:19302".to_string()], + ..Default::default() + }]) + .build(); + + let peer_connection = RTCPeerConnectionBuilder::new() + .with_configuration(config) + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .build()?; + + Ok(peer_connection) +} + +// ============================================================================ +// The session: a plain struct that owns the peer connection +// ============================================================================ + +/// Everything the example needs to run one session. +/// +/// The point of this struct is what is *missing* from it: a type parameter. Holding an +/// `RTCPeerConnection` would have made this `struct RtcpSession`, and +/// then every `impl` block, every helper function, and any collection of sessions would +/// have had to carry `I` too. With the chain erased, this is an ordinary struct with +/// ordinary methods — and a server could keep a `Vec` or +/// `HashMap` even if each session were configured with a +/// different chain. +struct RtcpSession { + peer_connection: RTCPeerConnection, + socket: Arc, + local_addr: SocketAddr, + ssrc2kind: HashMap, + rtcp_count: u64, +} + +impl RtcpSession { + /// Bind a socket and build the peer connection with the requested chain. + async fn new(forward_rtcp: bool) -> Result { + let socket = UdpSocket::bind("127.0.0.1:0").await?; + let local_addr = socket.local_addr()?; + + let mut media_engine = MediaEngine::default(); + + // Register VP8 codec for video + media_engine.register_codec( + RTCRtpCodecParameters { + rtp_codec: RTCRtpCodec { + mime_type: MIME_TYPE_VP8.to_string(), + clock_rate: 90000, + channels: 0, + sdp_fmtp_line: "".to_string(), + rtcp_feedback: vec![], + }, + payload_type: 96, + }, + RtpCodecKind::Video, + )?; + + // Register Opus codec for audio + media_engine.register_codec( + RTCRtpCodecParameters { + rtp_codec: RTCRtpCodec { + mime_type: MIME_TYPE_OPUS.to_string(), + clock_rate: 48000, + channels: 2, + sdp_fmtp_line: "".to_string(), + rtcp_feedback: vec![], + }, + payload_type: 111, + }, + RtpCodecKind::Audio, + )?; + + let peer_connection = build_peer_connection(forward_rtcp, media_engine)?; + + Ok(Self { + peer_connection, + socket: Arc::new(socket), + local_addr, + ssrc2kind: HashMap::new(), + rtcp_count: 0, + }) + } + + /// Apply the browser's offer and produce our answer. + fn answer(&mut self, offer: RTCSessionDescription) -> Result { + self.peer_connection.set_remote_description(offer)?; + + let candidate = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: self.local_addr.ip().to_string(), + port: self.local_addr.port(), + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + self.peer_connection + .add_local_candidate(RTCIceCandidate::from(&candidate).to_json()?)?; + + let answer = self.peer_connection.create_answer(None)?; + self.peer_connection.set_local_description(answer.clone())?; + Ok(answer) + } + + /// Send everything the peer connection wants to put on the wire. + async fn flush_writes(&mut self) { + while let Some(msg) = self.peer_connection.poll_write() { + match self + .socket + .send_to(&msg.message, msg.transport.peer_addr) + .await + { + Ok(n) => trace!( + "socket write to {} with {} bytes", + msg.transport.peer_addr, n + ), + Err(err) => error!("socket write error: {}", err), + } + } + } + + /// Drain connection/track events. Returns `false` when the session should stop. + fn drain_events(&mut self) -> Result { + while let Some(event) = self.peer_connection.poll_event() { + match event { + RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { + println!("Connection State has changed: {}", state); + if state == RTCPeerConnectionState::Failed { + println!("Connection failed, exiting..."); + return Ok(false); + } else if state == RTCPeerConnectionState::Connected { + println!("Connection established! Waiting for RTCP packets...\n"); + } + } + RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(init)) => { + println!( + "Track has started - track_id: {}, receiver_id: {:?}", + init.track_id, init.receiver_id + ); + + if let Some(receiver) = self.peer_connection.rtp_receiver(init.receiver_id) { + let track = receiver.track(); + let ssrc = track + .ssrcs() + .next() + .ok_or(Error::ErrRTPReceiverForSSRCTrackStreamNotFound)?; + let codec = track.codec(ssrc).ok_or(Error::ErrCodecNotFound)?; + + println!( + " Stream ID: {}, Track ID: {}, Kind: {}, Codec: {}", + track.stream_id(), + track.track_id(), + track.kind(), + codec.mime_type + ); + + self.ssrc2kind.insert(ssrc, track.kind()); + } + println!(); + } + RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnClose(track_id)) => { + println!("Track closed: {}", track_id); + } + _ => {} + } + } + Ok(true) + } + + /// Print every RTCP packet the forwarder surfaced. + /// + /// Nothing arrives here when the session was built without the forwarder — that is + /// what `--no-rtcp-forwarding` demonstrates. + fn drain_reads(&mut self) { + while let Some(message) = self.peer_connection.poll_read() { + match message { + RTCMessage::RtpPacket(_track_id, _rtp_packet) => { + // We're not processing RTP packets in this example + trace!("Received RTP packet"); + } + RTCMessage::RtcpPacket(track_id, rtcp_packets) => { + self.rtcp_count += 1; + println!( + "=== RTCP Packet #{} (Track: {}) ===", + self.rtcp_count, track_id + ); + + for (i, packet) in rtcp_packets.iter().enumerate() { + let header = packet.header(); + println!( + " [{}] Type: {:?}, Length: {} words", + i + 1, + header.packet_type, + header.length + ); + + // The RTCP packets implement Display for human-readable output + for line in format!("{}", packet).lines() { + println!(" {}", line); + } + } + println!(); + } + RTCMessage::DataChannelMessage(_, _) => {} + _ => {} + } + } + } + + /// Feed a datagram read off the socket into the peer connection. + fn handle_datagram(&mut self, data: &[u8], peer_addr: SocketAddr) -> Result<()> { + self.peer_connection.handle_read(TaggedBytesMut { + now: Instant::now(), + transport: TransportContext { + local_addr: self.local_addr, + peer_addr, + ecn: None, + transport_protocol: TransportProtocol::UDP, + }, + message: BytesMut::from(data), + })?; + Ok(()) + } +} + +// ============================================================================ +// Main Application +// ============================================================================ + +#[derive(Parser)] +#[command(name = "rtcp-processing-boxed")] +#[command(author = "Rusty Rain ")] +#[command(version = "0.1.0")] +#[command(about = "RTCP packet processing with a type-erased interceptor chain")] +struct Cli { + #[arg(short, long)] + debug: bool, + #[arg(short, long, default_value_t = format!("INFO"))] + log_level: String, + #[arg(short, long, default_value_t = format!(""))] + input_sdp_file: String, + #[arg(short, long, default_value_t = format!(""))] + output_log_file: String, + /// Build the chain *without* the RTCP forwarder. The peer connection still has type + /// `RTCPeerConnection`; it simply never surfaces RTCP to the + /// application, so no RTCP packets are printed. + #[arg(long)] + no_rtcp_forwarding: bool, +} + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + let input_sdp_file = cli.input_sdp_file; + let output_log_file = cli.output_log_file; + let log_level = log::LevelFilter::from_str(&cli.log_level)?; + + if cli.debug { + env_logger::Builder::new() + .target(if !output_log_file.is_empty() { + Target::Pipe(Box::new( + OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(output_log_file)?, + )) + } else { + Target::Stdout + }) + .format(|buf, record| { + writeln!( + buf, + "{}:{} [{}] {} - {}", + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + record.level(), + chrono::Local::now().format("%H:%M:%S.%6f"), + record.args() + ) + }) + .filter(None, log_level) + .init(); + } + + run(input_sdp_file, !cli.no_rtcp_forwarding).await?; + + Ok(()) +} + +async fn run(input_sdp_file: String, forward_rtcp: bool) -> Result<()> { + // The chain is decided here, at runtime — and the session type does not change. + let mut session = RtcpSession::new(forward_rtcp).await?; + if forward_rtcp { + println!("Interceptor chain: defaults + RTCP forwarder (boxed)"); + } else { + println!("Interceptor chain: defaults only (boxed) — no RTCP will be printed"); + } + + // Wait for the offer to be pasted + println!("Paste your offer here:"); + let line = if input_sdp_file.is_empty() { + signal::must_read_stdin()? + } else { + std::fs::read_to_string(&input_sdp_file)? + }; + let desc_data = signal::decode(line.as_str())?; + let offer = serde_json::from_str::(&desc_data)?; + println!("Offer received: {}", offer); + + let answer = session.answer(offer)?; + + println!( + "RTCP Processing (boxed) listening on {}...", + session.local_addr + ); + + // Output the answer + let json_str = serde_json::to_string(&answer)?; + let b64 = signal::encode(&json_str); + println!("\nPaste this answer in your browser:\n{}\n", b64); + + let (_event_tx, mut event_rx) = channel::(8); + let mut buf = vec![0; 2000]; + + println!("Waiting for RTCP packets..."); + println!("Press Ctrl-C to stop\n"); + + // Event loop — all of it non-generic, because `RtcpSession` is. + 'EventLoop: loop { + session.flush_writes().await; + if !session.drain_events()? { + break 'EventLoop; + } + session.drain_reads(); + + // Poll peer_connection to get next timeout + let eto = session + .peer_connection + .poll_timeout() + .unwrap_or(Instant::now() + DEFAULT_TIMEOUT_DURATION); + + let delay_from_now = eto + .checked_duration_since(Instant::now()) + .unwrap_or(Duration::from_secs(0)); + if delay_from_now.is_zero() { + session.peer_connection.handle_timeout(Instant::now())?; + continue; + } + + let timer = tokio::time::sleep(delay_from_now); + tokio::pin!(timer); + // Clone the socket handle out so the recv future does not borrow `session` while + // the other arms mutate it. + let socket = Arc::clone(&session.socket); + + tokio::select! { + biased; + + _ = tokio::signal::ctrl_c() => { + println!("\nCtrl-C received, shutting down..."); + println!("Total RTCP packets received: {}", session.rtcp_count); + break 'EventLoop; + } + res = event_rx.recv() => { + match res { + Some(event) => { + session.peer_connection.handle_event(event)?; + } + None => { + eprintln!("event_rx closed"); + break 'EventLoop; + } + } + } + _ = timer.as_mut() => { + session.peer_connection.handle_timeout(Instant::now())?; + } + res = socket.recv_from(&mut buf) => { + match res { + Ok((n, peer_addr)) => { + trace!("socket read {} bytes from {}", n, peer_addr); + session.handle_datagram(&buf[..n], peer_addr)?; + } + Err(err) => { + eprintln!("socket read error {}", err); + break 'EventLoop; + } + } + } + } + } + + session.peer_connection.close()?; + println!("Event loop exited"); + Ok(()) +} diff --git a/examples/rtcp-processing/rtcp-processing.rs b/examples/rtcp-processing/rtcp-processing.rs index 101ed679..5f82f815 100644 --- a/examples/rtcp-processing/rtcp-processing.rs +++ b/examples/rtcp-processing/rtcp-processing.rs @@ -391,6 +391,7 @@ async fn run(input_sdp_file: String) -> Result<()> { println!(); } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/rtp-forwarder/rtp-forwarder.rs b/examples/rtp-forwarder/rtp-forwarder.rs index db9e6299..5560ce7f 100644 --- a/examples/rtp-forwarder/rtp-forwarder.rs +++ b/examples/rtp-forwarder/rtp-forwarder.rs @@ -314,6 +314,7 @@ async fn run_peer_connection( trace!("Received RTCP packets"); } rtc::peer_connection::message::RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/rtp-to-webrtc/rtp-to-webrtc.rs b/examples/rtp-to-webrtc/rtp-to-webrtc.rs index 41b8f141..7299d44b 100644 --- a/examples/rtp-to-webrtc/rtp-to-webrtc.rs +++ b/examples/rtp-to-webrtc/rtp-to-webrtc.rs @@ -245,6 +245,7 @@ async fn run_peer_connection(offer: RTCSessionDescription, rtp_listener: UdpSock trace!("Received RTCP packets"); } rtc::peer_connection::message::RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/save-to-disk-av1/save-to-disk-av1.rs b/examples/save-to-disk-av1/save-to-disk-av1.rs index c265efae..3f15aca5 100644 --- a/examples/save-to-disk-av1/save-to-disk-av1.rs +++ b/examples/save-to-disk-av1/save-to-disk-av1.rs @@ -354,6 +354,7 @@ async fn run( // like NACK this needs to be called. } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/save-to-disk-h26x/save-to-disk-h26x.rs b/examples/save-to-disk-h26x/save-to-disk-h26x.rs index 18894a0e..16dfb535 100644 --- a/examples/save-to-disk-h26x/save-to-disk-h26x.rs +++ b/examples/save-to-disk-h26x/save-to-disk-h26x.rs @@ -419,6 +419,7 @@ async fn run( // like NACK this needs to be called. } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/save-to-disk-vpx/save-to-disk-vpx.rs b/examples/save-to-disk-vpx/save-to-disk-vpx.rs index 3fd375f6..c1b2f1d2 100644 --- a/examples/save-to-disk-vpx/save-to-disk-vpx.rs +++ b/examples/save-to-disk-vpx/save-to-disk-vpx.rs @@ -430,6 +430,7 @@ async fn run( // like NACK this needs to be called. } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/signal/src/lib.rs b/examples/signal/src/lib.rs index 0645447b..a0bb84a2 100644 --- a/examples/signal/src/lib.rs +++ b/examples/signal/src/lib.rs @@ -1,6 +1,29 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! Signaling helpers for the WebRTC.rs examples. +//! +//! The examples in this workspace exchange SDP by hand: one side prints a base64 blob, you +//! paste it into the other. This crate holds the few functions that make that work, so the +//! examples can stay focused on the WebRTC parts. +//! +//! # Example +//! +//! ``` +//! # fn main() -> Result<(), Box> { +//! let json = r#"{"type":"offer","sdp":"v=0\r\n"}"#; +//! +//! // The examples print this blob for you to paste into the other peer. +//! let blob = rtc_signal::encode(json); +//! assert_eq!(rtc_signal::decode(&blob)?, json); +//! # Ok(()) +//! # } +//! ``` +//! +//! It is a support crate for the examples, not part of the WebRTC API — nothing here is +//! needed to use [`rtc`](https://docs.rs/rtc) or [`webrtc`](https://docs.rs/webrtc). + use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::str::FromStr; use std::sync::Arc; @@ -102,6 +125,11 @@ pub fn decode(s: &str) -> Result { Ok(s) } +/// Best-effort discovery of this host's primary local IPv4 address. +/// +/// Opens a UDP socket toward a public address and reads back the local address the OS +/// picked — no packet is sent. Falls back to `127.0.0.1` if that fails, which is what the +/// examples want when running offline. pub fn get_local_ip() -> IpAddr { if let Ok(socket) = std::net::UdpSocket::bind("0.0.0.0:0") && socket.connect("8.8.8.8:80").is_ok() diff --git a/examples/simulcast/simulcast.rs b/examples/simulcast/simulcast.rs index 96f1afc1..7dbae51c 100644 --- a/examples/simulcast/simulcast.rs +++ b/examples/simulcast/simulcast.rs @@ -408,6 +408,7 @@ async fn run( // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/simulcast_bidirection/simulcast_bidirection.rs b/examples/simulcast_bidirection/simulcast_bidirection.rs index e4ef2453..389e3da6 100644 --- a/examples/simulcast_bidirection/simulcast_bidirection.rs +++ b/examples/simulcast_bidirection/simulcast_bidirection.rs @@ -400,6 +400,7 @@ async fn run( // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/stats/stats.rs b/examples/stats/stats.rs index 31ea6dcc..cbdaf362 100644 --- a/examples/stats/stats.rs +++ b/examples/stats/stats.rs @@ -318,6 +318,7 @@ async fn run( // Read incoming RTCP packets } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/swap-tracks/swap-tracks.rs b/examples/swap-tracks/swap-tracks.rs index 5a96619a..c4d0335f 100644 --- a/examples/swap-tracks/swap-tracks.rs +++ b/examples/swap-tracks/swap-tracks.rs @@ -447,6 +447,7 @@ async fn run( // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/examples/trickle-ice-host/trickle-ice-host.rs b/examples/trickle-ice-host/trickle-ice-host.rs index ccd79a10..358bbbcb 100644 --- a/examples/trickle-ice-host/trickle-ice-host.rs +++ b/examples/trickle-ice-host/trickle-ice-host.rs @@ -186,21 +186,20 @@ async fn run_main_loop() -> Result<()> { println!("Peer Connection connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!( - "{} - Data channel '{}'-'{}' open", - chrono::Local::now().format("%H:%M:%S"), - dc.label(), - dc.id() - ); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!( + "{} - Data channel '{}'-'{}' open", + chrono::Local::now().format("%H:%M:%S"), + dc.label(), + dc.id() + ); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -215,6 +214,7 @@ async fn run_main_loop() -> Result<()> { .unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/trickle-ice-relay/trickle-ice-relay.rs b/examples/trickle-ice-relay/trickle-ice-relay.rs index 33ca5c07..1e9a81d1 100644 --- a/examples/trickle-ice-relay/trickle-ice-relay.rs +++ b/examples/trickle-ice-relay/trickle-ice-relay.rs @@ -14,6 +14,7 @@ use hyper::service::{make_service_fn, service_fn}; use hyper::{Body, Method, Response, Server, StatusCode}; use ice::candidate::candidate_relay::CandidateRelayConfig; use log::{error, info, trace}; +use rtc::crypto; use rtc::peer_connection::configuration::RTCConfigurationBuilder; use rtc::peer_connection::configuration::setting_engine::SettingEngine; use rtc::peer_connection::event::RTCDataChannelEvent; @@ -188,7 +189,9 @@ async fn run_main_loop( rto_in_ms: 0, }; - let mut turn_client = Client::new(cfg)?; + // An application is the outside caller, so it selects the crypto provider explicitly. + // Library code never resolves a default. + let mut turn_client = Client::new(cfg, crypto::default_provider()?)?; let allocate_tid = turn_client.allocate()?; let mut relay_addr: Option = None; let relay_local_addr = local_addr; @@ -225,44 +228,42 @@ async fn run_main_loop( Event::TransactionTimeout(_) => { error!("TURN transaction timeout"); } - Event::AllocateResponse(tid, addr) => { - if tid == allocate_tid { - println!("TURN allocation successful, relay address: {}", addr); - relay_addr = Some(addr); - - // If peer connection already exists and we haven't added the relay candidate yet, add it now - if let Some(pc) = peer_connection.as_mut() { - if !relay_candidate_added { - match add_relay_candidate(pc, addr, relay_local_addr) { - Ok(local_candidate_init) => { - relay_candidate_added = true; - println!( - "Added local Relay ICE candidate: {}", - local_candidate_init.candidate - ); - - // Send to browser via WebSocket - if let Some(ref mut ws) = ws_stream { - if let Ok(json) = - serde_json::to_string(&local_candidate_init) + Event::AllocateResponse(tid, addr) if tid == allocate_tid => { + println!("TURN allocation successful, relay address: {}", addr); + relay_addr = Some(addr); + + // If peer connection already exists and we haven't added the relay candidate yet, add it now + if let Some(pc) = peer_connection.as_mut() { + if !relay_candidate_added { + match add_relay_candidate(pc, addr, relay_local_addr) { + Ok(local_candidate_init) => { + relay_candidate_added = true; + println!( + "Added local Relay ICE candidate: {}", + local_candidate_init.candidate + ); + + // Send to browser via WebSocket + if let Some(ref mut ws) = ws_stream { + if let Ok(json) = + serde_json::to_string(&local_candidate_init) + { + info!( + "Sending local ICE candidate: {}", + local_candidate_init.candidate + ); + if let Err(e) = + ws.send(Message::Text(json.into())).await { - info!( - "Sending local ICE candidate: {}", - local_candidate_init.candidate + error!( + "Failed to send relay candidate to browser: {}", + e ); - if let Err(e) = - ws.send(Message::Text(json.into())).await - { - error!( - "Failed to send relay candidate to browser: {}", - e - ); - } } } } - Err(e) => error!("Failed to add relay candidate: {}", e), } + Err(e) => error!("Failed to add relay candidate: {}", e), } } } @@ -270,11 +271,11 @@ async fn run_main_loop( Event::AllocateError(_, err) => { error!("TURN allocation error: {}", err); } - Event::CreatePermissionResponse(tid, peer_addr) => { - if pending_permissions.remove(&tid).is_some() { - println!("CreatePermission for peer addr {} is granted", peer_addr); - granted_permissions.insert(peer_addr); - } + Event::CreatePermissionResponse(tid, peer_addr) + if pending_permissions.remove(&tid).is_some() => + { + println!("CreatePermission for peer addr {} is granted", peer_addr); + granted_permissions.insert(peer_addr); } Event::CreatePermissionError(_, err) => { error!("CreatePermission error: {}", err); @@ -349,21 +350,20 @@ async fn run_main_loop( println!("Peer Connection connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!( - "{} - Data channel '{}'-'{}' open", - chrono::Local::now().format("%H:%M:%S"), - dc.label(), - dc.id() - ); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!( + "{} - Data channel '{}'-'{}' open", + chrono::Local::now().format("%H:%M:%S"), + dc.label(), + dc.id() + ); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -378,6 +378,7 @@ async fn run_main_loop( .unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/trickle-ice-srflx/trickle-ice-srflx.rs b/examples/trickle-ice-srflx/trickle-ice-srflx.rs index d4e4673b..261d9424 100644 --- a/examples/trickle-ice-srflx/trickle-ice-srflx.rs +++ b/examples/trickle-ice-srflx/trickle-ice-srflx.rs @@ -193,21 +193,20 @@ async fn run_main_loop() -> Result<()> { println!("Peer Connection connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!( - "{} - Data channel '{}'-'{}' open", - chrono::Local::now().format("%H:%M:%S"), - dc.label(), - dc.id() - ); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!( + "{} - Data channel '{}'-'{}' open", + chrono::Local::now().format("%H:%M:%S"), + dc.label(), + dc.id() + ); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -222,6 +221,7 @@ async fn run_main_loop() -> Result<()> { .unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/examples/trickle-ice/trickle-ice.rs b/examples/trickle-ice/trickle-ice.rs index 9e18c8a3..1825ee17 100644 --- a/examples/trickle-ice/trickle-ice.rs +++ b/examples/trickle-ice/trickle-ice.rs @@ -21,6 +21,7 @@ use ice::candidate::candidate_host::CandidateHostConfig; use ice::candidate::candidate_relay::CandidateRelayConfig; use ice::candidate::candidate_server_reflexive::CandidateServerReflexiveConfig; use log::{error, info, trace, warn}; +use rtc::crypto; use rtc::peer_connection::configuration::RTCConfigurationBuilder; use rtc::peer_connection::configuration::setting_engine::SettingEngine; use rtc::peer_connection::event::RTCDataChannelEvent; @@ -281,7 +282,7 @@ async fn run_main_loop(cli: Cli) -> Result<()> { rto_in_ms: 0, }; - let mut client = TurnClient::new(cfg)?; + let mut client = TurnClient::new(cfg, crypto::default_provider()?)?; let tid = client.allocate()?; allocate_tid = Some(tid); turn_client = Some(client); @@ -387,22 +388,19 @@ async fn run_main_loop(cli: Cli) -> Result<()> { TurnEvent::TransactionTimeout(_) => { error!("TURN transaction timeout"); } - TurnEvent::AllocateResponse(tid, addr) => { - if Some(tid) == allocate_tid { - println!("TURN allocation successful, relay address: {}", addr); - relay_addr = Some(addr); - - // Add relay candidate if peer connection exists - if let Some(pc) = peer_connection.as_mut() { - if !relay_candidate_added { - if let Err(e) = - add_relay_candidate(pc, addr, local_addr, &mut ws_stream) - .await - { - error!("Failed to add relay candidate: {}", e); - } else { - relay_candidate_added = true; - } + TurnEvent::AllocateResponse(tid, addr) if Some(tid) == allocate_tid => { + println!("TURN allocation successful, relay address: {}", addr); + relay_addr = Some(addr); + + // Add relay candidate if peer connection exists + if let Some(pc) = peer_connection.as_mut() { + if !relay_candidate_added { + if let Err(e) = + add_relay_candidate(pc, addr, local_addr, &mut ws_stream).await + { + error!("Failed to add relay candidate: {}", e); + } else { + relay_candidate_added = true; } } } @@ -410,11 +408,11 @@ async fn run_main_loop(cli: Cli) -> Result<()> { TurnEvent::AllocateError(_, err) => { error!("TURN allocation error: {}", err); } - TurnEvent::CreatePermissionResponse(tid, peer_addr) => { - if pending_permissions.remove(&tid).is_some() { - println!("CreatePermission for peer addr {} is granted", peer_addr); - granted_permissions.insert(peer_addr); - } + TurnEvent::CreatePermissionResponse(tid, peer_addr) + if pending_permissions.remove(&tid).is_some() => + { + println!("CreatePermission for peer addr {} is granted", peer_addr); + granted_permissions.insert(peer_addr); } TurnEvent::CreatePermissionError(_, err) => { error!("CreatePermission error: {}", err); @@ -498,21 +496,20 @@ async fn run_main_loop(cli: Cli) -> Result<()> { println!("Peer Connection connected!"); } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = pc.data_channel(channel_id) { - println!( - "{} - Data channel '{}'-'{}' open", - chrono::Local::now().format("%H:%M:%S"), - dc.label(), - dc.id() - ); - data_channel_id = Some(channel_id); - last_send = Instant::now(); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen( + channel_id, + )) => { + if let Some(dc) = pc.data_channel(channel_id) { + println!( + "{} - Data channel '{}'-'{}' open", + chrono::Local::now().format("%H:%M:%S"), + dc.label(), + dc.id() + ); + data_channel_id = Some(channel_id); + last_send = Instant::now(); } - _ => {} - }, + } _ => {} } } @@ -527,6 +524,7 @@ async fn run_main_loop(cli: Cli) -> Result<()> { .unwrap_or_default(); println!("Message from DataChannel: '{}'", msg_str); } + _ => {} } } diff --git a/rtc-crypto/Cargo.toml b/rtc-crypto/Cargo.toml new file mode 100644 index 00000000..6fa5fec1 --- /dev/null +++ b/rtc-crypto/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "rtc-crypto" +version.workspace = true +authors.workspace = true +edition.workspace = true +description = "Provider-neutral cryptography for the webrtc-rs RTC stack" +license.workspace = true +documentation = "https://docs.rs/rtc-crypto" +homepage.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true +readme = "README.md" + +[features] +default = ["crypto-ring"] +crypto-ring = ["dep:ring", "dep:aes", "dep:ctr", "dep:ccm", "dep:md-5", "dep:hmac", "dep:sha1"] +crypto-aws-lc-rs = ["dep:aws-lc-rs", "dep:aes", "dep:ctr", "dep:ccm", "dep:md-5", "dep:hmac", "dep:sha1"] +test-support = [] + +[dependencies] +aes = { workspace = true, optional = true } +ctr = { workspace = true, optional = true } +rand.workspace = true +ccm = { workspace = true, optional = true } +md-5 = { workspace = true, optional = true } +hmac = { workspace = true, optional = true } +sha1 = { workspace = true, optional = true } +subtle.workspace = true +thiserror.workspace = true +zeroize.workspace = true +ring = { workspace = true, optional = true } +aws-lc-rs = { workspace = true, optional = true } + +[dev-dependencies] +pem.workspace = true diff --git a/rtc-crypto/README.md b/rtc-crypto/README.md new file mode 100644 index 00000000..e74880b2 --- /dev/null +++ b/rtc-crypto/README.md @@ -0,0 +1,19 @@ +# rtc-crypto + +`rtc-crypto` provides the provider-neutral cryptographic operations used by the webrtc-rs RTC stack. Applications can use a built-in Ring or AWS-LC-RS provider, or implement the public provider traits without registration or sealing. + +The crate owns primitives and opaque keyed state. DTLS, SRTP, STUN, certificate policy, and wire-format composition remain in their protocol crates. + +Enable `test-support` to run the reusable provider conformance suite from a downstream provider's tests. + +```rust +#[test] +fn provider_conforms() { + let provider = MyProvider::new(); + rtc_crypto::conformance::assert_provider(&provider); +} +``` + +Partial providers can invoke the public operation-family helpers that match their advertised capabilities. The feature adds no built-in backend, so a downstream provider can test with `default-features = false, features = ["test-support"]`. + +The `ring` and `aws-lc-rs` features are additive. Ring is the default when enabled, including builds that enable both backends. With neither backend enabled, applications construct their own `Arc` and `default_provider()` returns `CryptoError::NoDefaultProvider`. diff --git a/rtc-crypto/src/algorithm.rs b/rtc-crypto/src/algorithm.rs new file mode 100644 index 00000000..704e1a29 --- /dev/null +++ b/rtc-crypto/src/algorithm.rs @@ -0,0 +1,155 @@ +/// Algorithms accepted by [`crate::RTCCrypto::hash`]. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum HashAlgorithm { + /// MD5, retained only for STUN long-term credential derivation. + Md5, + /// SHA-256. + Sha256, +} + +/// Algorithms accepted by the HMAC operations. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum HmacAlgorithm { + /// HMAC-SHA1. + Sha1, + /// HMAC-SHA256. + Sha256, +} + +impl HmacAlgorithm { + /// Returns the native tag length in bytes. + #[must_use] + pub const fn output_len(self) -> usize { + match self { + Self::Sha1 => 20, + Self::Sha256 => 32, + } + } +} + +/// Authenticated-encryption algorithms. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum AeadAlgorithm { + /// AES-128-GCM. + Aes128Gcm, + /// AES-256-GCM. + Aes256Gcm, + /// AES-128-CCM with a 16-byte tag. + Aes128Ccm, + /// AES-128-CCM with an 8-byte tag. + Aes128Ccm8, + /// ChaCha20-Poly1305. + ChaCha20Poly1305, +} + +/// Stream-cipher algorithms. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum StreamCipherAlgorithm { + /// AES-128 in counter mode. + Aes128Ctr, + /// AES-256 in counter mode. + Aes256Ctr, +} + +/// Single-block encryption algorithms. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BlockCipherAlgorithm { + /// AES-128. + Aes128, + /// AES-256. + Aes256, +} + +/// CBC algorithms. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CbcAlgorithm { + /// AES-256-CBC. + Aes256Cbc, +} + +/// Ephemeral key-agreement algorithms. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum KeyExchangeAlgorithm { + /// ECDH over NIST P-256. + P256, + /// ECDH over NIST P-384. + P384, + /// X25519. + X25519, +} + +/// Signature schemes currently used by DTLS. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SignatureScheme { + /// Ed25519. + Ed25519, + /// ECDSA P-256 with SHA-256 and ASN.1 DER signatures. + EcdsaP256Sha256, + /// ECDSA P-384 with SHA-384 and ASN.1 DER signatures. + EcdsaP384Sha384, + /// RSA PKCS#1 v1.5 with SHA-1, for legacy verification only. + RsaPkcs1Sha1, + /// RSA PKCS#1 v1.5 with SHA-256. + RsaPkcs1Sha256, + /// RSA PKCS#1 v1.5 with SHA-384. + RsaPkcs1Sha384, + /// RSA PKCS#1 v1.5 with SHA-512. + RsaPkcs1Sha512, +} + +/// The encoding of public-key bytes. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PublicKeyEncoding { + /// Complete DER-encoded SubjectPublicKeyInfo. + SubjectPublicKeyInfoDer, + /// SEC1 uncompressed elliptic-curve point. + EcUncompressedPoint, + /// Raw 32-byte Ed25519 public key. + Ed25519Raw, + /// PKCS#1 DER `RSAPublicKey`. + RsaPkcs1Der, +} + +/// Borrowed public-key bytes with an explicit encoding. +#[derive(Debug, Clone, Copy)] +pub struct PublicKey<'a> { + /// Encoding of `bytes`. + pub encoding: PublicKeyEncoding, + /// Encoded public key. + pub bytes: &'a [u8], +} + +/// A provider capability identifier. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CryptoAlgorithm { + /// Stateless hash operation. + Hash(HashAlgorithm), + /// HMAC generation and verification. + Hmac(HmacAlgorithm), + /// Authenticated encryption. + Aead(AeadAlgorithm), + /// Stream encryption. + StreamCipher(StreamCipherAlgorithm), + /// Single-block encryption. + BlockCipher(BlockCipherAlgorithm), + /// CBC encryption and decryption. + Cbc(CbcAlgorithm), + /// Ephemeral key agreement. + KeyExchange(KeyExchangeAlgorithm), + /// Signature verification. + Signature(SignatureScheme), + /// Signing-key generation. + SigningKeyGeneration(SignatureScheme), + /// PKCS#8 signing-key import. + SigningKeyImport(SignatureScheme), +} diff --git a/rtc-crypto/src/common.rs b/rtc-crypto/src/common.rs new file mode 100644 index 00000000..f7a1776b --- /dev/null +++ b/rtc-crypto/src/common.rs @@ -0,0 +1,372 @@ +use crate::{ + AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, CryptoError, + SecretVec, StreamCipher, StreamCipherAlgorithm, +}; +use aes::cipher::generic_array::GenericArray; +use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, KeyIvInit}; +use aes::{Aes128, Aes256}; +use ccm::Ccm; +use ccm::aead::AeadInPlace; +use ccm::consts::{U8, U12, U16}; +use ctr::cipher::StreamCipher as CtrStreamCipher; +use md5::{Digest, Md5}; + +const AES_BLOCK_LEN: usize = 16; +const CCM_NONCE_LEN: usize = 12; + +type Aes128Ccm = Ccm; +type Aes128Ccm8 = Ccm; + +// SRTP counter mode (RFC 3711 section 4.1.1) is AES-CTR with a big-endian 128-bit counter. +type Aes128Ctr = ctr::Ctr128BE; +type Aes256Ctr = ctr::Ctr128BE; + +/// Fills `output` from the thread-local CSPRNG shared by the built-in providers. +/// +/// This is a ChaCha-based generator seeded from the operating system and periodically reseeded, +/// not a per-call OS read. `ring::rand::SystemRandom` and `aws_lc_rs::rand::SystemRandom` reach +/// the OS on every call — measured at ~829 ns and ~2196 ns for an 8-byte fill, against ~8 ns +/// here. DTLS generates a GCM explicit nonce and a CBC record IV *per record*, so the difference +/// showed up as a 3-8x regression on DTLS encryption; see `rtc-dtls/benches/README.md`. +/// +/// This restores what the pre-provider code did (`rand::rng()`), and matches how BoringSSL and +/// OpenSSL buffer internally. A deployment that requires every byte of entropy to come from a +/// validated module supplies its own [`RTCRandom`](crate::RTCRandom) implementation; that is what +/// the trait is for. +pub(crate) fn fill_random(output: &mut [u8]) -> Result<(), CryptoError> { + rand::fill(output); + Ok(()) +} + +pub(crate) fn md5(data: &[u8]) -> Vec { + Md5::digest(data).to_vec() +} + +pub(crate) fn block_encrypt( + algorithm: BlockCipherAlgorithm, + key: &[u8], + block: &mut [u8], +) -> Result<(), CryptoError> { + check_len(AES_BLOCK_LEN, block.len(), LengthKind::Output)?; + match algorithm { + BlockCipherAlgorithm::Aes128 => { + check_key_len(16, key.len())?; + Aes128::new_from_slice(key) + .map_err(|_| invalid_key(16, key.len()))? + .encrypt_block(GenericArray::from_mut_slice(block)); + } + BlockCipherAlgorithm::Aes256 => { + check_key_len(32, key.len())?; + Aes256::new_from_slice(key) + .map_err(|_| invalid_key(32, key.len()))? + .encrypt_block(GenericArray::from_mut_slice(block)); + } + } + Ok(()) +} + +pub(crate) fn new_stream_cipher( + algorithm: StreamCipherAlgorithm, + key: &[u8], +) -> Result, CryptoError> { + let bits = match algorithm { + StreamCipherAlgorithm::Aes128Ctr => { + check_key_len(16, key.len())?; + AesKeyBits::Aes128 + } + StreamCipherAlgorithm::Aes256Ctr => { + check_key_len(32, key.len())?; + AesKeyBits::Aes256 + } + }; + Ok(Box::new(AesCtr { + key: SecretVec::new(key.to_vec()), + bits, + })) +} + +pub(crate) fn new_cbc( + algorithm: CbcAlgorithm, + key: &[u8], +) -> Result, CryptoError> { + match algorithm { + CbcAlgorithm::Aes256Cbc => Ok(Box::new(AesCbc { + key: ExpandedAesKey::new_256(key)?, + })), + } +} + +pub(crate) fn new_ccm( + algorithm: AeadAlgorithm, + key: &[u8], +) -> Result, CryptoError> { + check_key_len(16, key.len())?; + let cipher = match algorithm { + AeadAlgorithm::Aes128Ccm => { + CommonCcm::Full(Aes128Ccm::new_from_slice(key).map_err(|_| invalid_key(16, key.len()))?) + } + AeadAlgorithm::Aes128Ccm8 => CommonCcm::Short( + Aes128Ccm8::new_from_slice(key).map_err(|_| invalid_key(16, key.len()))?, + ), + _ => { + return Err(CryptoError::UnsupportedAlgorithm( + crate::CryptoAlgorithm::Aead(algorithm), + )); + } + }; + Ok(Box::new(cipher)) +} + +// Stores an expanded key inside an already boxed cipher object, avoiding another allocation in +// every constructed state object. Only AES-256 is needed: `CbcAlgorithm` has a single variant, +// and CTR now goes through the `ctr` crate. +#[allow(clippy::large_enum_variant)] +enum ExpandedAesKey { + Aes256(Aes256), +} + +impl ExpandedAesKey { + fn new_256(key: &[u8]) -> Result { + check_key_len(32, key.len())?; + Ok(Self::Aes256( + Aes256::new_from_slice(key).map_err(|_| invalid_key(32, key.len()))?, + )) + } + + fn encrypt(&self, block: &mut [u8; AES_BLOCK_LEN]) { + match self { + Self::Aes256(cipher) => cipher.encrypt_block(GenericArray::from_mut_slice(block)), + } + } + + fn decrypt(&self, block: &mut [u8; AES_BLOCK_LEN]) { + match self { + Self::Aes256(cipher) => cipher.decrypt_block(GenericArray::from_mut_slice(block)), + } + } +} + +/// AES counter mode. +/// +/// Delegates to the `ctr` crate rather than driving `encrypt_block` once per 16-byte block, which +/// defeats the batching that lets AES-NI / ARMv8 crypto instructions pipeline. Measured ~9-10% +/// faster on a 1200-byte SRTP payload and ~8-10% slower on a two-block RTCP packet, where the +/// per-call setup dominates; RTP traffic dominates in practice. See +/// `rtc-srtp/benches/README.md`. +/// +/// The key is retained rather than pre-expanded because `ctr::Ctr128BE` owns its own cipher +/// state and is constructed per call. It is held in a [`SecretVec`] so it is zeroized on drop. +struct AesCtr { + key: SecretVec, + bits: AesKeyBits, +} + +#[derive(Clone, Copy)] +enum AesKeyBits { + Aes128, + Aes256, +} + +impl StreamCipher for AesCtr { + fn apply_keystream(&mut self, iv: &[u8], data: &mut [u8]) -> Result<(), CryptoError> { + check_nonce_len(AES_BLOCK_LEN, iv.len())?; + let nonce = GenericArray::from_slice(iv); + let key = self.key.as_ref(); + match self.bits { + AesKeyBits::Aes128 => { + let mut stream = Aes128Ctr::new(GenericArray::from_slice(key), nonce); + stream.apply_keystream(data); + } + AesKeyBits::Aes256 => { + let mut stream = Aes256Ctr::new(GenericArray::from_slice(key), nonce); + stream.apply_keystream(data); + } + } + Ok(()) + } +} + +struct AesCbc { + key: ExpandedAesKey, +} + +impl CbcCipher for AesCbc { + fn block_len(&self) -> usize { + AES_BLOCK_LEN + } + + fn encrypt_blocks(&mut self, iv: &[u8], blocks: &mut [u8]) -> Result<(), CryptoError> { + check_nonce_len(AES_BLOCK_LEN, iv.len())?; + check_blocks(blocks)?; + let mut previous: [u8; AES_BLOCK_LEN] = iv + .try_into() + .map_err(|_| invalid_nonce(AES_BLOCK_LEN, iv.len()))?; + + for chunk in blocks.chunks_exact_mut(AES_BLOCK_LEN) { + for (byte, prior) in chunk.iter_mut().zip(previous) { + *byte ^= prior; + } + let block: &mut [u8; AES_BLOCK_LEN] = chunk.try_into().expect("exact AES block"); + self.key.encrypt(block); + previous.copy_from_slice(block); + } + Ok(()) + } + + fn decrypt_blocks(&mut self, iv: &[u8], blocks: &mut [u8]) -> Result<(), CryptoError> { + check_nonce_len(AES_BLOCK_LEN, iv.len())?; + check_blocks(blocks)?; + let mut previous: [u8; AES_BLOCK_LEN] = iv + .try_into() + .map_err(|_| invalid_nonce(AES_BLOCK_LEN, iv.len()))?; + + for chunk in blocks.chunks_exact_mut(AES_BLOCK_LEN) { + let ciphertext: [u8; AES_BLOCK_LEN] = chunk.try_into().expect("exact AES block"); + let block: &mut [u8; AES_BLOCK_LEN] = chunk.try_into().expect("exact AES block"); + self.key.decrypt(block); + for (byte, prior) in block.iter_mut().zip(previous) { + *byte ^= prior; + } + previous = ciphertext; + } + Ok(()) + } +} + +enum CommonCcm { + Full(Aes128Ccm), + Short(Aes128Ccm8), +} + +impl AeadCipher for CommonCcm { + fn tag_len(&self) -> usize { + match self { + Self::Full(_) => 16, + Self::Short(_) => 8, + } + } + + fn seal_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + plaintext_and_ciphertext: &mut [u8], + tag_out: &mut [u8], + ) -> Result<(), CryptoError> { + check_nonce_len(CCM_NONCE_LEN, nonce.len())?; + check_tag_len(self.tag_len(), tag_out.len())?; + match self { + Self::Full(cipher) => { + let tag = cipher + .encrypt_in_place_detached( + GenericArray::from_slice(nonce), + aad, + plaintext_and_ciphertext, + ) + .map_err(|_| CryptoError::AuthenticationFailed)?; + tag_out.copy_from_slice(&tag); + } + Self::Short(cipher) => { + let tag = cipher + .encrypt_in_place_detached( + GenericArray::from_slice(nonce), + aad, + plaintext_and_ciphertext, + ) + .map_err(|_| CryptoError::AuthenticationFailed)?; + tag_out.copy_from_slice(&tag); + } + } + Ok(()) + } + + fn open_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + ciphertext_and_plaintext: &mut [u8], + tag: &[u8], + ) -> Result<(), CryptoError> { + check_nonce_len(CCM_NONCE_LEN, nonce.len())?; + check_tag_len(self.tag_len(), tag.len())?; + match self { + Self::Full(cipher) => cipher + .decrypt_in_place_detached( + GenericArray::from_slice(nonce), + aad, + ciphertext_and_plaintext, + GenericArray::from_slice(tag), + ) + .map_err(|_| CryptoError::AuthenticationFailed), + Self::Short(cipher) => cipher + .decrypt_in_place_detached( + GenericArray::from_slice(nonce), + aad, + ciphertext_and_plaintext, + GenericArray::from_slice(tag), + ) + .map_err(|_| CryptoError::AuthenticationFailed), + } + } +} + +fn check_blocks(blocks: &[u8]) -> Result<(), CryptoError> { + if blocks.is_empty() || !blocks.len().is_multiple_of(AES_BLOCK_LEN) { + return Err(CryptoError::OutputTooSmall { + required: blocks + .len() + .next_multiple_of(AES_BLOCK_LEN) + .max(AES_BLOCK_LEN), + actual: blocks.len(), + }); + } + Ok(()) +} + +enum LengthKind { + Output, +} + +fn check_len(expected: usize, actual: usize, kind: LengthKind) -> Result<(), CryptoError> { + if expected == actual { + return Ok(()); + } + match kind { + LengthKind::Output => Err(CryptoError::OutputTooSmall { + required: expected, + actual, + }), + } +} + +pub(crate) fn check_key_len(expected: usize, actual: usize) -> Result<(), CryptoError> { + if expected == actual { + Ok(()) + } else { + Err(invalid_key(expected, actual)) + } +} + +pub(crate) fn check_nonce_len(expected: usize, actual: usize) -> Result<(), CryptoError> { + if expected == actual { + Ok(()) + } else { + Err(invalid_nonce(expected, actual)) + } +} + +pub(crate) fn check_tag_len(expected: usize, actual: usize) -> Result<(), CryptoError> { + if expected == actual { + Ok(()) + } else { + Err(CryptoError::InvalidTagLength { expected, actual }) + } +} + +fn invalid_key(expected: usize, actual: usize) -> CryptoError { + CryptoError::InvalidKeyLength { expected, actual } +} + +fn invalid_nonce(expected: usize, actual: usize) -> CryptoError { + CryptoError::InvalidNonceLength { expected, actual } +} diff --git a/rtc-crypto/src/conformance.rs b/rtc-crypto/src/conformance.rs new file mode 100644 index 00000000..3612e188 --- /dev/null +++ b/rtc-crypto/src/conformance.rs @@ -0,0 +1,542 @@ +//! Reusable conformance assertions for built-in and application-provided crypto providers. + +use crate::{ + AeadAlgorithm, BlockCipherAlgorithm, CbcAlgorithm, CryptoAlgorithm, CryptoError, HashAlgorithm, + HmacAlgorithm, KeyExchangeAlgorithm, PublicKey, PublicKeyEncoding, RTCCrypto, + RTCCryptoProvider, SignatureScheme, StreamCipherAlgorithm, +}; + +/// Exercises the complete initial RTC crypto contract implemented by the built-in providers. +/// +/// This helper panics on a contract violation so provider authors can call it directly from a +/// normal `#[test]` function. +pub fn assert_provider(provider: &dyn RTCCryptoProvider) { + assert_basic_capabilities(provider.crypto()); + assert_hashes_and_hmac(provider.crypto()); + assert_block_and_stream_ciphers(provider.crypto()); + assert_cbc(provider.crypto()); + assert_aead(provider.crypto()); + assert_key_exchange(provider.crypto()); + assert_signatures(provider.crypto()); + assert_random(provider); +} + +fn assert_basic_capabilities(crypto: &dyn RTCCrypto) { + let capabilities = [ + CryptoAlgorithm::Hash(HashAlgorithm::Md5), + CryptoAlgorithm::Hash(HashAlgorithm::Sha256), + CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1), + CryptoAlgorithm::Hmac(HmacAlgorithm::Sha256), + CryptoAlgorithm::BlockCipher(BlockCipherAlgorithm::Aes128), + CryptoAlgorithm::BlockCipher(BlockCipherAlgorithm::Aes256), + CryptoAlgorithm::StreamCipher(StreamCipherAlgorithm::Aes128Ctr), + CryptoAlgorithm::StreamCipher(StreamCipherAlgorithm::Aes256Ctr), + CryptoAlgorithm::Cbc(CbcAlgorithm::Aes256Cbc), + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Gcm), + CryptoAlgorithm::Aead(AeadAlgorithm::Aes256Gcm), + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Ccm), + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Ccm8), + CryptoAlgorithm::Aead(AeadAlgorithm::ChaCha20Poly1305), + CryptoAlgorithm::KeyExchange(KeyExchangeAlgorithm::P256), + CryptoAlgorithm::KeyExchange(KeyExchangeAlgorithm::P384), + CryptoAlgorithm::KeyExchange(KeyExchangeAlgorithm::X25519), + CryptoAlgorithm::Signature(SignatureScheme::Ed25519), + CryptoAlgorithm::Signature(SignatureScheme::EcdsaP256Sha256), + CryptoAlgorithm::Signature(SignatureScheme::EcdsaP384Sha384), + CryptoAlgorithm::Signature(SignatureScheme::RsaPkcs1Sha1), + CryptoAlgorithm::Signature(SignatureScheme::RsaPkcs1Sha256), + CryptoAlgorithm::Signature(SignatureScheme::RsaPkcs1Sha384), + CryptoAlgorithm::Signature(SignatureScheme::RsaPkcs1Sha512), + CryptoAlgorithm::SigningKeyGeneration(SignatureScheme::Ed25519), + CryptoAlgorithm::SigningKeyGeneration(SignatureScheme::EcdsaP256Sha256), + CryptoAlgorithm::SigningKeyImport(SignatureScheme::Ed25519), + CryptoAlgorithm::SigningKeyImport(SignatureScheme::EcdsaP256Sha256), + CryptoAlgorithm::SigningKeyImport(SignatureScheme::RsaPkcs1Sha256), + ]; + for capability in capabilities { + assert!( + crypto.supports(capability), + "missing capability: {capability:?}" + ); + } + assert!(!crypto.supports(CryptoAlgorithm::SigningKeyGeneration( + SignatureScheme::RsaPkcs1Sha256 + ))); + assert!(matches!( + crypto.generate_signing_key(SignatureScheme::RsaPkcs1Sha256), + Err(CryptoError::UnsupportedAlgorithm(_)) + )); + for scheme in [ + SignatureScheme::Ed25519, + SignatureScheme::EcdsaP256Sha256, + SignatureScheme::RsaPkcs1Sha256, + ] { + assert!(matches!( + crypto.import_signing_key(scheme, b"not a PKCS#8 key"), + Err(CryptoError::InvalidPrivateKey) + )); + } +} + +/// Checks hash and HMAC known-answer vectors and their error contract. +pub fn assert_hashes_and_hmac(crypto: &dyn RTCCrypto) { + // RFC 1321, FIPS 180-4, RFC 2202, and RFC 4231 known-answer vectors. + assert_eq!( + crypto.hash(HashAlgorithm::Md5, b"abc").unwrap(), + bytes("900150983cd24fb0d6963f7d28e17f72") + ); + assert_eq!( + crypto.hash(HashAlgorithm::Sha256, b"abc").unwrap(), + bytes("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad") + ); + + let key = [0x0b; 20]; + let mut sha1 = [0; 20]; + let mut sha1_mac = crypto.new_hmac(HmacAlgorithm::Sha1, &key).unwrap(); + assert_eq!(sha1_mac.output_len(), 20); + sha1_mac.sign(&[b"Hi ", b"There"], &mut sha1).unwrap(); + assert_eq!( + sha1.as_slice(), + bytes("b617318655057264e28bc0b6fb378c8ef146be00") + ); + + // A keyed MAC is reusable: the second message must not be affected by the first. + let mut repeat = [0; 20]; + sha1_mac.sign(&[b"Hi ", b"There"], &mut repeat).unwrap(); + assert_eq!(repeat, sha1, "a Mac must produce the same tag when reused"); + + let mut sha256 = [0; 32]; + let mut sha256_mac = crypto.new_hmac(HmacAlgorithm::Sha256, &key).unwrap(); + sha256_mac.sign(&[b"Hi ", b"There"], &mut sha256).unwrap(); + assert_eq!( + sha256.as_slice(), + bytes("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7") + ); + + // Splitting the input across slices must not change the tag. + let mut joined = [0; 32]; + sha256_mac.sign(&[b"Hi There"], &mut joined).unwrap(); + assert_eq!(joined, sha256, "slice boundaries must not affect the tag"); + + sha256_mac.verify(&[b"Hi There"], &sha256).unwrap(); + let mut bad_tag = sha256; + bad_tag[0] ^= 1; + assert_eq!( + sha256_mac.verify(&[b"Hi There"], &bad_tag), + Err(CryptoError::AuthenticationFailed) + ); + assert!(matches!( + sha256_mac.sign(&[b"x"], &mut [0; 31]), + Err(CryptoError::InvalidTagLength { .. }) + )); +} + +/// Checks AES block and stream-cipher known-answer vectors and malformed inputs. +pub fn assert_block_and_stream_ciphers(crypto: &dyn RTCCrypto) { + // FIPS 197 and NIST SP 800-38A known-answer vectors. + let mut block = bytes("00112233445566778899aabbccddeeff"); + crypto + .block_encrypt( + BlockCipherAlgorithm::Aes128, + &bytes("000102030405060708090a0b0c0d0e0f"), + &mut block, + ) + .unwrap(); + assert_eq!(block, bytes("69c4e0d86a7b0430d8cdb78070b4c55a")); + + let mut block = bytes("00112233445566778899aabbccddeeff"); + crypto + .block_encrypt( + BlockCipherAlgorithm::Aes256, + &bytes("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"), + &mut block, + ) + .unwrap(); + assert_eq!(block, bytes("8ea2b7ca516745bfeafc49904b496089")); + + let key = bytes("2b7e151628aed2a6abf7158809cf4f3c"); + let iv = bytes("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); + let plaintext = bytes("6bc1bee22e409f96e93d7e117393172a"); + let mut encrypted = plaintext.clone(); + crypto + .new_stream_cipher(StreamCipherAlgorithm::Aes128Ctr, &key) + .unwrap() + .apply_keystream(&iv, &mut encrypted) + .unwrap(); + assert_eq!(encrypted, bytes("874d6191b620e3261bef6864990db6ce")); + crypto + .new_stream_cipher(StreamCipherAlgorithm::Aes128Ctr, &key) + .unwrap() + .apply_keystream(&iv, &mut encrypted) + .unwrap(); + assert_eq!(encrypted, plaintext); + + let key = bytes("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"); + let mut encrypted = plaintext.clone(); + crypto + .new_stream_cipher(StreamCipherAlgorithm::Aes256Ctr, &key) + .unwrap() + .apply_keystream(&iv, &mut encrypted) + .unwrap(); + assert_eq!(encrypted, bytes("601ec313775789a5b7a7f504bbf3d228")); + + assert!(matches!( + crypto.block_encrypt(BlockCipherAlgorithm::Aes128, &[0; 15], &mut [0; 16]), + Err(CryptoError::InvalidKeyLength { .. }) + )); + assert!(matches!( + crypto.block_encrypt(BlockCipherAlgorithm::Aes128, &[0; 16], &mut [0; 15]), + Err(CryptoError::OutputTooSmall { .. }) + )); + assert!(matches!( + crypto.new_stream_cipher(StreamCipherAlgorithm::Aes128Ctr, &[0; 15]), + Err(CryptoError::InvalidKeyLength { .. }) + )); + let mut stream = crypto + .new_stream_cipher(StreamCipherAlgorithm::Aes128Ctr, &[0; 16]) + .unwrap(); + assert!(matches!( + stream.apply_keystream(&[0; 15], &mut [0; 1]), + Err(CryptoError::InvalidNonceLength { .. }) + )); +} + +/// Checks AES-CBC known-answer vectors and malformed inputs. +pub fn assert_cbc(crypto: &dyn RTCCrypto) { + // NIST SP 800-38A F.2.5. + let key = bytes("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4"); + let iv = bytes("000102030405060708090a0b0c0d0e0f"); + let plaintext = bytes("6bc1bee22e409f96e93d7e117393172a"); + let mut blocks = plaintext.clone(); + let mut cipher = crypto.new_cbc(CbcAlgorithm::Aes256Cbc, &key).unwrap(); + assert_eq!(cipher.block_len(), 16); + cipher.encrypt_blocks(&iv, &mut blocks).unwrap(); + assert_eq!(blocks, bytes("f58c4c04d6e5f1ba779eabfb5f7bfbd6")); + cipher.decrypt_blocks(&iv, &mut blocks).unwrap(); + assert_eq!(blocks, plaintext); + + assert!(matches!( + crypto.new_cbc(CbcAlgorithm::Aes256Cbc, &[0; 31]), + Err(CryptoError::InvalidKeyLength { .. }) + )); + assert!(matches!( + cipher.encrypt_blocks(&[0; 15], &mut [0; 16]), + Err(CryptoError::InvalidNonceLength { .. }) + )); + assert!(matches!( + cipher.encrypt_blocks(&[0; 16], &mut []), + Err(CryptoError::OutputTooSmall { .. }) + )); + assert!(matches!( + cipher.decrypt_blocks(&[0; 16], &mut [0; 15]), + Err(CryptoError::OutputTooSmall { .. }) + )); +} + +/// Checks AEAD known-answer vectors, round trips, authentication failures, and malformed sizes. +pub fn assert_aead(crypto: &dyn RTCCrypto) { + // NIST SP 800-38D, NIST SP 800-38C, and RFC 8439 known-answer vectors. + let mut gcm = crypto.new_aead(AeadAlgorithm::Aes128Gcm, &[0; 16]).unwrap(); + let mut block = vec![0; 16]; + let mut tag = vec![0; gcm.tag_len()]; + gcm.seal_in_place(&[0; 12], &[], &mut block, &mut tag) + .unwrap(); + assert_eq!(block, bytes("0388dace60b6a392f328c2b971b2fe78")); + assert_eq!(tag, bytes("ab6e47d42cec13bdf53a67b21257bddf")); + gcm.open_in_place(&[0; 12], &[], &mut block, &tag).unwrap(); + assert_eq!(block, vec![0; 16]); + + let mut gcm = crypto.new_aead(AeadAlgorithm::Aes256Gcm, &[0; 32]).unwrap(); + let mut block = vec![0; 16]; + let mut tag = vec![0; gcm.tag_len()]; + gcm.seal_in_place(&[0; 12], &[], &mut block, &mut tag) + .unwrap(); + assert_eq!(block, bytes("cea7403d4d606b6e074ec5d3baf39d18")); + assert_eq!(tag, bytes("d0d1c8a799996bf0265b98b5d48ab919")); + + let mut ccm8 = crypto + .new_aead( + AeadAlgorithm::Aes128Ccm8, + &bytes("404142434445464748494a4b4c4d4e4f"), + ) + .unwrap(); + let mut ccm8_buffer = bytes("202122232425262728292a2b2c2d2e2f3031323334353637"); + let mut ccm8_tag = vec![0; ccm8.tag_len()]; + ccm8.seal_in_place( + &bytes("101112131415161718191a1b"), + &bytes("000102030405060708090a0b0c0d0e0f10111213"), + &mut ccm8_buffer, + &mut ccm8_tag, + ) + .unwrap(); + assert_eq!( + ccm8_buffer, + bytes("e3b201a9f5b71a7a9b1ceaeccd97e70b6176aad9a4428aa5") + ); + assert_eq!(ccm8_tag, bytes("484392fbc1b09951")); + + let mut chacha = crypto + .new_aead( + AeadAlgorithm::ChaCha20Poly1305, + &bytes("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f"), + ) + .unwrap(); + let mut chacha_buffer = bytes(concat!( + "4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393", + "a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f722074", + "6865206675747572652c2073756e73637265656e20776f756c642062652069742e" + )); + let mut chacha_tag = vec![0; chacha.tag_len()]; + chacha + .seal_in_place( + &bytes("070000004041424344454647"), + &bytes("50515253c0c1c2c3c4c5c6c7"), + &mut chacha_buffer, + &mut chacha_tag, + ) + .unwrap(); + assert_eq!( + chacha_buffer, + bytes(concat!( + "d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca967128", + "2fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fa", + "b324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116" + )) + ); + assert_eq!(chacha_tag, bytes("1ae10b594f09e26a7e902ecbd0600691")); + assert!(matches!( + crypto.new_aead(AeadAlgorithm::Aes128Gcm, &[0; 15]), + Err(CryptoError::InvalidKeyLength { .. }) + )); + assert!(matches!( + gcm.seal_in_place(&[0; 11], &[], &mut [], &mut [0; 16]), + Err(CryptoError::InvalidNonceLength { .. }) + )); + assert!(matches!( + gcm.seal_in_place(&[0; 12], &[], &mut [], &mut [0; 15]), + Err(CryptoError::InvalidTagLength { .. }) + )); + + let cases = [ + (AeadAlgorithm::Aes256Gcm, 32), + (AeadAlgorithm::Aes128Ccm, 16), + (AeadAlgorithm::Aes128Ccm8, 16), + (AeadAlgorithm::ChaCha20Poly1305, 32), + ]; + for (algorithm, key_len) in cases { + let mut cipher = crypto.new_aead(algorithm, &vec![7; key_len]).unwrap(); + let plaintext = b"provider conformance".to_vec(); + let mut encrypted = plaintext.clone(); + let mut tag = vec![0; cipher.tag_len()]; + cipher + .seal_in_place(&[3; 12], b"aad", &mut encrypted, &mut tag) + .unwrap(); + assert_ne!(encrypted, plaintext); + cipher + .open_in_place(&[3; 12], b"aad", &mut encrypted, &tag) + .unwrap(); + assert_eq!(encrypted, plaintext); + + let mut ciphertext = plaintext.clone(); + let mut valid_tag = vec![0; cipher.tag_len()]; + cipher + .seal_in_place(&[3; 12], b"aad", &mut ciphertext, &mut valid_tag) + .unwrap(); + + let mut tampered = ciphertext.clone(); + tag[0] ^= 1; + assert_eq!( + cipher.open_in_place(&[3; 12], b"aad", &mut tampered, &tag), + Err(CryptoError::AuthenticationFailed) + ); + + let mut wrong_aad = ciphertext.clone(); + assert_eq!( + cipher.open_in_place(&[3; 12], b"bad", &mut wrong_aad, &valid_tag), + Err(CryptoError::AuthenticationFailed) + ); + + let mut changed_ciphertext = ciphertext.clone(); + changed_ciphertext[0] ^= 1; + assert_eq!( + cipher.open_in_place(&[3; 12], b"aad", &mut changed_ciphertext, &valid_tag), + Err(CryptoError::AuthenticationFailed) + ); + + let mut wrong_key_cipher = crypto.new_aead(algorithm, &vec![8; key_len]).unwrap(); + assert_eq!( + wrong_key_cipher.open_in_place(&[3; 12], b"aad", &mut ciphertext, &valid_tag), + Err(CryptoError::AuthenticationFailed) + ); + } +} + +/// Checks every supported one-shot key exchange and malformed peer keys. +pub fn assert_key_exchange(crypto: &dyn RTCCrypto) { + for algorithm in [ + KeyExchangeAlgorithm::P256, + KeyExchangeAlgorithm::P384, + KeyExchangeAlgorithm::X25519, + ] { + let left = crypto.start_key_exchange(algorithm).unwrap(); + let right = crypto.start_key_exchange(algorithm).unwrap(); + assert_eq!(left.algorithm(), algorithm); + let left_public = left.public_key().to_vec(); + let right_public = right.public_key().to_vec(); + let left_secret = left.complete(&right_public).unwrap(); + let right_secret = right.complete(&left_public).unwrap(); + assert_eq!(left_secret.as_ref(), right_secret.as_ref()); + assert!(!left_secret.is_empty()); + + let invalid = crypto.start_key_exchange(algorithm).unwrap(); + assert!(matches!( + invalid.complete(&[0; 1]), + Err(CryptoError::InvalidPublicKey) + )); + } +} + +/// Checks signing, verification, key import/export, and invalid signatures and encodings. +pub fn assert_signatures(crypto: &dyn RTCCrypto) { + for scheme in [SignatureScheme::Ed25519, SignatureScheme::EcdsaP256Sha256] { + let key = crypto.generate_signing_key(scheme).unwrap(); + let message = b"rtc-crypto provider conformance"; + let signature = key.sign(scheme, message).unwrap(); + crypto + .verify_signature(scheme, key.public_key(), message, &signature) + .unwrap(); + assert_eq!( + crypto.verify_signature(scheme, key.public_key(), b"changed", &signature), + Err(CryptoError::InvalidSignature) + ); + + let exported = key.to_pkcs8_der().unwrap().unwrap(); + let imported = crypto + .import_signing_key(scheme, exported.as_ref()) + .unwrap(); + let imported_signature = imported.sign(scheme, message).unwrap(); + crypto + .verify_signature(scheme, imported.public_key(), message, &imported_signature) + .unwrap(); + assert!(matches!( + imported.sign(SignatureScheme::RsaPkcs1Sha256, message), + Err(CryptoError::UnsupportedAlgorithm(_)) + )); + } + + assert_eq!( + crypto.verify_signature( + SignatureScheme::Ed25519, + PublicKey { + encoding: PublicKeyEncoding::SubjectPublicKeyInfoDer, + bytes: &[0; 32], + }, + b"message", + &[0; 64], + ), + Err(CryptoError::InvalidPublicKey) + ); + + assert_verification_only_schemes(crypto); +} + +fn assert_verification_only_schemes(crypto: &dyn RTCCrypto) { + let message = b"rtc-crypto verification vector"; + let p384_public_key = bytes(concat!( + "04c298b589fdd33f544610d13c277e0c703b2e3a72c0dfa2a81725e761614bd8c4", + "cb80ecf40bba853f37aec2f4e13c7b5d05e7be9231d651bd1dc2848050bcd19858", + "e448d27bf2418b350626a1f241c4914795c404aa35afab97e15e202296244a" + )); + let p384_signature = bytes(concat!( + "306502302c6f36a6a01282982213b037f73ec8f935e1fcf4dc63035824c2bcb6", + "aaa378f716d15f63df23e85d60f7d5e46c028ad1023100877055a3a8849e179", + "ad94da98dc5125f1e78852cf9017795087b90751b99985b989786d2a537f84b08cdf7243820c313" + )); + crypto + .verify_signature( + SignatureScheme::EcdsaP384Sha384, + PublicKey { + encoding: PublicKeyEncoding::EcUncompressedPoint, + bytes: &p384_public_key, + }, + message, + &p384_signature, + ) + .unwrap(); + + let rsa_public_key = bytes( + "3082010a0282010100b0f8c4868ce9bcd4d11632162c0376bb09dce2facadaa27a6d4ad01b217c0a29e036b1bfd0052254ec3d349383019d8fd2d5c895ac6790bcc6c4dfca7c26bbcf570ee3bdff70826f80e8254776d8e73b431e2bece3d7d515edd23ada88e2c7136ac796685df8a40ed38ce59eaefc2509e1b277a0363938eb8e998a3f9fd6f2ed011b969e48d89d76508904961f8d83756466eff0f1fd5e4621e8ff083fd0d6aa87b31e560c185a1862059ba0fa95f8125ac96bfc7e051c3996587dc1271bb3ebf49303314ee888ccc8441de13a59b0646d9375dfbcbd66c1435398e164bc4672fd63a2eef162a7c1ac7ddb3e9cdb68cfeacff01f036a557477b45172593727f50203010001", + ); + let rsa_vectors = [ + ( + SignatureScheme::RsaPkcs1Sha1, + "967636ce08cc1923f2b6fa792d2a7cd6521f4a793adb4a0f94cfc6543e483d5383a6b36dbcef3a5cfd5cc12c333ebc6a22f2d452cac61a352111247e26f5e13595f0b78a9e94be1ff7eb4ab60ef48fff3a0e8c1a70ea041f63413c7dc4a5219d2ddb8349058ce1b7d2c02eb98f285d589f858a18f6473a1d3af47de653d520ceba6825e1a1bd5a0065ef0e4d9ac5929bc1cc3ffab014081224db7e7787c67f1580913fb98a870c1355c86a33770d17f654f24dccd781f3bf8a7de29e00198a99dd2bf1a1298630da433982ae63cbf71c265abebdd003c1c2ee870f7f8ea96c465dedb158a1e2e5048763d308a69eefc91424ad0795e8320fa0e753673f657a45", + ), + ( + SignatureScheme::RsaPkcs1Sha256, + "19b21fd70c4dacec4c3cc6f6aac961e4ea0fb9b5d0ee3862cc35b60849388f2f461ab4697c59c25abb251f88b1de312ced6861ed90152c911356dff768cc4eafdadaa2f8d6b5d70c630d2739f1178fe5cbaff62cee343a20bb38404d35c58d247befa486dd5a3c69affdad5bdecaab876799dd297bd9cc4700e099b92d18d8ad78b64f570e4a398b0b8e9baf9a1b01aaa873b23b7c381917f3383482b7780e0e11e734409fc18daacb0428082789f791af2c4a79d5c75cde45c201e46ae347ab7624e0137940945e174dfab59888892c25b8e10abee1a72f2f31fb3f0e8840db490ba752e1df966979f267756767c88ea0d7909145f379811412f68465e08fd2", + ), + ( + SignatureScheme::RsaPkcs1Sha384, + "0095d1ef58850cbf09dd1219a8678d7fa3f8c490a4a35feb19f50d85991caf659131dc90c52f14466c429a099a53e1d5f321a49065e85f250b85e08158ee46328515acee03bd215d610ec2335e1cbd1525058b950fb8ecb5d073f9fc474613c830c1111868efc3554c2ec62d9efd4694db281a6b3e48ed68d934278f5ba9a6fb19e8d648baa6b5f48d126def83986166ed05d710d5cdccd457092649b08d5e54c5ccfe42786852c98113b78291e3e2ced51ff3ed51ddef8cb81d311a2983cc6c93f0bb583537c067c6afc63e8e4a9d15635882b14d189a217d9588a469ac1899e3adeafdd276150b1023d7b64fea94b50a9ba22cc47c0d8d6ca4faf6b23f76d0", + ), + ( + SignatureScheme::RsaPkcs1Sha512, + "7373b3eaf66ba56bda1ae85a466c2f0321d15c37ce293642f6b69fddbea0e6f4dfa76dd0ba274afaabf71ba9d85198c0e75af95af6c64cb9bb304ea1ea3c04e653649891af78145b823d3ed1179fdbdc4edbedcf1eb3201b44a9a40930bf005da1bbe138dfac2d272364a991fcadcfba9093e889fa03f153771fdd51525ffae16cb2155eac8f6da49b602ae8fb34a5a131693459cacbc5adbc9b6002473732a7e205c2150bd25afe850c29845054e17d56a028d81f293b1dc634ba11a6790aa478f0f36bcc64787e356384ae90ef83c5226183afca64c9ac28c83ca803e01ff801f1ee12c806005d9f5d3562afae08e358722ca606d46d500d43901256625fc4", + ), + ]; + for (scheme, signature) in rsa_vectors { + crypto + .verify_signature( + scheme, + PublicKey { + encoding: PublicKeyEncoding::RsaPkcs1Der, + bytes: &rsa_public_key, + }, + message, + &bytes(signature), + ) + .unwrap(); + } +} + +/// Checks that a provider's secure random source returns fresh output. +pub fn assert_random(provider: &dyn RTCCryptoProvider) { + let mut first = [0; 32]; + let mut second = [0; 32]; + provider.random().fill(&mut first).unwrap(); + provider.random().fill(&mut second).unwrap(); + assert_ne!(first, second); +} + +/// Checks the default unsupported-operation behavior required for partial providers. +pub fn assert_unsupported_hash(crypto: &dyn RTCCrypto) { + assert!(!crypto.supports(CryptoAlgorithm::Hash(HashAlgorithm::Sha256))); + assert_eq!( + crypto.hash(HashAlgorithm::Sha256, b"input"), + Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hash( + HashAlgorithm::Sha256 + ))) + ); +} + +/// Checks the provider-neutral failure returned by a deliberately failing random source. +pub fn assert_random_failure(random: &dyn crate::RTCRandom) { + assert_eq!(random.fill(&mut [0; 1]), Err(CryptoError::RandomnessFailed)); +} + +fn bytes(hex: &str) -> Vec { + assert!(hex.len().is_multiple_of(2)); + hex.as_bytes() + .chunks_exact(2) + .map(|pair| (nibble(pair[0]) << 4) | nibble(pair[1])) + .collect() +} + +fn nibble(byte: u8) -> u8 { + match byte { + b'0'..=b'9' => byte - b'0', + b'a'..=b'f' => byte - b'a' + 10, + _ => panic!("invalid hexadecimal test vector"), + } +} diff --git a/rtc-crypto/src/error.rs b/rtc-crypto/src/error.rs new file mode 100644 index 00000000..c8cf15af --- /dev/null +++ b/rtc-crypto/src/error.rs @@ -0,0 +1,63 @@ +use crate::CryptoAlgorithm; + +/// A provider-neutral cryptographic failure. +#[non_exhaustive] +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum CryptoError { + /// No built-in default provider was compiled. + #[error("no default crypto provider is enabled")] + NoDefaultProvider, + /// The provider does not implement an algorithm. + #[error("unsupported algorithm: {0:?}")] + UnsupportedAlgorithm(CryptoAlgorithm), + /// A key has the wrong length. + #[error("invalid key length: expected {expected}, got {actual}")] + InvalidKeyLength { + /// Required length. + expected: usize, + /// Supplied length. + actual: usize, + }, + /// A nonce or IV has the wrong length. + #[error("invalid nonce length: expected {expected}, got {actual}")] + InvalidNonceLength { + /// Required length. + expected: usize, + /// Supplied length. + actual: usize, + }, + /// An authentication tag has the wrong length. + #[error("invalid tag length: expected {expected}, got {actual}")] + InvalidTagLength { + /// Required length. + expected: usize, + /// Supplied length. + actual: usize, + }, + /// Public-key bytes are malformed or use the wrong encoding. + #[error("invalid public key")] + InvalidPublicKey, + /// Private-key bytes are malformed or incompatible with the scheme. + #[error("invalid private key")] + InvalidPrivateKey, + /// Decryption, padding, or tag authentication failed. + #[error("authentication failed")] + AuthenticationFailed, + /// Signature verification failed. + #[error("signature verification failed")] + InvalidSignature, + /// The cryptographically secure random source failed. + #[error("randomness source failed")] + RandomnessFailed, + /// A caller-owned output buffer is too small. + #[error("output buffer is too small: required {required}, got {actual}")] + OutputTooSmall { + /// Required length. + required: usize, + /// Supplied length. + actual: usize, + }, + /// Sanitized provider diagnostic context. + #[error("provider failure: {0}")] + Provider(String), +} diff --git a/rtc-crypto/src/lib.rs b/rtc-crypto/src/lib.rs new file mode 100644 index 00000000..99fc34a9 --- /dev/null +++ b/rtc-crypto/src/lib.rs @@ -0,0 +1,87 @@ +//! Provider-neutral cryptography for the webrtc-rs RTC stack. +//! +//! The open traits in this crate allow applications to supply cryptography and randomness without +//! registering global state. Built-in providers are selected with additive Cargo features. + +mod algorithm; +mod error; +mod provider; +mod secret; +mod traits; + +#[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] +mod common; +pub mod providers; + +#[cfg(feature = "test-support")] +pub mod conformance; + +pub use algorithm::*; +pub use error::CryptoError; +pub use provider::default_provider; +pub use secret::SecretVec; +pub use traits::*; + +/// Compares equal-length byte strings without data-dependent early exit. +#[must_use] +pub fn constant_time_eq(left: &[u8], right: &[u8]) -> bool { + use subtle::ConstantTimeEq; + + left.len() == right.len() && bool::from(left.ct_eq(right)) +} + +const _: () = { + #[allow(dead_code)] + #[allow(clippy::too_many_arguments)] + fn assert_dyn_compatible( + _provider: &dyn RTCCryptoProvider, + _crypto: &dyn RTCCrypto, + _random: &dyn RTCRandom, + _mac: &dyn Mac, + _stream: &dyn StreamCipher, + _aead: &dyn AeadCipher, + _cbc: &dyn CbcCipher, + _exchange: &dyn ActiveKeyExchange, + _signing_key: &dyn SigningKey, + ) { + } +}; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn secret_debug_is_redacted() { + let secret = SecretVec::new(vec![1, 2, 3, 4]); + let debug = format!("{secret:?}"); + assert!(debug.contains("REDACTED")); + assert!(debug.contains("len: 4")); + assert!(!debug.contains("1, 2, 3, 4")); + } + + #[test] + fn secret_into_bytes_is_explicit() { + let secret = SecretVec::new(vec![1, 2, 3]); + assert_eq!(secret.into_bytes(), vec![1, 2, 3]); + } + + #[test] + fn errors_have_provider_neutral_text() { + assert_eq!( + CryptoError::AuthenticationFailed.to_string(), + "authentication failed" + ); + assert_eq!( + CryptoError::InvalidSignature.to_string(), + "signature verification failed" + ); + } + + #[test] + fn constant_time_equality_checks_length_and_content() { + assert!(constant_time_eq(b"same", b"same")); + assert!(!constant_time_eq(b"same", b"diff")); + assert!(!constant_time_eq(b"same", b"same-longer")); + } +} diff --git a/rtc-crypto/src/provider.rs b/rtc-crypto/src/provider.rs new file mode 100644 index 00000000..8bb430d7 --- /dev/null +++ b/rtc-crypto/src/provider.rs @@ -0,0 +1,24 @@ +use std::sync::Arc; + +use crate::{CryptoError, RTCCryptoProvider}; + +/// Constructs the built-in default provider. +/// +/// Ring remains the default whenever its feature is enabled. AWS-LC-RS is selected only when it is +/// the sole built-in. With no built-in features this returns [`CryptoError::NoDefaultProvider`]. +pub fn default_provider() -> Result, CryptoError> { + #[cfg(feature = "crypto-ring")] + { + Ok(Arc::new(crate::providers::RingProvider::new())) + } + + #[cfg(all(not(feature = "crypto-ring"), feature = "crypto-aws-lc-rs"))] + { + Ok(Arc::new(crate::providers::AwsLcRsProvider::new())) + } + + #[cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] + { + Err(CryptoError::NoDefaultProvider) + } +} diff --git a/rtc-crypto/src/providers/aws_lc_rs.rs b/rtc-crypto/src/providers/aws_lc_rs.rs new file mode 100644 index 00000000..1988f2b2 --- /dev/null +++ b/rtc-crypto/src/providers/aws_lc_rs.rs @@ -0,0 +1,531 @@ +use std::sync::Arc; + +use aws_lc_rs::rand::SystemRandom; +use aws_lc_rs::signature::{self, KeyPair}; +use aws_lc_rs::{aead, agreement, digest, hmac}; + +use crate::common; +use crate::{ + ActiveKeyExchange, AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, + CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, KeyExchangeAlgorithm, Mac, + PublicKey, PublicKeyEncoding, RTCCrypto, RTCCryptoProvider, RTCRandom, SecretVec, + SignatureScheme, SigningKey, StreamCipher, StreamCipherAlgorithm, constant_time_eq, +}; + +/// The built-in AWS-LC-RS provider bundle. +#[derive(Default)] +pub struct AwsLcRsProvider { + crypto: AwsLcRsCrypto, + random: AwsLcRsRandom, +} + +impl AwsLcRsProvider { + /// Creates an AWS-LC-RS provider. + #[must_use] + pub const fn new() -> Self { + Self { + crypto: AwsLcRsCrypto, + random: AwsLcRsRandom, + } + } +} + +impl RTCCryptoProvider for AwsLcRsProvider { + fn name(&self) -> &'static str { + "aws-lc-rs" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + &self.random + } +} + +/// AWS-LC-RS-backed operations, with RustCrypto fallbacks for primitives that are not exposed by +/// its stable high-level API: MD5, AES block/CTR/CBC, and AES-CCM. +#[derive(Default)] +pub struct AwsLcRsCrypto; + +/// AWS-LC-RS's operating-system-backed secure random source. +#[derive(Default)] +pub struct AwsLcRsRandom; + +impl RTCRandom for AwsLcRsRandom { + fn fill(&self, output: &mut [u8]) -> Result<(), CryptoError> { + common::fill_random(output) + } +} + +impl RTCCrypto for AwsLcRsCrypto { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + matches!( + algorithm, + CryptoAlgorithm::Hash(HashAlgorithm::Md5 | HashAlgorithm::Sha256) + | CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1 | HmacAlgorithm::Sha256) + | CryptoAlgorithm::Aead( + AeadAlgorithm::Aes128Gcm + | AeadAlgorithm::Aes256Gcm + | AeadAlgorithm::Aes128Ccm + | AeadAlgorithm::Aes128Ccm8 + | AeadAlgorithm::ChaCha20Poly1305, + ) + | CryptoAlgorithm::StreamCipher( + StreamCipherAlgorithm::Aes128Ctr | StreamCipherAlgorithm::Aes256Ctr, + ) + | CryptoAlgorithm::BlockCipher( + BlockCipherAlgorithm::Aes128 | BlockCipherAlgorithm::Aes256, + ) + | CryptoAlgorithm::Cbc(CbcAlgorithm::Aes256Cbc) + | CryptoAlgorithm::KeyExchange( + KeyExchangeAlgorithm::P256 + | KeyExchangeAlgorithm::P384 + | KeyExchangeAlgorithm::X25519, + ) + | CryptoAlgorithm::Signature( + SignatureScheme::Ed25519 + | SignatureScheme::EcdsaP256Sha256 + | SignatureScheme::EcdsaP384Sha384 + | SignatureScheme::RsaPkcs1Sha1 + | SignatureScheme::RsaPkcs1Sha256 + | SignatureScheme::RsaPkcs1Sha384 + | SignatureScheme::RsaPkcs1Sha512, + ) + | CryptoAlgorithm::SigningKeyGeneration( + SignatureScheme::Ed25519 | SignatureScheme::EcdsaP256Sha256, + ) + | CryptoAlgorithm::SigningKeyImport( + SignatureScheme::Ed25519 + | SignatureScheme::EcdsaP256Sha256 + | SignatureScheme::RsaPkcs1Sha256, + ) + ) + } + + fn hash(&self, algorithm: HashAlgorithm, data: &[u8]) -> Result, CryptoError> { + match algorithm { + HashAlgorithm::Md5 => Ok(common::md5(data)), + HashAlgorithm::Sha256 => Ok(digest::digest(&digest::SHA256, data).as_ref().to_vec()), + } + } + + fn new_hmac(&self, algorithm: HmacAlgorithm, key: &[u8]) -> Result, CryptoError> { + Ok(Box::new(AwsLcRsHmac { + key: hmac::Key::new(hmac_algorithm(algorithm), key), + output_len: algorithm.output_len(), + })) + } + + fn block_encrypt( + &self, + algorithm: BlockCipherAlgorithm, + key: &[u8], + block: &mut [u8], + ) -> Result<(), CryptoError> { + common::block_encrypt(algorithm, key, block) + } + + fn new_stream_cipher( + &self, + algorithm: StreamCipherAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + common::new_stream_cipher(algorithm, key) + } + + fn new_aead( + &self, + algorithm: AeadAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + match algorithm { + AeadAlgorithm::Aes128Ccm | AeadAlgorithm::Aes128Ccm8 => common::new_ccm(algorithm, key), + AeadAlgorithm::Aes128Gcm => AwsLcRsAead::create(&aead::AES_128_GCM, key), + AeadAlgorithm::Aes256Gcm => AwsLcRsAead::create(&aead::AES_256_GCM, key), + AeadAlgorithm::ChaCha20Poly1305 => AwsLcRsAead::create(&aead::CHACHA20_POLY1305, key), + } + } + + fn new_cbc( + &self, + algorithm: CbcAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + common::new_cbc(algorithm, key) + } + + fn start_key_exchange( + &self, + algorithm: KeyExchangeAlgorithm, + ) -> Result, CryptoError> { + AwsLcRsKeyExchange::start(algorithm) + } + + fn generate_signing_key( + &self, + scheme: SignatureScheme, + ) -> Result, CryptoError> { + AwsLcRsSigningKey::generate(scheme) + } + + fn import_signing_key( + &self, + scheme: SignatureScheme, + pkcs8_der: &[u8], + ) -> Result, CryptoError> { + AwsLcRsSigningKey::import(scheme, pkcs8_der) + } + + fn verify_signature( + &self, + scheme: SignatureScheme, + public_key: PublicKey<'_>, + message: &[u8], + signature: &[u8], + ) -> Result<(), CryptoError> { + verify_public_key_encoding(scheme, public_key.encoding)?; + signature::UnparsedPublicKey::new(verification_algorithm(scheme), public_key.bytes) + .verify(message, signature) + .map_err(|_| CryptoError::InvalidSignature) + } +} + +/// A keyed HMAC holding an `aws_lc_rs::hmac::Key`. +/// +/// `Key::new` performs the ipad/opad derivation; `Context::with_key` only clones the resulting +/// state. Keeping the key here is what moves that derivation off the per-packet path. +struct AwsLcRsHmac { + key: hmac::Key, + output_len: usize, +} + +impl Mac for AwsLcRsHmac { + fn output_len(&self) -> usize { + self.output_len + } + + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<(), CryptoError> { + common::check_tag_len(self.output_len, output.len())?; + let mut context = hmac::Context::with_key(&self.key); + for part in input { + context.update(part); + } + output.copy_from_slice(context.sign().as_ref()); + Ok(()) + } + + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> Result<(), CryptoError> { + common::check_tag_len(self.output_len, expected.len())?; + let mut actual = vec![0; self.output_len]; + self.sign(input, &mut actual)?; + if constant_time_eq(&actual, expected) { + Ok(()) + } else { + Err(CryptoError::AuthenticationFailed) + } + } +} + +fn hmac_algorithm(algorithm: HmacAlgorithm) -> hmac::Algorithm { + match algorithm { + HmacAlgorithm::Sha1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, + HmacAlgorithm::Sha256 => hmac::HMAC_SHA256, + } +} + +struct AwsLcRsAead { + key: aead::LessSafeKey, +} + +impl AwsLcRsAead { + fn create( + algorithm: &'static aead::Algorithm, + key: &[u8], + ) -> Result, CryptoError> { + common::check_key_len(algorithm.key_len(), key.len())?; + let key = aead::UnboundKey::new(algorithm, key) + .map(aead::LessSafeKey::new) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + Ok(Box::new(Self { key })) + } +} + +impl AeadCipher for AwsLcRsAead { + fn tag_len(&self) -> usize { + 16 + } + + fn seal_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + plaintext_and_ciphertext: &mut [u8], + tag_out: &mut [u8], + ) -> Result<(), CryptoError> { + common::check_nonce_len(12, nonce.len())?; + common::check_tag_len(self.tag_len(), tag_out.len())?; + let nonce = aead::Nonce::try_assume_unique_for_key(nonce).map_err(|_| { + CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + } + })?; + let tag = self + .key + .seal_in_place_separate_tag(nonce, aead::Aad::from(aad), plaintext_and_ciphertext) + .map_err(|_| CryptoError::AuthenticationFailed)?; + tag_out.copy_from_slice(tag.as_ref()); + Ok(()) + } + + fn open_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + ciphertext_and_plaintext: &mut [u8], + tag: &[u8], + ) -> Result<(), CryptoError> { + common::check_nonce_len(12, nonce.len())?; + common::check_tag_len(self.tag_len(), tag.len())?; + let nonce = aead::Nonce::try_assume_unique_for_key(nonce).map_err(|_| { + CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + } + })?; + self.key + .open_in_place_separate_tag(nonce, aead::Aad::from(aad), tag, ciphertext_and_plaintext) + .map(|_| ()) + .map_err(|_| CryptoError::AuthenticationFailed) + } +} + +struct AwsLcRsKeyExchange { + algorithm: KeyExchangeAlgorithm, + backend_algorithm: &'static agreement::Algorithm, + private_key: agreement::PrivateKey, + public_key: Vec, +} + +impl AwsLcRsKeyExchange { + fn start(algorithm: KeyExchangeAlgorithm) -> Result, CryptoError> { + let backend_algorithm = agreement_algorithm(algorithm); + let private_key = agreement::PrivateKey::generate(backend_algorithm) + .map_err(|_| CryptoError::RandomnessFailed)?; + let public_key = private_key + .compute_public_key() + .map_err(|_| CryptoError::Provider("key exchange public-key generation failed".into()))? + .as_ref() + .to_vec(); + Ok(Box::new(Self { + algorithm, + backend_algorithm, + private_key, + public_key, + })) + } +} + +impl ActiveKeyExchange for AwsLcRsKeyExchange { + fn algorithm(&self) -> KeyExchangeAlgorithm { + self.algorithm + } + + fn public_key(&self) -> &[u8] { + &self.public_key + } + + fn complete(self: Box, peer_public_key: &[u8]) -> Result { + let peer = agreement::UnparsedPublicKey::new(self.backend_algorithm, peer_public_key); + agreement::agree( + &self.private_key, + peer, + CryptoError::InvalidPublicKey, + |secret| Ok(SecretVec::new(secret.to_vec())), + ) + } +} + +enum AwsLcRsSigningKeyKind { + Ed25519(signature::Ed25519KeyPair), + EcdsaP256(signature::EcdsaKeyPair), + Rsa(signature::RsaKeyPair), +} + +struct AwsLcRsSigningKey { + scheme: SignatureScheme, + kind: AwsLcRsSigningKeyKind, + public_key: Vec, + public_key_encoding: PublicKeyEncoding, + pkcs8_der: SecretVec, +} + +impl AwsLcRsSigningKey { + fn generate(scheme: SignatureScheme) -> Result, CryptoError> { + let rng = SystemRandom::new(); + let pkcs8 = match scheme { + SignatureScheme::Ed25519 => signature::Ed25519KeyPair::generate_pkcs8(&rng), + SignatureScheme::EcdsaP256Sha256 => signature::EcdsaKeyPair::generate_pkcs8( + &signature::ECDSA_P256_SHA256_ASN1_SIGNING, + &rng, + ), + _ => { + return Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::SigningKeyGeneration(scheme), + )); + } + } + .map_err(|_| CryptoError::RandomnessFailed)?; + Self::import(scheme, pkcs8.as_ref()) + } + + fn import( + scheme: SignatureScheme, + pkcs8_der: &[u8], + ) -> Result, CryptoError> { + let (kind, public_key, public_key_encoding) = match scheme { + SignatureScheme::Ed25519 => { + let key = signature::Ed25519KeyPair::from_pkcs8_maybe_unchecked(pkcs8_der) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + let public = key.public_key().as_ref().to_vec(); + ( + AwsLcRsSigningKeyKind::Ed25519(key), + public, + PublicKeyEncoding::Ed25519Raw, + ) + } + SignatureScheme::EcdsaP256Sha256 => { + let key = signature::EcdsaKeyPair::from_pkcs8( + &signature::ECDSA_P256_SHA256_ASN1_SIGNING, + pkcs8_der, + ) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + let public = key.public_key().as_ref().to_vec(); + ( + AwsLcRsSigningKeyKind::EcdsaP256(key), + public, + PublicKeyEncoding::EcUncompressedPoint, + ) + } + SignatureScheme::RsaPkcs1Sha256 => { + let key = signature::RsaKeyPair::from_pkcs8(pkcs8_der) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + let public = key.public_key().as_ref().to_vec(); + ( + AwsLcRsSigningKeyKind::Rsa(key), + public, + PublicKeyEncoding::RsaPkcs1Der, + ) + } + _ => { + return Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::SigningKeyImport(scheme), + )); + } + }; + Ok(Arc::new(Self { + scheme, + kind, + public_key, + public_key_encoding, + pkcs8_der: SecretVec::new(pkcs8_der.to_vec()), + })) + } +} + +impl SigningKey for AwsLcRsSigningKey { + fn supports(&self, scheme: SignatureScheme) -> bool { + self.scheme == scheme + } + + fn public_key(&self) -> PublicKey<'_> { + PublicKey { + encoding: self.public_key_encoding, + bytes: &self.public_key, + } + } + + fn sign(&self, scheme: SignatureScheme, message: &[u8]) -> Result, CryptoError> { + if !self.supports(scheme) { + return Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::Signature(scheme), + )); + } + match &self.kind { + AwsLcRsSigningKeyKind::Ed25519(key) => Ok(key.sign(message).as_ref().to_vec()), + AwsLcRsSigningKeyKind::EcdsaP256(key) => key + .sign(&SystemRandom::new(), message) + .map(|signature| signature.as_ref().to_vec()) + .map_err(|_| CryptoError::Provider("signature generation failed".into())), + AwsLcRsSigningKeyKind::Rsa(key) => { + let mut signature = vec![0; key.public_modulus_len()]; + key.sign( + &signature::RSA_PKCS1_SHA256, + &SystemRandom::new(), + message, + &mut signature, + ) + .map_err(|_| CryptoError::Provider("signature generation failed".into()))?; + Ok(signature) + } + } + } + + fn to_pkcs8_der(&self) -> Result, CryptoError> { + Ok(Some(self.pkcs8_der.clone())) + } +} + +fn agreement_algorithm(algorithm: KeyExchangeAlgorithm) -> &'static agreement::Algorithm { + match algorithm { + KeyExchangeAlgorithm::P256 => &agreement::ECDH_P256, + KeyExchangeAlgorithm::P384 => &agreement::ECDH_P384, + KeyExchangeAlgorithm::X25519 => &agreement::X25519, + } +} + +fn verification_algorithm( + scheme: SignatureScheme, +) -> &'static dyn signature::VerificationAlgorithm { + match scheme { + SignatureScheme::Ed25519 => &signature::ED25519, + SignatureScheme::EcdsaP256Sha256 => &signature::ECDSA_P256_SHA256_ASN1, + SignatureScheme::EcdsaP384Sha384 => &signature::ECDSA_P384_SHA384_ASN1, + SignatureScheme::RsaPkcs1Sha1 => &signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY, + SignatureScheme::RsaPkcs1Sha256 => { + &signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY + } + SignatureScheme::RsaPkcs1Sha384 => &signature::RSA_PKCS1_2048_8192_SHA384, + SignatureScheme::RsaPkcs1Sha512 => { + &signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY + } + } +} + +fn verify_public_key_encoding( + scheme: SignatureScheme, + encoding: PublicKeyEncoding, +) -> Result<(), CryptoError> { + let valid = matches!( + (scheme, encoding), + (SignatureScheme::Ed25519, PublicKeyEncoding::Ed25519Raw) + | ( + SignatureScheme::EcdsaP256Sha256 | SignatureScheme::EcdsaP384Sha384, + PublicKeyEncoding::EcUncompressedPoint + ) + | ( + SignatureScheme::RsaPkcs1Sha1 + | SignatureScheme::RsaPkcs1Sha256 + | SignatureScheme::RsaPkcs1Sha384 + | SignatureScheme::RsaPkcs1Sha512, + PublicKeyEncoding::RsaPkcs1Der + ) + ); + if valid { + Ok(()) + } else { + Err(CryptoError::InvalidPublicKey) + } +} diff --git a/rtc-crypto/src/providers/mod.rs b/rtc-crypto/src/providers/mod.rs new file mode 100644 index 00000000..f6b8f91e --- /dev/null +++ b/rtc-crypto/src/providers/mod.rs @@ -0,0 +1,9 @@ +#[cfg(feature = "crypto-aws-lc-rs")] +mod aws_lc_rs; +#[cfg(feature = "crypto-ring")] +mod ring; + +#[cfg(feature = "crypto-aws-lc-rs")] +pub use aws_lc_rs::{AwsLcRsCrypto, AwsLcRsProvider, AwsLcRsRandom}; +#[cfg(feature = "crypto-ring")] +pub use ring::{RingCrypto, RingProvider, RingRandom}; diff --git a/rtc-crypto/src/providers/ring.rs b/rtc-crypto/src/providers/ring.rs new file mode 100644 index 00000000..6158bd69 --- /dev/null +++ b/rtc-crypto/src/providers/ring.rs @@ -0,0 +1,603 @@ +use crate::common; +use crate::common::check_tag_len; +use crate::{ + ActiveKeyExchange, AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, + CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, KeyExchangeAlgorithm, Mac, + PublicKey, PublicKeyEncoding, RTCCrypto, RTCCryptoProvider, RTCRandom, SecretVec, + SignatureScheme, SigningKey, StreamCipher, StreamCipherAlgorithm, constant_time_eq, +}; +use ::hmac::Mac as RustCryptoMac; +use ring::aead; +use ring::agreement; +use ring::digest; +use ring::hmac; +use ring::rand::SystemRandom; +use ring::signature::{self, KeyPair}; +use sha1::Sha1; +use std::sync::Arc; + +/// The built-in Ring provider bundle. +#[derive(Default)] +pub struct RingProvider { + crypto: RingCrypto, + random: RingRandom, +} + +impl RingProvider { + /// Creates a Ring provider. + #[must_use] + pub const fn new() -> Self { + Self { + crypto: RingCrypto, + random: RingRandom, + } + } +} + +impl RTCCryptoProvider for RingProvider { + fn name(&self) -> &'static str { + "ring" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + &self.random + } +} + +/// Ring-backed cryptographic operations, with documented RustCrypto fallbacks for primitives Ring +/// does not expose (MD5, AES block/CTR/CBC, and AES-CCM). +#[derive(Default)] +pub struct RingCrypto; + +/// Ring's operating-system-backed secure random source. +#[derive(Default)] +pub struct RingRandom; + +impl RTCRandom for RingRandom { + fn fill(&self, output: &mut [u8]) -> Result<(), CryptoError> { + common::fill_random(output) + } +} + +impl RTCCrypto for RingCrypto { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + matches!( + algorithm, + CryptoAlgorithm::Hash(HashAlgorithm::Md5 | HashAlgorithm::Sha256) + | CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1 | HmacAlgorithm::Sha256) + | CryptoAlgorithm::Aead( + AeadAlgorithm::Aes128Gcm + | AeadAlgorithm::Aes256Gcm + | AeadAlgorithm::Aes128Ccm + | AeadAlgorithm::Aes128Ccm8 + | AeadAlgorithm::ChaCha20Poly1305, + ) + | CryptoAlgorithm::StreamCipher( + StreamCipherAlgorithm::Aes128Ctr | StreamCipherAlgorithm::Aes256Ctr, + ) + | CryptoAlgorithm::BlockCipher( + BlockCipherAlgorithm::Aes128 | BlockCipherAlgorithm::Aes256, + ) + | CryptoAlgorithm::Cbc(CbcAlgorithm::Aes256Cbc) + | CryptoAlgorithm::KeyExchange( + KeyExchangeAlgorithm::P256 + | KeyExchangeAlgorithm::P384 + | KeyExchangeAlgorithm::X25519, + ) + | CryptoAlgorithm::Signature( + SignatureScheme::Ed25519 + | SignatureScheme::EcdsaP256Sha256 + | SignatureScheme::EcdsaP384Sha384 + | SignatureScheme::RsaPkcs1Sha1 + | SignatureScheme::RsaPkcs1Sha256 + | SignatureScheme::RsaPkcs1Sha384 + | SignatureScheme::RsaPkcs1Sha512, + ) + | CryptoAlgorithm::SigningKeyGeneration( + SignatureScheme::Ed25519 | SignatureScheme::EcdsaP256Sha256, + ) + | CryptoAlgorithm::SigningKeyImport( + SignatureScheme::Ed25519 + | SignatureScheme::EcdsaP256Sha256 + | SignatureScheme::RsaPkcs1Sha256, + ) + ) + } + + fn hash(&self, algorithm: HashAlgorithm, data: &[u8]) -> Result, CryptoError> { + match algorithm { + HashAlgorithm::Md5 => Ok(common::md5(data)), + HashAlgorithm::Sha256 => Ok(digest::digest(&digest::SHA256, data).as_ref().to_vec()), + } + } + + fn new_hmac(&self, algorithm: HmacAlgorithm, key: &[u8]) -> Result, CryptoError> { + match algorithm { + // ring's SHA-1 is a software implementation and measures 3.3x slower than + // RustCrypto's; see common::RustCryptoHmacSha1. SHA-256 stays on ring, which uses + // the hardware instructions. + HmacAlgorithm::Sha1 => Ok(Box::new(RustCryptoHmacSha1::new(key))), + HmacAlgorithm::Sha256 => Ok(Box::new(RingHmac { + key: hmac::Key::new(hmac_algorithm(algorithm), key), + output_len: algorithm.output_len(), + })), + } + } + + fn block_encrypt( + &self, + algorithm: BlockCipherAlgorithm, + key: &[u8], + block: &mut [u8], + ) -> Result<(), CryptoError> { + common::block_encrypt(algorithm, key, block) + } + + fn new_stream_cipher( + &self, + algorithm: StreamCipherAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + common::new_stream_cipher(algorithm, key) + } + + fn new_aead( + &self, + algorithm: AeadAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + match algorithm { + AeadAlgorithm::Aes128Ccm | AeadAlgorithm::Aes128Ccm8 => common::new_ccm(algorithm, key), + AeadAlgorithm::Aes128Gcm => RingAead::create(&aead::AES_128_GCM, key), + AeadAlgorithm::Aes256Gcm => RingAead::create(&aead::AES_256_GCM, key), + AeadAlgorithm::ChaCha20Poly1305 => RingAead::create(&aead::CHACHA20_POLY1305, key), + } + } + + fn new_cbc( + &self, + algorithm: CbcAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + common::new_cbc(algorithm, key) + } + + fn start_key_exchange( + &self, + algorithm: KeyExchangeAlgorithm, + ) -> Result, CryptoError> { + RingKeyExchange::start(algorithm) + } + + fn generate_signing_key( + &self, + scheme: SignatureScheme, + ) -> Result, CryptoError> { + RingSigningKey::generate(scheme) + } + + fn import_signing_key( + &self, + scheme: SignatureScheme, + pkcs8_der: &[u8], + ) -> Result, CryptoError> { + RingSigningKey::import(scheme, pkcs8_der) + } + + fn verify_signature( + &self, + scheme: SignatureScheme, + public_key: PublicKey<'_>, + message: &[u8], + signature: &[u8], + ) -> Result<(), CryptoError> { + verify_public_key_encoding(scheme, public_key.encoding)?; + signature::UnparsedPublicKey::new(verification_algorithm(scheme), public_key.bytes) + .verify(message, signature) + .map_err(|_| CryptoError::InvalidSignature) + } +} + +/// A keyed HMAC holding a `ring::hmac::Key`. +/// +/// `Key::new` performs the ipad/opad derivation; `Context::with_key` only clones the resulting +/// state. Keeping the key here is what moves that derivation off the per-packet path. +struct RingHmac { + key: hmac::Key, + output_len: usize, +} + +impl Mac for RingHmac { + fn output_len(&self) -> usize { + self.output_len + } + + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<(), CryptoError> { + common::check_tag_len(self.output_len, output.len())?; + let mut context = hmac::Context::with_key(&self.key); + for part in input { + context.update(part); + } + output.copy_from_slice(context.sign().as_ref()); + Ok(()) + } + + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> Result<(), CryptoError> { + common::check_tag_len(self.output_len, expected.len())?; + let mut actual = vec![0; self.output_len]; + self.sign(input, &mut actual)?; + if constant_time_eq(&actual, expected) { + Ok(()) + } else { + Err(CryptoError::AuthenticationFailed) + } + } +} + +fn hmac_algorithm(algorithm: HmacAlgorithm) -> hmac::Algorithm { + match algorithm { + HmacAlgorithm::Sha1 => hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, + HmacAlgorithm::Sha256 => hmac::HMAC_SHA256, + } +} + +struct RingAead { + key: aead::LessSafeKey, +} + +impl RingAead { + fn create( + algorithm: &'static aead::Algorithm, + key: &[u8], + ) -> Result, CryptoError> { + common::check_key_len(algorithm.key_len(), key.len())?; + let key = aead::UnboundKey::new(algorithm, key) + .map(aead::LessSafeKey::new) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + Ok(Box::new(Self { key })) + } +} + +impl AeadCipher for RingAead { + fn tag_len(&self) -> usize { + 16 + } + + fn seal_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + plaintext_and_ciphertext: &mut [u8], + tag_out: &mut [u8], + ) -> Result<(), CryptoError> { + common::check_nonce_len(12, nonce.len())?; + common::check_tag_len(self.tag_len(), tag_out.len())?; + let nonce = aead::Nonce::try_assume_unique_for_key(nonce).map_err(|_| { + CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + } + })?; + let tag = self + .key + .seal_in_place_separate_tag(nonce, aead::Aad::from(aad), plaintext_and_ciphertext) + .map_err(|_| CryptoError::AuthenticationFailed)?; + tag_out.copy_from_slice(tag.as_ref()); + Ok(()) + } + + fn open_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + ciphertext_and_plaintext: &mut [u8], + tag: &[u8], + ) -> Result<(), CryptoError> { + common::check_nonce_len(12, nonce.len())?; + common::check_tag_len(self.tag_len(), tag.len())?; + let nonce = aead::Nonce::try_assume_unique_for_key(nonce).map_err(|_| { + CryptoError::InvalidNonceLength { + expected: 12, + actual: nonce.len(), + } + })?; + let tag = aead::Tag::try_from(tag).map_err(|_| CryptoError::InvalidTagLength { + expected: self.tag_len(), + actual: tag.len(), + })?; + self.key + .open_in_place_separate_tag( + nonce, + aead::Aad::from(aad), + tag, + ciphertext_and_plaintext, + 0.., + ) + .map(|_| ()) + .map_err(|_| CryptoError::AuthenticationFailed) + } +} + +struct RingKeyExchange { + algorithm: KeyExchangeAlgorithm, + backend_algorithm: &'static agreement::Algorithm, + private_key: agreement::EphemeralPrivateKey, + public_key: Vec, +} + +impl RingKeyExchange { + fn start(algorithm: KeyExchangeAlgorithm) -> Result, CryptoError> { + let backend_algorithm = agreement_algorithm(algorithm); + let private_key = + agreement::EphemeralPrivateKey::generate(backend_algorithm, &SystemRandom::new()) + .map_err(|_| CryptoError::RandomnessFailed)?; + let public_key = private_key + .compute_public_key() + .map_err(|_| CryptoError::Provider("key exchange public-key generation failed".into()))? + .as_ref() + .to_vec(); + Ok(Box::new(Self { + algorithm, + backend_algorithm, + private_key, + public_key, + })) + } +} + +impl ActiveKeyExchange for RingKeyExchange { + fn algorithm(&self) -> KeyExchangeAlgorithm { + self.algorithm + } + + fn public_key(&self) -> &[u8] { + &self.public_key + } + + fn complete(self: Box, peer_public_key: &[u8]) -> Result { + let peer = agreement::UnparsedPublicKey::new(self.backend_algorithm, peer_public_key); + agreement::agree_ephemeral(self.private_key, &peer, |secret| { + SecretVec::new(secret.to_vec()) + }) + .map_err(|_| CryptoError::InvalidPublicKey) + } +} + +enum RingSigningKeyKind { + Ed25519(signature::Ed25519KeyPair), + EcdsaP256(signature::EcdsaKeyPair), + Rsa(signature::RsaKeyPair), +} + +struct RingSigningKey { + scheme: SignatureScheme, + kind: RingSigningKeyKind, + public_key: Vec, + public_key_encoding: PublicKeyEncoding, + pkcs8_der: SecretVec, +} + +impl RingSigningKey { + fn generate(scheme: SignatureScheme) -> Result, CryptoError> { + let rng = SystemRandom::new(); + let pkcs8 = match scheme { + SignatureScheme::Ed25519 => signature::Ed25519KeyPair::generate_pkcs8(&rng), + SignatureScheme::EcdsaP256Sha256 => signature::EcdsaKeyPair::generate_pkcs8( + &signature::ECDSA_P256_SHA256_ASN1_SIGNING, + &rng, + ), + _ => { + return Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::SigningKeyGeneration(scheme), + )); + } + } + .map_err(|_| CryptoError::RandomnessFailed)?; + Self::import(scheme, pkcs8.as_ref()) + } + + fn import( + scheme: SignatureScheme, + pkcs8_der: &[u8], + ) -> Result, CryptoError> { + let rng = SystemRandom::new(); + let (kind, public_key, public_key_encoding) = match scheme { + SignatureScheme::Ed25519 => { + let key = signature::Ed25519KeyPair::from_pkcs8_maybe_unchecked(pkcs8_der) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + let public = key.public_key().as_ref().to_vec(); + ( + RingSigningKeyKind::Ed25519(key), + public, + PublicKeyEncoding::Ed25519Raw, + ) + } + SignatureScheme::EcdsaP256Sha256 => { + let key = signature::EcdsaKeyPair::from_pkcs8( + &signature::ECDSA_P256_SHA256_ASN1_SIGNING, + pkcs8_der, + &rng, + ) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + let public = key.public_key().as_ref().to_vec(); + ( + RingSigningKeyKind::EcdsaP256(key), + public, + PublicKeyEncoding::EcUncompressedPoint, + ) + } + SignatureScheme::RsaPkcs1Sha256 => { + let key = signature::RsaKeyPair::from_pkcs8(pkcs8_der) + .map_err(|_| CryptoError::InvalidPrivateKey)?; + let public = key.public().as_ref().to_vec(); + ( + RingSigningKeyKind::Rsa(key), + public, + PublicKeyEncoding::RsaPkcs1Der, + ) + } + _ => { + return Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::SigningKeyImport(scheme), + )); + } + }; + Ok(Arc::new(Self { + scheme, + kind, + public_key, + public_key_encoding, + pkcs8_der: SecretVec::new(pkcs8_der.to_vec()), + })) + } +} + +impl SigningKey for RingSigningKey { + fn supports(&self, scheme: SignatureScheme) -> bool { + self.scheme == scheme + } + + fn public_key(&self) -> PublicKey<'_> { + PublicKey { + encoding: self.public_key_encoding, + bytes: &self.public_key, + } + } + + fn sign(&self, scheme: SignatureScheme, message: &[u8]) -> Result, CryptoError> { + if !self.supports(scheme) { + return Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::Signature(scheme), + )); + } + match &self.kind { + RingSigningKeyKind::Ed25519(key) => Ok(key.sign(message).as_ref().to_vec()), + RingSigningKeyKind::EcdsaP256(key) => key + .sign(&SystemRandom::new(), message) + .map(|signature| signature.as_ref().to_vec()) + .map_err(|_| CryptoError::Provider("signature generation failed".into())), + RingSigningKeyKind::Rsa(key) => { + let mut signature = vec![0; key.public().modulus_len()]; + key.sign( + &signature::RSA_PKCS1_SHA256, + &SystemRandom::new(), + message, + &mut signature, + ) + .map_err(|_| CryptoError::Provider("signature generation failed".into()))?; + Ok(signature) + } + } + } + + fn to_pkcs8_der(&self) -> Result, CryptoError> { + Ok(Some(self.pkcs8_der.clone())) + } +} + +fn agreement_algorithm(algorithm: KeyExchangeAlgorithm) -> &'static agreement::Algorithm { + match algorithm { + KeyExchangeAlgorithm::P256 => &agreement::ECDH_P256, + KeyExchangeAlgorithm::P384 => &agreement::ECDH_P384, + KeyExchangeAlgorithm::X25519 => &agreement::X25519, + } +} + +fn verification_algorithm( + scheme: SignatureScheme, +) -> &'static dyn signature::VerificationAlgorithm { + match scheme { + SignatureScheme::Ed25519 => &signature::ED25519, + SignatureScheme::EcdsaP256Sha256 => &signature::ECDSA_P256_SHA256_ASN1, + SignatureScheme::EcdsaP384Sha384 => &signature::ECDSA_P384_SHA384_ASN1, + SignatureScheme::RsaPkcs1Sha1 => &signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY, + SignatureScheme::RsaPkcs1Sha256 => { + &signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY + } + SignatureScheme::RsaPkcs1Sha384 => &signature::RSA_PKCS1_2048_8192_SHA384, + SignatureScheme::RsaPkcs1Sha512 => { + &signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY + } + } +} + +fn verify_public_key_encoding( + scheme: SignatureScheme, + encoding: PublicKeyEncoding, +) -> Result<(), CryptoError> { + let valid = matches!( + (scheme, encoding), + (SignatureScheme::Ed25519, PublicKeyEncoding::Ed25519Raw) + | ( + SignatureScheme::EcdsaP256Sha256 | SignatureScheme::EcdsaP384Sha384, + PublicKeyEncoding::EcUncompressedPoint + ) + | ( + SignatureScheme::RsaPkcs1Sha1 + | SignatureScheme::RsaPkcs1Sha256 + | SignatureScheme::RsaPkcs1Sha384 + | SignatureScheme::RsaPkcs1Sha512, + PublicKeyEncoding::RsaPkcs1Der + ) + ); + if valid { + Ok(()) + } else { + Err(CryptoError::InvalidPublicKey) + } +} + +type HmacSha1 = ::hmac::Hmac; + +/// HMAC-SHA1 backed by RustCrypto, keyed once. +/// +/// `ring` exposes SHA-1 only as `HMAC_SHA1_FOR_LEGACY_USE_ONLY` and does not use the ARMv8 SHA-1 +/// instructions, measuring 4469 ns against RustCrypto's 1373 ns over a 1212-byte SRTP packet — +/// 3.3x, and the whole of the SRTP AES-CM/HMAC regression. The built-in providers are already +/// composite (AES-CTR, CCM, CBC and MD5 come from RustCrypto too), so HMAC-SHA1 is composed the +/// same way rather than making the slower backend the default. `aws-lc-rs` has fast SHA-1 and +/// keeps its own. +pub(crate) struct RustCryptoHmacSha1 { + keyed: HmacSha1, +} + +impl RustCryptoHmacSha1 { + pub(crate) fn new(key: &[u8]) -> Self { + Self { + // HMAC accepts any key length: it hashes longer keys and zero-pads shorter ones. + keyed: ::new_from_slice(key) + .expect("HMAC accepts keys of any length"), + } + } +} + +impl Mac for RustCryptoHmacSha1 { + fn output_len(&self) -> usize { + 20 + } + + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<(), CryptoError> { + check_tag_len(20, output.len())?; + let mut mac = self.keyed.clone(); + for part in input { + mac.update(part); + } + output.copy_from_slice(&mac.finalize().into_bytes()); + Ok(()) + } + + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> Result<(), CryptoError> { + check_tag_len(20, expected.len())?; + let mut actual = [0u8; 20]; + self.sign(input, &mut actual)?; + if crate::constant_time_eq(&actual, expected) { + Ok(()) + } else { + Err(CryptoError::AuthenticationFailed) + } + } +} diff --git a/rtc-crypto/src/secret.rs b/rtc-crypto/src/secret.rs new file mode 100644 index 00000000..547a6f1d --- /dev/null +++ b/rtc-crypto/src/secret.rs @@ -0,0 +1,66 @@ +use std::fmt; + +use zeroize::Zeroizing; + +/// An owned byte vector that zeroizes its allocation when dropped. +pub struct SecretVec(Zeroizing>); + +impl SecretVec { + /// Wraps secret bytes. + #[must_use] + pub fn new(bytes: Vec) -> Self { + Self(Zeroizing::new(bytes)) + } + + /// Returns the number of secret bytes. + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns whether the secret is empty. + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Explicitly unwraps the secret bytes. The returned vector is no longer automatically zeroized. + #[must_use] + pub fn into_bytes(mut self) -> Vec { + std::mem::take(&mut *self.0) + } +} + +impl AsRef<[u8]> for SecretVec { + fn as_ref(&self) -> &[u8] { + self.0.as_slice() + } +} + +impl AsMut<[u8]> for SecretVec { + fn as_mut(&mut self) -> &mut [u8] { + self.0.as_mut_slice() + } +} + +impl Clone for SecretVec { + fn clone(&self) -> Self { + Self::new(self.as_ref().to_vec()) + } +} + +impl fmt::Debug for SecretVec { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("SecretVec") + .field("len", &self.len()) + .field("bytes", &"[REDACTED]") + .finish() + } +} + +impl From> for SecretVec { + fn from(value: Vec) -> Self { + Self::new(value) + } +} diff --git a/rtc-crypto/src/traits.rs b/rtc-crypto/src/traits.rs new file mode 100644 index 00000000..bcab4f19 --- /dev/null +++ b/rtc-crypto/src/traits.rs @@ -0,0 +1,234 @@ +use std::sync::Arc; + +use crate::{ + AeadAlgorithm, BlockCipherAlgorithm, CbcAlgorithm, CryptoAlgorithm, CryptoError, HashAlgorithm, + HmacAlgorithm, KeyExchangeAlgorithm, PublicKey, SecretVec, SignatureScheme, + StreamCipherAlgorithm, +}; + +/// A bundle of cryptographic operations and cryptographically secure randomness. +pub trait RTCCryptoProvider: Send + Sync { + /// Returns a non-secret diagnostic name. + fn name(&self) -> &'static str; + + /// Returns the cryptographic operations implementation. + fn crypto(&self) -> &dyn RTCCrypto; + + /// Returns the cryptographically secure random source. + fn random(&self) -> &dyn RTCRandom; +} + +/// A cryptographically secure random byte generator. +pub trait RTCRandom: Send + Sync { + /// Fills all of `output` with random bytes. + fn fill(&self, output: &mut [u8]) -> Result<(), CryptoError>; +} + +/// Provider-neutral cryptographic operations. +pub trait RTCCrypto: Send + Sync { + /// Reports whether an operation is implemented. + fn supports(&self, algorithm: CryptoAlgorithm) -> bool; + + /// Hashes `data`. + fn hash(&self, algorithm: HashAlgorithm, _data: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hash( + algorithm, + ))) + } + + /// Encrypts exactly one block in place. + fn block_encrypt( + &self, + algorithm: BlockCipherAlgorithm, + _key: &[u8], + _block: &mut [u8], + ) -> Result<(), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::BlockCipher(algorithm), + )) + } + + /// Creates a keyed MAC. + /// + /// This is the only HMAC entry point. Deriving the key schedule is the expensive part, so it + /// happens here rather than per message; a caller that authenticates many messages with one + /// key holds the returned [`Mac`]. One-shot callers simply drop it after a single `sign` or + /// `verify`. + fn new_hmac(&self, algorithm: HmacAlgorithm, _key: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hmac( + algorithm, + ))) + } + + /// Creates a keyed stream cipher. + fn new_stream_cipher( + &self, + algorithm: StreamCipherAlgorithm, + _key: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::StreamCipher(algorithm), + )) + } + + /// Creates a keyed AEAD cipher. + fn new_aead( + &self, + algorithm: AeadAlgorithm, + _key: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Aead( + algorithm, + ))) + } + + /// Creates a keyed CBC cipher. + fn new_cbc( + &self, + algorithm: CbcAlgorithm, + _key: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Cbc( + algorithm, + ))) + } + + /// Starts a one-shot ephemeral key exchange. + fn start_key_exchange( + &self, + algorithm: KeyExchangeAlgorithm, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::KeyExchange(algorithm), + )) + } + + /// Generates an exportable signing key. + fn generate_signing_key( + &self, + scheme: SignatureScheme, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::SigningKeyGeneration(scheme), + )) + } + + /// Imports an exportable PKCS#8 signing key. + fn import_signing_key( + &self, + scheme: SignatureScheme, + _pkcs8_der: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::SigningKeyImport(scheme), + )) + } + + /// Verifies a signature. + fn verify_signature( + &self, + scheme: SignatureScheme, + _public_key: PublicKey<'_>, + _message: &[u8], + _signature: &[u8], + ) -> Result<(), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm( + CryptoAlgorithm::Signature(scheme), + )) + } +} + +/// A keyed message authentication code with a reusable key schedule. +/// +/// Created once per key by [`RTCCrypto::new_hmac`] and used for every message authenticated with +/// that key, so the ipad/opad derivation is paid once rather than per packet. +/// +/// `Send` and mutable, like the keyed cipher traits, and for the same reason: `&mut self` lets an +/// implementation carry per-message state — a reused streaming context, a hardware session +/// handle — without interior mutability, and does not impose `Sync` on implementors that cannot +/// offer it. A caller whose own signature is fixed to `&self`, such as STUN's `Setter::add_to`, +/// can still create a local `Mac` per message and use it mutably. +pub trait Mac: Send { + /// Returns the untruncated tag length in bytes. + fn output_len(&self) -> usize; + + /// Writes the tag over the concatenation of `input` into `output`. + /// + /// `output` must be exactly [`output_len`](Self::output_len) bytes. Protocols that transmit a + /// truncated tag — SRTP sends 80 or 32 bits of an SHA-1 tag — truncate the result themselves. + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<(), CryptoError>; + + /// Verifies a complete untruncated tag in constant time. + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> Result<(), CryptoError>; +} + +/// A keyed stream cipher with a reusable expanded key. +pub trait StreamCipher: Send { + /// Applies the keystream in place with a fresh IV. + fn apply_keystream(&mut self, iv: &[u8], data: &mut [u8]) -> Result<(), CryptoError>; +} + +/// A keyed authenticated cipher with detached tags. +pub trait AeadCipher: Send { + /// Returns the detached tag length in bytes. + fn tag_len(&self) -> usize; + + /// Encrypts and authenticates a caller-owned buffer. + fn seal_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + plaintext_and_ciphertext: &mut [u8], + tag_out: &mut [u8], + ) -> Result<(), CryptoError>; + + /// Authenticates and decrypts a caller-owned buffer. + fn open_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + ciphertext_and_plaintext: &mut [u8], + tag: &[u8], + ) -> Result<(), CryptoError>; +} + +/// A keyed CBC block cipher with a reusable expanded key. +pub trait CbcCipher: Send { + /// Returns the block and IV length in bytes. + fn block_len(&self) -> usize; + + /// Encrypts whole blocks in place without applying padding. + fn encrypt_blocks(&mut self, iv: &[u8], blocks: &mut [u8]) -> Result<(), CryptoError>; + + /// Decrypts whole blocks in place without removing padding. + fn decrypt_blocks(&mut self, iv: &[u8], blocks: &mut [u8]) -> Result<(), CryptoError>; +} + +/// Provider-owned one-shot ephemeral key exchange. +pub trait ActiveKeyExchange: Send { + /// Returns the key-exchange algorithm. + fn algorithm(&self) -> KeyExchangeAlgorithm; + + /// Returns the encoded wire public key. + fn public_key(&self) -> &[u8]; + + /// Consumes the private key and derives the shared secret. + fn complete(self: Box, peer_public_key: &[u8]) -> Result; +} + +/// A provider-owned signing key, including external or non-exportable keys. +pub trait SigningKey: Send + Sync { + /// Reports whether this key can sign with `scheme`. + fn supports(&self, scheme: SignatureScheme) -> bool; + + /// Returns the public key with explicit encoding. + fn public_key(&self) -> PublicKey<'_>; + + /// Signs `message`. + fn sign(&self, scheme: SignatureScheme, message: &[u8]) -> Result, CryptoError>; + + /// Exports PKCS#8 when supported. `Ok(None)` means the key is non-exportable. + fn to_pkcs8_der(&self) -> Result, CryptoError> { + Ok(None) + } +} diff --git a/rtc-crypto/tests/conformance.rs b/rtc-crypto/tests/conformance.rs new file mode 100644 index 00000000..51035093 --- /dev/null +++ b/rtc-crypto/tests/conformance.rs @@ -0,0 +1,13 @@ +#![cfg(feature = "test-support")] + +#[cfg(feature = "crypto-ring")] +#[test] +fn ring_provider_conforms() { + rtc_crypto::conformance::assert_provider(&rtc_crypto::providers::RingProvider::new()); +} + +#[cfg(feature = "crypto-aws-lc-rs")] +#[test] +fn aws_lc_rs_provider_conforms() { + rtc_crypto::conformance::assert_provider(&rtc_crypto::providers::AwsLcRsProvider::new()); +} diff --git a/rtc-crypto/tests/cross_provider.rs b/rtc-crypto/tests/cross_provider.rs new file mode 100644 index 00000000..03894257 --- /dev/null +++ b/rtc-crypto/tests/cross_provider.rs @@ -0,0 +1,171 @@ +#![cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] + +use rtc_crypto::providers::{AwsLcRsProvider, RingProvider}; +use rtc_crypto::{ + AeadAlgorithm, CbcAlgorithm, KeyExchangeAlgorithm, RTCCrypto, RTCCryptoProvider, + SignatureScheme, StreamCipherAlgorithm, +}; + +fn providers() -> (RingProvider, AwsLcRsProvider) { + (RingProvider::new(), AwsLcRsProvider::new()) +} + +#[test] +fn symmetric_ciphertext_is_interoperable() { + let (ring, aws) = providers(); + cross_symmetric(ring.crypto(), aws.crypto()); + cross_symmetric(aws.crypto(), ring.crypto()); +} + +fn cross_symmetric(sealer: &dyn RTCCrypto, opener: &dyn RTCCrypto) { + for (algorithm, key_len) in [ + (AeadAlgorithm::Aes128Gcm, 16), + (AeadAlgorithm::Aes256Gcm, 32), + (AeadAlgorithm::Aes128Ccm, 16), + (AeadAlgorithm::Aes128Ccm8, 16), + (AeadAlgorithm::ChaCha20Poly1305, 32), + ] { + let key = vec![0x23; key_len]; + let plaintext = b"cross-provider authenticated encryption".to_vec(); + let mut buffer = plaintext.clone(); + let mut sealer = sealer.new_aead(algorithm, &key).unwrap(); + let mut tag = vec![0; sealer.tag_len()]; + sealer + .seal_in_place(&[0x45; 12], b"rtc", &mut buffer, &mut tag) + .unwrap(); + opener + .new_aead(algorithm, &key) + .unwrap() + .open_in_place(&[0x45; 12], b"rtc", &mut buffer, &tag) + .unwrap(); + assert_eq!(buffer, plaintext); + } + + for (algorithm, key_len) in [ + (StreamCipherAlgorithm::Aes128Ctr, 16), + (StreamCipherAlgorithm::Aes256Ctr, 32), + ] { + let key = vec![0x34; key_len]; + let plaintext = b"cross-provider stream cipher".to_vec(); + let mut buffer = plaintext.clone(); + sealer + .new_stream_cipher(algorithm, &key) + .unwrap() + .apply_keystream(&[0x56; 16], &mut buffer) + .unwrap(); + opener + .new_stream_cipher(algorithm, &key) + .unwrap() + .apply_keystream(&[0x56; 16], &mut buffer) + .unwrap(); + assert_eq!(buffer, plaintext); + } + + let key = [0x67; 32]; + let iv = [0x78; 16]; + let plaintext = [0x89; 32]; + let mut blocks = plaintext; + sealer + .new_cbc(CbcAlgorithm::Aes256Cbc, &key) + .unwrap() + .encrypt_blocks(&iv, &mut blocks) + .unwrap(); + opener + .new_cbc(CbcAlgorithm::Aes256Cbc, &key) + .unwrap() + .decrypt_blocks(&iv, &mut blocks) + .unwrap(); + assert_eq!(blocks, plaintext); +} + +#[test] +fn key_exchange_is_interoperable() { + let (ring, aws) = providers(); + for algorithm in [ + KeyExchangeAlgorithm::P256, + KeyExchangeAlgorithm::P384, + KeyExchangeAlgorithm::X25519, + ] { + let left = ring.crypto().start_key_exchange(algorithm).unwrap(); + let right = aws.crypto().start_key_exchange(algorithm).unwrap(); + let left_public = left.public_key().to_vec(); + let right_public = right.public_key().to_vec(); + let left_secret = left.complete(&right_public).unwrap(); + let right_secret = right.complete(&left_public).unwrap(); + assert_eq!(left_secret.as_ref(), right_secret.as_ref()); + } +} + +#[test] +fn generated_and_imported_signatures_are_interoperable() { + let (ring, aws) = providers(); + cross_signatures(ring.crypto(), aws.crypto()); + cross_signatures(aws.crypto(), ring.crypto()); + + let rsa_der = pem::parse(include_str!("data/rsa-2048.pkcs8.pem")) + .unwrap() + .into_contents(); + let ring_key = ring + .crypto() + .import_signing_key(SignatureScheme::RsaPkcs1Sha256, &rsa_der) + .unwrap(); + let aws_key = aws + .crypto() + .import_signing_key(SignatureScheme::RsaPkcs1Sha256, &rsa_der) + .unwrap(); + let message = b"cross-provider RSA"; + let ring_signature = ring_key + .sign(SignatureScheme::RsaPkcs1Sha256, message) + .unwrap(); + aws.crypto() + .verify_signature( + SignatureScheme::RsaPkcs1Sha256, + ring_key.public_key(), + message, + &ring_signature, + ) + .unwrap(); + let aws_signature = aws_key + .sign(SignatureScheme::RsaPkcs1Sha256, message) + .unwrap(); + ring.crypto() + .verify_signature( + SignatureScheme::RsaPkcs1Sha256, + aws_key.public_key(), + message, + &aws_signature, + ) + .unwrap(); +} + +fn cross_signatures(generator: &dyn RTCCrypto, verifier: &dyn RTCCrypto) { + for scheme in [SignatureScheme::Ed25519, SignatureScheme::EcdsaP256Sha256] { + let generated = generator.generate_signing_key(scheme).unwrap(); + let message = b"cross-provider signature"; + let signature = generated.sign(scheme, message).unwrap(); + verifier + .verify_signature(scheme, generated.public_key(), message, &signature) + .unwrap(); + + let exported = generated.to_pkcs8_der().unwrap().unwrap(); + let imported = verifier + .import_signing_key(scheme, exported.as_ref()) + .unwrap(); + let signature = imported.sign(scheme, message).unwrap(); + generator + .verify_signature(scheme, imported.public_key(), message, &signature) + .unwrap(); + } +} + +#[test] +fn ring_remains_the_default_when_both_backends_are_enabled() { + assert_eq!(rtc_crypto::default_provider().unwrap().name(), "ring"); + + let providers: Vec> = vec![ + Box::new(RingProvider::new()), + Box::new(AwsLcRsProvider::new()), + ]; + assert_eq!(providers[0].name(), "ring"); + assert_eq!(providers[1].name(), "aws-lc-rs"); +} diff --git a/rtc-crypto/tests/custom_provider.rs b/rtc-crypto/tests/custom_provider.rs new file mode 100644 index 00000000..14a0982d --- /dev/null +++ b/rtc-crypto/tests/custom_provider.rs @@ -0,0 +1,76 @@ +use rtc_crypto::{CryptoAlgorithm, CryptoError, RTCCrypto, RTCCryptoProvider, RTCRandom}; + +#[cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] +use rtc_crypto::default_provider; + +struct CustomProvider { + crypto: CustomCrypto, + random: CustomRandom, +} + +struct CustomCrypto; + +impl RTCCrypto for CustomCrypto { + fn supports(&self, _algorithm: CryptoAlgorithm) -> bool { + false + } +} + +struct CustomRandom; + +impl RTCRandom for CustomRandom { + fn fill(&self, output: &mut [u8]) -> Result<(), CryptoError> { + output.fill(0x5a); + Ok(()) + } +} + +#[cfg(feature = "test-support")] +struct FailingRandom; + +#[cfg(feature = "test-support")] +impl RTCRandom for FailingRandom { + fn fill(&self, _output: &mut [u8]) -> Result<(), CryptoError> { + Err(CryptoError::RandomnessFailed) + } +} + +impl RTCCryptoProvider for CustomProvider { + fn name(&self) -> &'static str { + "application-provider" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + &self.random + } +} + +#[test] +fn downstream_provider_requires_no_registration_or_backend_types() { + let provider: &dyn RTCCryptoProvider = &CustomProvider { + crypto: CustomCrypto, + random: CustomRandom, + }; + assert_eq!(provider.name(), "application-provider"); + let mut output = [0; 4]; + provider.random().fill(&mut output).unwrap(); + assert_eq!(output, [0x5a; 4]); + #[cfg(feature = "test-support")] + { + rtc_crypto::conformance::assert_unsupported_hash(provider.crypto()); + rtc_crypto::conformance::assert_random_failure(&FailingRandom); + } +} + +#[cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] +#[test] +fn no_builtin_provider_is_a_normal_error() { + assert!(matches!( + default_provider(), + Err(CryptoError::NoDefaultProvider) + )); +} diff --git a/rtc-crypto/tests/data/rsa-2048.pkcs8.pem b/rtc-crypto/tests/data/rsa-2048.pkcs8.pem new file mode 100644 index 00000000..8a1cf1b3 --- /dev/null +++ b/rtc-crypto/tests/data/rsa-2048.pkcs8.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCw+MSGjOm81NEW +MhYsA3a7Cdzi+sraonptStAbIXwKKeA2sb/QBSJU7D00k4MBnY/S1ciVrGeQvMbE +38p8JrvPVw7jvf9wgm+A6CVHdtjnO0MeK+zj19UV7dI62ojixxNqx5ZoXfikDtOM +5Z6u/CUJ4bJ3oDY5OOuOmYo/n9by7QEblp5I2J12UIkElh+Ng3VkZu/w8f1eRiHo +/wg/0Naqh7MeVgwYWhhiBZug+pX4ElrJa/x+BRw5llh9wScbs+v0kwMxTuiIzMhE +HeE6WbBkbZN137y9ZsFDU5jhZLxGcv1jou7xYqfBrH3bPpzbaM/qz/AfA2pVdHe0 +UXJZNyf1AgMBAAECggEABFM1bQW5RKh9f6mztFmqp4GN3heQ/kI69GbVVS8T+qTx +Wi+CkjvojeNuz3/Mvi0H8c17EdAH1pgL0jiXQ0IoaYsleFgRYReJpWsxq2Byhpwt +KhSavSrDhhNXhaFc97szyfKcxhTd2cHpq6eE9vPUl+bl11nurqapzcS02z81yp6z +rqbb0QfufF5TywinK78a0rKyY3mtF83z8kw9D3e1QF5r2AfrAK8vcGkZfOlK8mhk +3q3SaLKsWvB7emQ4ldigCM282jT3x+4vmPhOGbvtjsEpJEm28VTzIChqttHv2Rpg ++IVZccPVlyy4OEZoLqbnwP14rm5tX0R5cNjrxVIzQQKBgQD2yBbJ0UR/Cp3wKS3j +NTTGYDn7VYNBMlaXL4u2NKkNUGXHaUMykORx0IlQt1VpplX7qs4QHVwBekOJbgRa +aIjlT7QO8Z3EEQj/1Ba6fMmVad/fo6ToI2JAvT04gaSInYO2PkRyHNyi89+E2GpG +2yrY+2FGE2aNe+UdmjuW2zXvhQKBgQC3lRq9haGD+TvC/tGjzMgsMo0AaEaGqDm+ +k7M/+eCLsoT7xznloizM1UZ/jKg7jSOvqfzZs1Wps08DPZrCPyYJc9ak+vBimyWE +RTVHVxrmmqUpR8iehGdrTVAmbFOss2TnUK+io5+JCb3wRjt2RyQPWm5zSpHdl1wy +1oZICKNpsQKBgQCwOwlTDDd/BcTt6WpUk/1hIPynCFUYLOt7Qb/i2U5ULLLSKdCL +/r60rHgzBQlgziEe/MX06hJ3F6m9Lay8J2SDZVyvQ0on5wZnMz0b5dtK8PWnzkQI +ZqRWmQ1sGeC2ks2pSmQ0nXnOgJuBUc7rVL4Pf8zibx5QMUbX0fl17ItixQKBgHoB +oyLfg6c05Y3DUkodF8+fzOu/YVeux6mreY6EH8JX4199WTIO5N1AxLiSH2BsfZIK +VBvOvpiorVNHBuofk8Tmcnl0uHugBn/wiucdsagekLNtnJwU/LJoUGMozTdShjXg +/skFG0q06cGcu3nw77swa4U9wtFU/ZZf0iBfdVMRAoGAcZ2FcL4UsrBbIg6t8b/4 +K4wM5zNEt2o7kdVWLrrM0aBbWL1EEXFXLPXqhOEguSM4QL1ZCC8fxozYG0bNuiuj +y2R3K/AaeJDN/W9rM6x+Dbuj7QoOLDP9cSUf9iwaYtbrQcbDTk8SCzsyXle/bCli +L3Tuvt9w14f5auU05DpIfYQ= +-----END PRIVATE KEY----- diff --git a/rtc-crypto/tests/default_provider.rs b/rtc-crypto/tests/default_provider.rs new file mode 100644 index 00000000..b375da58 --- /dev/null +++ b/rtc-crypto/tests/default_provider.rs @@ -0,0 +1,11 @@ +#[cfg(feature = "crypto-ring")] +#[test] +fn ring_is_the_default_when_enabled() { + assert_eq!(rtc_crypto::default_provider().unwrap().name(), "ring"); +} + +#[cfg(all(not(feature = "crypto-ring"), feature = "crypto-aws-lc-rs"))] +#[test] +fn aws_lc_rs_is_the_default_when_it_is_the_only_builtin() { + assert_eq!(rtc_crypto::default_provider().unwrap().name(), "aws-lc-rs"); +} diff --git a/rtc-crypto/tests/rsa_import.rs b/rtc-crypto/tests/rsa_import.rs new file mode 100644 index 00000000..d140ab08 --- /dev/null +++ b/rtc-crypto/tests/rsa_import.rs @@ -0,0 +1,40 @@ +#![cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] + +use rtc_crypto::{RTCCryptoProvider, SignatureScheme}; + +#[cfg(feature = "crypto-ring")] +#[test] +fn ring_imports_and_uses_rsa_pkcs8() { + assert_rsa_import(&rtc_crypto::providers::RingProvider::new()); +} + +#[cfg(feature = "crypto-aws-lc-rs")] +#[test] +fn aws_lc_rs_imports_and_uses_rsa_pkcs8() { + assert_rsa_import(&rtc_crypto::providers::AwsLcRsProvider::new()); +} + +fn assert_rsa_import(provider: &dyn RTCCryptoProvider) { + let pkcs8 = pem::parse(include_str!("data/rsa-2048.pkcs8.pem")) + .unwrap() + .into_contents(); + let key = provider + .crypto() + .import_signing_key(SignatureScheme::RsaPkcs1Sha256, &pkcs8) + .unwrap(); + let message = b"RSA PKCS#8 import conformance"; + let signature = key.sign(SignatureScheme::RsaPkcs1Sha256, message).unwrap(); + provider + .crypto() + .verify_signature( + SignatureScheme::RsaPkcs1Sha256, + key.public_key(), + message, + &signature, + ) + .unwrap(); + assert_eq!( + key.to_pkcs8_der().unwrap().unwrap().as_ref(), + pkcs8.as_slice() + ); +} diff --git a/rtc-datachannel/src/data_channel/mod.rs b/rtc-datachannel/src/data_channel/mod.rs index 6e9f6892..445d74d1 100644 --- a/rtc-datachannel/src/data_channel/mod.rs +++ b/rtc-datachannel/src/data_channel/mod.rs @@ -1,3 +1,15 @@ +//! One data channel over an SCTP stream. +//! +//! A [`DataChannel`](crate::data_channel::DataChannel) is opened either by the DCEP handshake (`DATA_CHANNEL_OPEN`, then +//! `DATA_CHANNEL_ACK`) or out of band when [`DataChannelConfig::negotiated`](crate::data_channel::DataChannelConfig::negotiated) is set and both +//! sides already agreed the stream id through signalling. +//! +//! The reliability the channel is opened with maps onto SCTP send parameters: +//! [`get_reliability_params`](crate::data_channel::DataChannel::get_reliability_params) turns a +//! [`ChannelType`](crate::message::message_channel_open::ChannelType) into the ordered flag and +//! partial-reliability setting SCTP needs, and +//! [`get_channel_type_and_reliability_parameter`](crate::data_channel::DataChannel::get_channel_type_and_reliability_parameter) +//! goes the other way from the W3C `maxPacketLifeTime`/`maxRetransmits` pair. #[cfg(test)] mod data_channel_test; @@ -17,20 +29,35 @@ const RECEIVE_MTU: usize = 8192; /// DataChannelConfig is used to configure the data channel. #[derive(Eq, PartialEq, Default, Clone, Debug)] pub struct DataChannelConfig { + /// The reliability and ordering guarantees to request. pub channel_type: ChannelType, + /// Whether the channel was negotiated out of band. + /// + /// When `true` no DCEP `DATA_CHANNEL_OPEN` is sent — both sides are assumed to have agreed + /// the stream id and parameters through signalling instead. pub negotiated: bool, + /// The channel's relative priority; see the `CHANNEL_PRIORITY_*` constants. pub priority: u16, + /// The retransmission count or message lifetime, interpreted according to + /// [`Self::channel_type`]. pub reliability_parameter: u32, + /// The channel label, used to distinguish channels on the same association. pub label: String, + /// The subprotocol name, or empty if none. pub protocol: String, } /// DataChannelMessage is used to data sent over SCTP #[derive(Debug, Default, Clone)] pub struct DataChannelMessage { + /// Identifies the SCTP association this message belongs to. pub association_handle: usize, + /// The SCTP stream the message arrived on or should be sent on. pub stream_id: u16, + /// The payload protocol identifier, which distinguishes DCEP control messages from string + /// and binary user data. pub ppi: PayloadProtocolIdentifier, + /// The message bytes. pub payload: BytesMut, /// Marks a `DATA_CHANNEL_OPEN` that belongs to an out-of-band *negotiated* @@ -167,6 +194,7 @@ impl DataChannel { self.stream_id } + /// The configuration this channel was opened with. pub fn config(&self) -> &DataChannelConfig { &self.config } @@ -264,6 +292,10 @@ impl DataChannel { self.stream.on_buffered_amount_low(f) }*/ + /// Decomposes a [`ChannelType`] into the SCTP send parameters it implies. + /// + /// Returns whether delivery is unordered, together with the reliability type and value SCTP + /// needs for partial reliability. pub fn get_reliability_params(channel_type: ChannelType) -> (bool, ReliabilityType) { match channel_type { ChannelType::Reliable => (false, ReliabilityType::Reliable), @@ -275,6 +307,10 @@ impl DataChannel { } } + /// Derives the [`ChannelType`] and reliability parameter from the `maxPacketLifeTime` / + /// `maxRetransmits` pair the W3C API exposes. + /// + /// The two are mutually exclusive; supplying neither yields a fully reliable channel. pub fn get_channel_type_and_reliability_parameter( ordered: bool, max_retransmits: Option, @@ -315,6 +351,10 @@ impl DataChannel { (channel_type, reliability_parameter) } + /// Builds the payload protocol identifier and payload for a user message. + /// + /// Empty messages get their own identifiers (`StringEmpty`/`BinaryEmpty`) because SCTP + /// cannot carry a zero-length payload. pub fn get_data_channel_message(is_string: bool, data: BytesMut) -> DataChannelMessage { // https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-12#section-6.6 // SCTP does not support the sending of empty user messages. Therefore, diff --git a/rtc-datachannel/src/lib.rs b/rtc-datachannel/src/lib.rs index f4b2e477..a46d58ec 100644 --- a/rtc-datachannel/src/lib.rs +++ b/rtc-datachannel/src/lib.rs @@ -1,5 +1,56 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! WebRTC data channels over SCTP. +//! +//! The Data Channel Establishment Protocol ([RFC 8832]) and the SCTP-based data channel +//! layer ([RFC 8831]): the `DATA_CHANNEL_OPEN` handshake, the reliability and ordering +//! parameters, and the payload protocol identifiers that distinguish string from binary +//! messages. +//! +//! # Structure +//! +//! * [`data_channel`] — one channel's state and its read/write surface over an SCTP +//! stream, including partial-reliability settings (`maxPacketLifeTime`, +//! `maxRetransmits`). +//! * [`message`] — the DCEP messages themselves: `DataChannelOpen`, `DataChannelAck`, and +//! the channel-type encoding. +//! +//! # Example +//! +//! ``` +//! use bytes::Bytes; +//! use rtc_datachannel::message::message_channel_open::{ChannelType, DataChannelOpen}; +//! use rtc_datachannel::message::{Message, message_type::MessageType}; +//! use shared::marshal::{Marshal, Unmarshal}; +//! +//! # fn example() -> Result<(), Box> { +//! let open = Message::DataChannelOpen(DataChannelOpen { +//! channel_type: ChannelType::PartialReliableRexmit, +//! priority: 256, +//! reliability_parameter: 3, // give up after 3 retransmissions +//! label: b"chat".to_vec(), +//! protocol: Vec::new(), +//! }); +//! assert_eq!(open.message_type(), MessageType::DataChannelOpen); +//! +//! let encoded = open.marshal()?; +//! let mut buf = Bytes::from(encoded.to_vec()); +//! assert_eq!(Message::unmarshal(&mut buf)?, open); +//! # Ok(()) +//! # } +//! ``` +//! +//! Most applications do not depend on this crate directly — the +//! [`rtc`](https://docs.rs/rtc) crate layers it over [`rtc-sctp`] and exposes +//! `RTCDataChannel`. +//! +//! [RFC 8832]: https://datatracker.ietf.org/doc/html/rfc8832 +//! [RFC 8831]: https://datatracker.ietf.org/doc/html/rfc8831 +//! [`rtc-sctp`]: https://docs.rs/rtc-sctp + +/// One data channel's state and its read/write surface over an SCTP stream. pub mod data_channel; +/// The DCEP messages exchanged to open, acknowledge and close a channel. pub mod message; diff --git a/rtc-datachannel/src/message/message_channel_open.rs b/rtc-datachannel/src/message/message_channel_open.rs index 56db1b24..d628ffa2 100644 --- a/rtc-datachannel/src/message/message_channel_open.rs +++ b/rtc-datachannel/src/message/message_channel_open.rs @@ -11,37 +11,42 @@ const CHANNEL_TYPE_LEN: usize = 1; /// ChannelPriority pub const CHANNEL_PRIORITY_BELOW_NORMAL: u16 = 128; +/// The default channel priority. pub const CHANNEL_PRIORITY_NORMAL: u16 = 256; +/// A higher-than-default channel priority. pub const CHANNEL_PRIORITY_HIGH: u16 = 512; +/// The highest channel priority defined by the spec. pub const CHANNEL_PRIORITY_EXTRA_HIGH: u16 = 1024; #[derive(Default, Eq, PartialEq, Copy, Clone, Debug)] +/// The reliability and ordering guarantees a data channel is opened with. +/// +/// Carried in the `DATA_CHANNEL_OPEN` message ([RFC 8832] §5.1); `reliability_parameter` +/// supplies the retransmission count or lifetime for the partial-reliability variants. +/// +/// [RFC 8832]: https://datatracker.ietf.org/doc/html/rfc8832 +#[non_exhaustive] pub enum ChannelType { - // `Reliable` determines the Data Channel provides a - // reliable in-order bi-directional communication. + /// Reliable, in-order delivery — the SCTP default, and what `RTCDataChannel` gives you + /// unless you ask otherwise. #[default] Reliable, - // `ReliableUnordered` determines the Data Channel - // provides a reliable unordered bi-directional communication. + /// Reliable delivery, but messages may arrive out of order. ReliableUnordered, - // `PartialReliableRexmit` determines the Data Channel - // provides a partially-reliable in-order bi-directional communication. - // User messages will not be retransmitted more times than specified in the Reliability Parameter. + /// Partially reliable and in-order: a message is retransmitted at most + /// `reliability_parameter` times before being abandoned. + /// + /// This is what `maxRetransmits` maps to. PartialReliableRexmit, - // `PartialReliableRexmitUnordered` determines - // the Data Channel provides a partial reliable unordered bi-directional communication. - // User messages will not be retransmitted more times than specified in the Reliability Parameter. + /// As [`Self::PartialReliableRexmit`], but messages may arrive out of order. PartialReliableRexmitUnordered, - // `PartialReliableTimed` determines the Data Channel - // provides a partial reliable in-order bi-directional communication. - // User messages might not be transmitted or retransmitted after - // a specified life-time given in milli- seconds in the Reliability Parameter. - // This life-time starts when providing the user message to the protocol stack. + /// Partially reliable and in-order: a message is abandoned once + /// `reliability_parameter` milliseconds have passed. + /// + /// The lifetime starts when the message is handed to the stack, not when it is first + /// transmitted. This is what `maxPacketLifeTime` maps to. PartialReliableTimed, - // The Data Channel provides a partial reliable unordered bi-directional - // communication. User messages might not be transmitted or retransmitted - // after a specified life-time given in milli- seconds in the Reliability Parameter. - // This life-time starts when providing the user message to the protocol stack. + /// As [`Self::PartialReliableTimed`], but messages may arrive out of order. PartialReliableTimedUnordered, } @@ -135,10 +140,16 @@ const CHANNEL_OPEN_HEADER_LEN: usize = 11; /// ``` #[derive(Eq, PartialEq, Clone, Debug)] pub struct DataChannelOpen { + /// The reliability and ordering guarantees requested for the channel. pub channel_type: ChannelType, + /// The channel's relative priority, one of the `CHANNEL_PRIORITY_*` constants. pub priority: u16, + /// The retransmission count or message lifetime in milliseconds, interpreted according to + /// [`Self::channel_type`]. pub reliability_parameter: u32, + /// The channel label, as UTF-8 bytes. pub label: Vec, + /// The subprotocol name, as UTF-8 bytes. Empty if none was negotiated. pub protocol: Vec, } diff --git a/rtc-datachannel/src/message/message_channel_threshold.rs b/rtc-datachannel/src/message/message_channel_threshold.rs index 01f24536..24cbfe8c 100644 --- a/rtc-datachannel/src/message/message_channel_threshold.rs +++ b/rtc-datachannel/src/message/message_channel_threshold.rs @@ -13,8 +13,14 @@ use shared::error::Result; ///+-+-+-+-+-+-+-+-+ /// ``` #[derive(Eq, PartialEq, Copy, Clone, Debug)] +/// A buffered-amount threshold crossing, reported internally so the channel can raise +/// `OnBufferedAmountLow`/`OnBufferedAmountHigh`. +/// +/// Not a DCEP message — it never appears on the wire. pub enum DataChannelThreshold { + /// The buffered amount fell to or below the low threshold, carrying its value. Low(u32), + /// The buffered amount rose to or above the high threshold, carrying its value. High(u32), } // internal usage only diff --git a/rtc-datachannel/src/message/message_type.rs b/rtc-datachannel/src/message/message_type.rs index 08d0206a..4ecdc2e7 100644 --- a/rtc-datachannel/src/message/message_type.rs +++ b/rtc-datachannel/src/message/message_type.rs @@ -8,12 +8,17 @@ pub(crate) const MESSAGE_TYPE_ACK: u8 = 0x02; pub(crate) const MESSAGE_TYPE_OPEN: u8 = 0x03; pub(crate) const MESSAGE_TYPE_LEN: usize = 1; -// A parsed DataChannel message +/// The one-byte type that prefixes a DCEP message. #[derive(Eq, PartialEq, Copy, Clone, Debug)] +#[non_exhaustive] pub enum MessageType { + /// A buffered-amount threshold crossing. Internal to this crate. DataChannelThreshold, // internal usage only - DataChannelClose, // internal usage only + /// A channel close notification. Internal to this crate. + DataChannelClose, // internal usage only + /// `DATA_CHANNEL_ACK` (`0x02`). DataChannelAck, + /// `DATA_CHANNEL_OPEN` (`0x03`). DataChannelOpen, } diff --git a/rtc-datachannel/src/message/mod.rs b/rtc-datachannel/src/message/mod.rs index b062bf3d..d655ce76 100644 --- a/rtc-datachannel/src/message/mod.rs +++ b/rtc-datachannel/src/message/mod.rs @@ -1,10 +1,15 @@ #[cfg(test)] mod message_test; +/// The `DATA_CHANNEL_ACK` message, which confirms a channel was opened. pub mod message_channel_ack; +/// An internal close notification (not a DCEP message). pub mod message_channel_close; +/// The `DATA_CHANNEL_OPEN` message and the channel parameters it carries. pub mod message_channel_open; +/// Internal buffered-amount threshold notifications. pub mod message_channel_threshold; +/// The one-byte message type that prefixes every DCEP message. pub mod message_type; use bytes::{Buf, BufMut}; @@ -18,10 +23,16 @@ use shared::marshal::*; /// A parsed DataChannel message #[derive(Eq, PartialEq, Clone, Debug)] +#[non_exhaustive] pub enum Message { + /// A buffered-amount threshold crossing. Internal to this crate — not a DCEP message. DataChannelThreshold(DataChannelThreshold), // internal usage only - DataChannelClose(DataChannelClose), // internal usage only + /// A channel close notification. Internal to this crate — not a DCEP message. + DataChannelClose(DataChannelClose), // internal usage only + /// `DATA_CHANNEL_ACK`, sent by the peer to confirm it accepted a `DATA_CHANNEL_OPEN`. DataChannelAck(DataChannelAck), + /// `DATA_CHANNEL_OPEN`, which opens a channel and carries its label, protocol and + /// reliability parameters. DataChannelOpen(DataChannelOpen), } @@ -79,6 +90,7 @@ impl Unmarshal for Message { } impl Message { + /// The type byte that identifies this message on the wire. pub fn message_type(&self) -> MessageType { match self { Self::DataChannelThreshold(_) => MessageType::DataChannelThreshold, // internal usage only diff --git a/rtc-dtls/Cargo.toml b/rtc-dtls/Cargo.toml index af53a007..5ad4d75f 100644 --- a/rtc-dtls/Cargo.toml +++ b/rtc-dtls/Cargo.toml @@ -11,53 +11,36 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[features] +default = ["crypto-ring"] +crypto-ring = ["crypto/crypto-ring", "rustls/ring", "rcgen/ring"] +crypto-aws-lc-rs = ["crypto/crypto-aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs"] + [dependencies] -shared = { workspace = true, default-features = false, features = ["crypto", "replay"] } +shared = { workspace = true, default-features = false, features = ["replay"] } +crypto.workspace = true bytes.workspace = true byteorder.workspace = true -rand_core = "0.6.4" -p256 = { version = "0.13.2", features = ["default", "ecdh", "ecdsa"] } -p384 = "0.13.0" -rand.workspace = true -hmac = "0.12.1" -sec1 = { version = "0.7", features = ["std"] } -sha1 = "0.10.6" -sha2 = "0.10.8" -aes = "0.8.4" -cbc = { version = "0.1.2", features = ["block-padding", "alloc"] } -# AES-GCM cipher suites use ring's hardware-accelerated AES-GCM (see -# crypto/crypto_gcm.rs); CCM/CBC/ChaCha20-Poly1305 stay on RustCrypto. -ccm = "0.5.0" -x25519-dalek = { version = "2.0.1", features = ["static_secrets"] } -x509-parser = "0.16.0" -der-parser = "9.0.0" +x509-parser.workspace = true +der-parser.workspace = true rcgen.workspace = true -ring = { workspace = true, optional = true } -aws-lc-rs = { workspace = true, optional = true } -rustls = { version = "0.23.27", default-features = false, features = ["std"] } -rkyv = "0.8" -bytecheck = "0.8" -subtle = "2.5.0" +rustls.workspace = true +rkyv.workspace = true +bytecheck.workspace = true log.workspace = true -pem = { version = "3.0.3", optional = true } -chacha20poly1305 = "0.10.1" +pem.workspace = true [dev-dependencies] -local-sync = "0.1.1" -core_affinity = "0.8.1" +criterion.workspace = true +local-sync.workspace = true +core_affinity.workspace = true chrono.workspace = true env_logger.workspace = true clap.workspace = true -anyhow = "1.0.80" +anyhow.workspace = true ctrlc.workspace = true -futures = "0.3.30" - -[features] -default = ["ring"] -pem = ["dep:pem"] -ring = ["dep:ring", "rustls/ring", "rcgen/ring"] -aws-lc-rs = ["dep:aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs"] +futures.workspace = true #[[example]] #name = "dtls_chat_server" @@ -104,3 +87,7 @@ aws-lc-rs = ["dep:aws-lc-rs", "rustls/aws-lc-rs", "rcgen/aws_lc_rs"] #name = "listen_verify" #path = "examples/listen/verify/listen_verify.rs" #bench = false + +[[bench]] +name = "record_protection" +harness = false diff --git a/rtc-dtls/benches/README.md b/rtc-dtls/benches/README.md new file mode 100644 index 00000000..1f9f9106 --- /dev/null +++ b/rtc-dtls/benches/README.md @@ -0,0 +1,104 @@ +# DTLS record-protection benchmarks + +```bash +cargo bench --package rtc-dtls --bench record_protection +cargo bench --package rtc-dtls --bench record_protection --no-default-features --features crypto-aws-lc-rs +``` + +`Setup/*` constructs a cipher — provider dispatch, key import, key schedule, and (after G3) keying +the record MAC. Paid once per DTLS epoch. `Encrypt/*` and `Decrypt/*` protect one 1200-byte record +on an already-keyed cipher; that is the hot path. + +## G3 crypto-provider migration: measured impact + +One machine (Apple M1 Max, macOS 26.5.2), identical criterion settings, comparing a worktree at +`fd81f68` (P4 — the last commit before DTLS moved to `rtc-crypto`) against the current tree. + +| Benchmark | Pre-migration | ring (default) | aws-lc-rs | +|---|---|---|---| +| `Setup/AES-128-GCM` | 274.2 ns | 326.9 ns | 296.6 ns | +| `Encrypt/AES-128-GCM` | 270.5 ns | 270.1 ns | 245.8 ns | +| `Decrypt/AES-128-GCM` | 280.6 ns | 274.8 ns | 225.6 ns | +| `Setup/AES-256-CBC` | 86.5 ns | 755.9 ns | 818.4 ns | +| `Encrypt/AES-256-CBC` | 3.419 µs | 3.204 µs | 2.498 µs | +| `Decrypt/AES-256-CBC` | 2.036 µs | 1.882 µs | 1.194 µs | + +**Every per-record path is at parity or better than before the migration**, on both providers. + +`Setup/*` rose, which is the intended trade: the record MAC key schedule moved there from the +per-record path, so it is paid once per epoch instead of once per record. + +Getting here took three fixes, each found by an initial measurement that showed encryption +3-8x slower. They are documented below in the order they were diagnosed. + +## Fixed: per-record `SystemRandom` + +DTLS generates fresh randomness per record — the GCM explicit nonce (`crypto_gcm.rs`, RFC 5288 +§3) and the CBC record IV (`crypto_cbc.rs`, RFC 5246 §6.2.3.2). Decryption does not, which is why +only encryption had regressed, and why `aws-lc-rs` — faster everywhere else — was the worse of the +two. + +Before G3 that randomness came from `rand::rng()`, a thread-local ChaCha CSPRNG. After it, it went +through `RTCRandom`, whose built-ins called the backend's `SystemRandom`, which reaches the +operating system on every call: + +| 8-byte fill | ns/call | +|---|---| +| `rand::fill` (thread-local) | **8.3** | +| `ring::rand::SystemRandom` | 829.1 | +| `aws_lc_rs::rand::SystemRandom` | 2196.5 | + +That accounted for the deltas almost exactly. Caching one `SystemRandom` instead of constructing +per call recovers only ~9% — the OS round trip dominates, so the handle was never the problem. + +The built-in `RTCRandom` implementations now use an OS-seeded, periodically reseeded thread-local +CSPRNG (`common::fill_random`), restoring what the pre-provider code did and what BoringSSL and +OpenSSL do internally. `SystemRandom` is still used where the backend owns the operation — +keypair generation and signing. + +| Benchmark (ring) | Backend `SystemRandom` | Thread-local CSPRNG | +|---|---|---| +| `Encrypt/AES-128-GCM` | 1.015 µs | **262.4 ns** | +| `Encrypt/AES-256-CBC` | 6.928 µs | 6.003 µs | + +A deployment that requires every byte of entropy to come from a validated module supplies its own +`RTCRandom`; that is what the trait is for. + +## Fixed: per-record HMAC key setup in CBC + +`CryptoCbc` passed raw key bytes to `prf_mac` on every record, so the HMAC key schedule was +re-derived per record — the same defect found in SRTP. It now holds two keyed `Mac` objects, keyed +once per epoch, and `prf_mac` takes `&mut dyn Mac` rather than a crypto handle plus a key. + +| Benchmark (ring) | Per-record keying | Pre-keyed `Mac` | +|---|---|---| +| `Encrypt/AES-256-CBC` | 7.240 µs | 6.928 µs | +| `Decrypt/AES-256-CBC` | 5.127 µs | 4.749 µs | + +`Setup/AES-256-CBC` rises correspondingly: two MACs are keyed there instead of on every record. + +## Fixed: `ring`'s software SHA-1 + +CBC authenticates every record with HMAC-SHA1, and `ring` exposes SHA-1 only as +`HMAC_SHA1_FOR_LEGACY_USE_ONLY`, without the ARMv8 SHA-1 instructions — 4469 ns against +RustCrypto's 1373 ns over 1212 bytes. The `ring` provider now composes RustCrypto's HMAC-SHA1, as +it already composes RustCrypto for AES-CTR, CCM, CBC and MD5. See `rtc-srtp/benches/README.md`. + +| Benchmark (ring) | `ring` SHA-1 | RustCrypto SHA-1 | +|---|---|---| +| `Encrypt/AES-256-CBC` | 6.003 µs | **3.204 µs** | +| `Decrypt/AES-256-CBC` | 4.702 µs | **1.882 µs** | + +## Reproducing + +```bash +# Current +cargo bench --package rtc-dtls --bench record_protection -- --warm-up-time 2 --measurement-time 4 + +# Pre-migration baseline: the bench does not exist at fd81f68, so port this file to the +# pre-G3 API — CryptoGcm::new / CryptoCbc::new took keys directly, without a provider. +git worktree add /tmp/rtc-dtls-base fd81f68 +``` + +The methodology, including why cross-machine numbers must not be compared, is in +`docs/benchmarking-crypto-migration.md`. diff --git a/rtc-dtls/benches/record_protection.rs b/rtc-dtls/benches/record_protection.rs new file mode 100644 index 00000000..f40ab032 --- /dev/null +++ b/rtc-dtls/benches/record_protection.rs @@ -0,0 +1,144 @@ +//! DTLS record-protection benchmarks. +//! +//! Measures the per-record cost of each record cipher after the G3 crypto-provider migration, +//! and separates it from one-time key-schedule setup: +//! +//! * `Setup/*` constructs a cipher, so it covers the provider lookup, key import, and key +//! schedule. This happens once per DTLS epoch. +//! * `Encrypt/*` and `Decrypt/*` protect and unprotect a single record on an already-keyed +//! cipher. This is the hot path and the number that matters for throughput. +//! +//! The split exists because the provider indirection is deliberately concentrated in setup: a +//! keyed cipher object is obtained once and then used per record with no further provider +//! dispatch. A regression in `Encrypt/*` would mean that property broke. +//! +//! Every cipher runs against each enabled built-in provider under identical inputs, so the two +//! backends are directly comparable. +//! +//! Run with: +//! +//! ```text +//! cargo bench --package rtc-dtls --bench record_protection +//! cargo bench --package rtc-dtls --bench record_protection --no-default-features --features aws-lc-rs +//! ``` + +use std::sync::Arc; + +use criterion::measurement::WallTime; +use criterion::{BenchmarkGroup, Criterion, criterion_main}; + +use crypto::RTCCryptoProvider; +use rtc_dtls::content::ContentType; +use rtc_dtls::crypto::crypto_cbc::CryptoCbc; +use rtc_dtls::crypto::crypto_gcm::CryptoGcm; +use rtc_dtls::record_layer::record_layer_header::{ + PROTOCOL_VERSION1_2, RECORD_LAYER_HEADER_SIZE, RecordLayerHeader, +}; + +/// A 1200-byte record, sized to a typical WebRTC MTU-bound payload. +const PAYLOAD_LEN: usize = 1200; + +const KEY_128: &[u8] = &[ + 0x60, 0xb4, 0x1f, 0x04, 0x77, 0x89, 0x80, 0xfc, 0x4b, 0xc2, 0xfc, 0x2c, 0x3f, 0x38, 0x3d, 0x37, +]; +const KEY_256: &[u8] = &[ + 0x60, 0xb4, 0x1f, 0x04, 0x77, 0x89, 0x80, 0xfc, 0x4b, 0xc2, 0xfc, 0x2c, 0x3f, 0x38, 0x3d, 0x37, + 0xf7, 0x1a, 0x31, 0x5e, 0x63, 0x1d, 0x4f, 0x5e, 0x05, 0x6f, 0xfc, 0xd8, 0x3e, 0xc3, 0x11, 0x22, +]; +const IV_GCM: &[u8] = &[0xf7, 0x1a, 0x31, 0x5e]; +const MAC_SHA1: &[u8] = &[ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, + 0x11, 0x12, 0x13, 0x14, +]; + +/// A DTLS 1.2 application-data record: marshalled header followed by the payload. +/// +/// The header must be marshalled rather than zero-filled, because `decrypt` re-parses it from +/// the ciphertext buffer. +fn record() -> (RecordLayerHeader, Vec) { + let header = RecordLayerHeader { + content_type: ContentType::ApplicationData, + protocol_version: PROTOCOL_VERSION1_2, + epoch: 1, + sequence_number: 1, + content_len: PAYLOAD_LEN as u16, + }; + let mut raw = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + PAYLOAD_LEN); + header.marshal(&mut raw).unwrap(); + raw.extend((0..PAYLOAD_LEN).map(|index| index as u8)); + (header, raw) +} + +/// The built-in providers compiled into this benchmark. +fn providers() -> Vec<(&'static str, Arc)> { + let mut providers: Vec<(&'static str, Arc)> = Vec::new(); + #[cfg(feature = "crypto-ring")] + providers.push(("ring", Arc::new(crypto::providers::RingProvider::default()))); + #[cfg(feature = "crypto-aws-lc-rs")] + providers.push(( + "aws-lc-rs", + Arc::new(crypto::providers::AwsLcRsProvider::default()), + )); + assert!( + !providers.is_empty(), + "enable `ring` or `aws-lc-rs` to run these benchmarks" + ); + providers +} + +fn benchmark_gcm(group: &mut BenchmarkGroup) { + for (name, provider) in providers() { + // One-time cost: provider dispatch, key import, key schedule. + group.bench_function(format!("Setup/AES-128-GCM/{name}"), |b| { + b.iter(|| { + CryptoGcm::new(Arc::clone(&provider), KEY_128, IV_GCM, KEY_128, IV_GCM).unwrap() + }); + }); + + let mut cipher = + CryptoGcm::new(Arc::clone(&provider), KEY_128, IV_GCM, KEY_128, IV_GCM).unwrap(); + let (header, raw) = record(); + + group.bench_function(format!("Encrypt/AES-128-GCM/{name}"), |b| { + b.iter(|| cipher.encrypt(&header, &raw).unwrap()); + }); + + let encrypted = cipher.encrypt(&header, &raw).unwrap(); + group.bench_function(format!("Decrypt/AES-128-GCM/{name}"), |b| { + b.iter(|| cipher.decrypt(&encrypted).unwrap()); + }); + } +} + +fn benchmark_cbc(group: &mut BenchmarkGroup) { + for (name, provider) in providers() { + group.bench_function(format!("Setup/AES-256-CBC/{name}"), |b| { + b.iter(|| { + CryptoCbc::new(Arc::clone(&provider), KEY_256, MAC_SHA1, KEY_256, MAC_SHA1).unwrap() + }); + }); + + let mut cipher = + CryptoCbc::new(Arc::clone(&provider), KEY_256, MAC_SHA1, KEY_256, MAC_SHA1).unwrap(); + let (header, raw) = record(); + + group.bench_function(format!("Encrypt/AES-256-CBC/{name}"), |b| { + b.iter(|| cipher.encrypt(&header, &raw).unwrap()); + }); + + let encrypted = cipher.encrypt(&header, &raw).unwrap(); + group.bench_function(format!("Decrypt/AES-256-CBC/{name}"), |b| { + b.iter(|| cipher.decrypt(&encrypted).unwrap()); + }); + } +} + +fn benches() { + let mut criterion = Criterion::default().configure_from_args(); + let mut group = criterion.benchmark_group("DTLS"); + benchmark_gcm(&mut group); + benchmark_cbc(&mut group); + group.finish(); +} + +criterion_main!(benches); diff --git a/rtc-dtls/examples/dtls_micro.rs b/rtc-dtls/examples/dtls_micro.rs index e5df6451..e23c35c7 100644 --- a/rtc-dtls/examples/dtls_micro.rs +++ b/rtc-dtls/examples/dtls_micro.rs @@ -25,8 +25,17 @@ fn main() { let remote_iv = [0x44u8; 4]; // Encrypt-side and decrypt-side contexts that mirror each other. - let sender = CryptoGcm::new(&local_key, &local_iv, &remote_key, &remote_iv); - let receiver = CryptoGcm::new(&remote_key, &remote_iv, &local_key, &local_iv); + let provider = crypto::default_provider().expect("a default crypto provider"); + let mut sender = CryptoGcm::new( + provider.clone(), + &local_key, + &local_iv, + &remote_key, + &remote_iv, + ) + .expect("create sender cipher"); + let mut receiver = CryptoGcm::new(provider, &remote_key, &remote_iv, &local_key, &local_iv) + .expect("create receiver cipher"); let header = RecordLayerHeader { content_type: ContentType::ApplicationData, diff --git a/rtc-dtls/src/alert/mod.rs b/rtc-dtls/src/alert/mod.rs index 9f4b7973..e818d5e1 100644 --- a/rtc-dtls/src/alert/mod.rs +++ b/rtc-dtls/src/alert/mod.rs @@ -161,14 +161,21 @@ impl fmt::Display for Alert { } impl Alert { + /// The record content type this message is carried in. pub fn content_type(&self) -> ContentType { ContentType::Alert } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 2 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(self.alert_level as u8)?; writer.write_u8(self.alert_description as u8)?; @@ -176,6 +183,11 @@ impl Alert { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let alert_level = reader.read_u8()?.into(); let alert_description = reader.read_u8()?.into(); diff --git a/rtc-dtls/src/application_data.rs b/rtc-dtls/src/application_data.rs index 0576771c..bc2b4963 100644 --- a/rtc-dtls/src/application_data.rs +++ b/rtc-dtls/src/application_data.rs @@ -15,24 +15,37 @@ use shared::error::Result; /// [RFC 5246 §10]: https://tools.ietf.org/html/rfc5246#section-10 #[derive(Clone, PartialEq, Eq, Debug)] pub struct ApplicationData { + /// The application payload. pub data: BytesMut, } impl ApplicationData { + /// The record content type this message is carried in. pub fn content_type(&self) -> ContentType { ContentType::ApplicationData } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { self.data.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_all(&self.data)?; Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { // Read straight into the BytesMut-backed Vec instead of staging in a // temporary Vec and copying the whole payload a second time. diff --git a/rtc-dtls/src/change_cipher_spec/mod.rs b/rtc-dtls/src/change_cipher_spec/mod.rs index ad884061..736e25fa 100644 --- a/rtc-dtls/src/change_cipher_spec/mod.rs +++ b/rtc-dtls/src/change_cipher_spec/mod.rs @@ -20,20 +20,32 @@ use shared::error::*; pub struct ChangeCipherSpec; impl ChangeCipherSpec { + /// The record content type this message is carried in. pub fn content_type(&self) -> ContentType { ContentType::ChangeCipherSpec } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 1 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(0x01)?; Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let data = reader.read_u8()?; if data != 0x01 { diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs b/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs index 3ab6fbdc..0389724d 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_ccm.rs @@ -3,7 +3,7 @@ use crate::client_certificate_type::ClientCertificateType; use crate::crypto::crypto_ccm::{CryptoCcm, CryptoCcmTagLen}; use crate::prf::*; -#[derive(Clone)] +/// The shared AES-128-CCM implementation, parameterized over the key exchange and signature. pub struct CipherSuiteAes128Ccm { ccm: Option, client_certificate_type: ClientCertificateType, @@ -17,6 +17,7 @@ impl CipherSuiteAes128Ccm { const PRF_KEY_LEN: usize = 16; const PRF_IV_LEN: usize = 4; + /// Builds an uninitialized AES-128-CCM suite; keys are installed later via `init`. pub fn new( client_certificate_type: ClientCertificateType, id: CipherSuiteId, @@ -60,51 +61,57 @@ impl CipherSuite for CipherSuiteAes128Ccm { fn init( &mut self, + provider: Arc, master_secret: &[u8], client_random: &[u8], server_random: &[u8], is_client: bool, ) -> Result<()> { let keys = prf_encryption_keys( + provider.crypto(), master_secret, client_random, server_random, - CipherSuiteAes128Ccm::PRF_MAC_LEN, - CipherSuiteAes128Ccm::PRF_KEY_LEN, - CipherSuiteAes128Ccm::PRF_IV_LEN, + EncryptionKeyLengths { + mac: CipherSuiteAes128Ccm::PRF_MAC_LEN, + key: CipherSuiteAes128Ccm::PRF_KEY_LEN, + iv: CipherSuiteAes128Ccm::PRF_IV_LEN, + }, self.hash_func(), )?; if is_client { self.ccm = Some(CryptoCcm::new( + provider, &self.crypto_ccm_tag_len, &keys.client_write_key, &keys.client_write_iv, &keys.server_write_key, &keys.server_write_iv, - )); + )?); } else { self.ccm = Some(CryptoCcm::new( + provider, &self.crypto_ccm_tag_len, &keys.server_write_key, &keys.server_write_iv, &keys.client_write_key, &keys.client_write_iv, - )); + )?); } Ok(()) } - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let ccm = self.ccm.as_ref().ok_or(Error::Other( + fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let ccm = self.ccm.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to encrypt".to_owned(), ))?; ccm.encrypt(pkt_rlh, raw) } - fn decrypt(&self, input: &[u8]) -> Result> { - let ccm = self.ccm.as_ref().ok_or(Error::Other( + fn decrypt(&mut self, input: &[u8]) -> Result> { + let ccm = self.ccm.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to decrypt".to_owned(), ))?; ccm.decrypt(input) diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs b/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs index 0c3d5ffd..08500b25 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_aes_128_gcm_sha256.rs @@ -2,7 +2,7 @@ use super::*; use crate::crypto::crypto_gcm::*; use crate::prf::*; -#[derive(Clone)] +/// The shared AES-128-GCM with SHA-256 implementation, parameterized over the key exchange and signature. pub struct CipherSuiteAes128GcmSha256 { gcm: Option, rsa: bool, @@ -13,6 +13,7 @@ impl CipherSuiteAes128GcmSha256 { const PRF_KEY_LEN: usize = 16; const PRF_IV_LEN: usize = 4; + /// Builds an uninitialized AES-128-GCM with SHA-256 suite; keys are installed later via `init`. pub fn new(rsa: bool) -> Self { CipherSuiteAes128GcmSha256 { gcm: None, rsa } } @@ -57,49 +58,55 @@ impl CipherSuite for CipherSuiteAes128GcmSha256 { fn init( &mut self, + provider: Arc, master_secret: &[u8], client_random: &[u8], server_random: &[u8], is_client: bool, ) -> Result<()> { let keys = prf_encryption_keys( + provider.crypto(), master_secret, client_random, server_random, - CipherSuiteAes128GcmSha256::PRF_MAC_LEN, - CipherSuiteAes128GcmSha256::PRF_KEY_LEN, - CipherSuiteAes128GcmSha256::PRF_IV_LEN, + EncryptionKeyLengths { + mac: CipherSuiteAes128GcmSha256::PRF_MAC_LEN, + key: CipherSuiteAes128GcmSha256::PRF_KEY_LEN, + iv: CipherSuiteAes128GcmSha256::PRF_IV_LEN, + }, self.hash_func(), )?; if is_client { self.gcm = Some(CryptoGcm::new( + provider, &keys.client_write_key, &keys.client_write_iv, &keys.server_write_key, &keys.server_write_iv, - )); + )?); } else { self.gcm = Some(CryptoGcm::new( + provider, &keys.server_write_key, &keys.server_write_iv, &keys.client_write_key, &keys.client_write_iv, - )); + )?); } Ok(()) } - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let cg = self.gcm.as_ref().ok_or(Error::Other( + fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let cg = self.gcm.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to encrypt".to_owned(), ))?; cg.encrypt(pkt_rlh, raw) } - fn decrypt(&self, input: &[u8]) -> Result> { - let cg = self.gcm.as_ref().ok_or(Error::Other( + fn decrypt(&mut self, input: &[u8]) -> Result> { + let cg = self.gcm.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to decrypt".to_owned(), ))?; cg.decrypt(input) diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs b/rtc-dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs index 3654f93f..f6989646 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_aes_256_cbc_sha.rs @@ -2,7 +2,7 @@ use super::*; use crate::crypto::crypto_cbc::*; use crate::prf::*; -#[derive(Clone)] +/// The shared AES-256-CBC with SHA-1 implementation, parameterized over the key exchange and signature. pub struct CipherSuiteAes256CbcSha { cbc: Option, rsa: bool, @@ -13,6 +13,7 @@ impl CipherSuiteAes256CbcSha { const PRF_KEY_LEN: usize = 32; const PRF_IV_LEN: usize = 16; + /// Builds an uninitialized AES-256-CBC with SHA-1 suite; keys are installed later via `init`. pub fn new(rsa: bool) -> Self { CipherSuiteAes256CbcSha { cbc: None, rsa } } @@ -57,23 +58,28 @@ impl CipherSuite for CipherSuiteAes256CbcSha { fn init( &mut self, + provider: Arc, master_secret: &[u8], client_random: &[u8], server_random: &[u8], is_client: bool, ) -> Result<()> { let keys = prf_encryption_keys( + provider.crypto(), master_secret, client_random, server_random, - CipherSuiteAes256CbcSha::PRF_MAC_LEN, - CipherSuiteAes256CbcSha::PRF_KEY_LEN, - CipherSuiteAes256CbcSha::PRF_IV_LEN, + EncryptionKeyLengths { + mac: CipherSuiteAes256CbcSha::PRF_MAC_LEN, + key: CipherSuiteAes256CbcSha::PRF_KEY_LEN, + iv: CipherSuiteAes256CbcSha::PRF_IV_LEN, + }, self.hash_func(), )?; if is_client { self.cbc = Some(CryptoCbc::new( + provider, &keys.client_write_key, &keys.client_mac_key, &keys.server_write_key, @@ -81,6 +87,7 @@ impl CipherSuite for CipherSuiteAes256CbcSha { )?); } else { self.cbc = Some(CryptoCbc::new( + provider, &keys.server_write_key, &keys.server_mac_key, &keys.client_write_key, @@ -91,15 +98,15 @@ impl CipherSuite for CipherSuiteAes256CbcSha { Ok(()) } - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let cg = self.cbc.as_ref().ok_or(Error::Other( + fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let cg = self.cbc.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to encrypt".to_owned(), ))?; cg.encrypt(pkt_rlh, raw) } - fn decrypt(&self, input: &[u8]) -> Result> { - let cg = self.cbc.as_ref().ok_or(Error::Other( + fn decrypt(&mut self, input: &[u8]) -> Result> { + let cg = self.cbc.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to decrypt".to_owned(), ))?; cg.decrypt(input) diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs b/rtc-dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs index 7066a4bf..624e7cd8 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_chacha20_poly1305_sha256.rs @@ -2,7 +2,7 @@ use super::*; use crate::crypto::crypto_chacha20::*; use crate::prf::*; -#[derive(Clone)] +/// The shared ChaCha20-Poly1305 with SHA-256 implementation, parameterized over the key exchange and signature. pub struct CipherSuiteChaCha20Poly1305Sha256 { rsa: bool, cipher: Option, @@ -13,6 +13,7 @@ impl CipherSuiteChaCha20Poly1305Sha256 { const PRF_KEY_LEN: usize = 32; const PRF_IV_LEN: usize = 12; + /// Builds an uninitialized ChaCha20-Poly1305 with SHA-256 suite; keys are installed later via `init`. pub fn new(rsa: bool) -> Self { CipherSuiteChaCha20Poly1305Sha256 { rsa, cipher: None } } @@ -57,49 +58,55 @@ impl CipherSuite for CipherSuiteChaCha20Poly1305Sha256 { fn init( &mut self, + provider: Arc, master_secret: &[u8], client_random: &[u8], server_random: &[u8], is_client: bool, ) -> Result<()> { let keys = prf_encryption_keys( + provider.crypto(), master_secret, client_random, server_random, - CipherSuiteChaCha20Poly1305Sha256::PRF_MAC_LEN, - CipherSuiteChaCha20Poly1305Sha256::PRF_KEY_LEN, - CipherSuiteChaCha20Poly1305Sha256::PRF_IV_LEN, + EncryptionKeyLengths { + mac: CipherSuiteChaCha20Poly1305Sha256::PRF_MAC_LEN, + key: CipherSuiteChaCha20Poly1305Sha256::PRF_KEY_LEN, + iv: CipherSuiteChaCha20Poly1305Sha256::PRF_IV_LEN, + }, self.hash_func(), )?; if is_client { self.cipher = Some(CryptoChaCha20::new( + provider, &keys.client_write_key, &keys.client_write_iv, &keys.server_write_key, &keys.server_write_iv, - )); + )?); } else { self.cipher = Some(CryptoChaCha20::new( + provider, &keys.server_write_key, &keys.server_write_iv, &keys.client_write_key, &keys.client_write_iv, - )); + )?); } Ok(()) } - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let cg = self.cipher.as_ref().ok_or(Error::Other( + fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let cg = self.cipher.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to encrypt".to_owned(), ))?; cg.encrypt(pkt_rlh, raw) } - fn decrypt(&self, input: &[u8]) -> Result> { - let cg = self.cipher.as_ref().ok_or(Error::Other( + fn decrypt(&mut self, input: &[u8]) -> Result> { + let cg = self.cipher.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to decrypt".to_owned(), ))?; cg.decrypt(input) diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs b/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs index c5bf2dfd..86df6793 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm.rs @@ -2,6 +2,7 @@ use super::*; use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; use crate::crypto::crypto_ccm::CryptoCcmTagLen; +/// Builds a `TLS_ECDHE_ECDSA_WITH_AES_128_CCM` cipher suite. pub fn new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm() -> CipherSuiteAes128Ccm { CipherSuiteAes128Ccm::new( ClientCertificateType::EcdsaSign, diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs b/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs index aa9a92ce..5e8ed547 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8.rs @@ -2,6 +2,7 @@ use super::*; use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; use crate::crypto::crypto_ccm::CryptoCcmTagLen; +/// Builds a `TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8` cipher suite. pub fn new_cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8() -> CipherSuiteAes128Ccm { CipherSuiteAes128Ccm::new( ClientCertificateType::EcdsaSign, diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs index 6f506e0e..cb25399b 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm.rs @@ -2,6 +2,7 @@ use super::*; use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; use crate::crypto::crypto_ccm::CryptoCcmTagLen; +/// Builds a `TLS_PSK_WITH_AES_128_CCM` cipher suite. pub fn new_cipher_suite_tls_psk_with_aes_128_ccm() -> CipherSuiteAes128Ccm { CipherSuiteAes128Ccm::new( ClientCertificateType::Unsupported, diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs index 64b9f1a5..62861cda 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_ccm8.rs @@ -2,6 +2,7 @@ use super::*; use crate::cipher_suite::cipher_suite_aes_128_ccm::CipherSuiteAes128Ccm; use crate::crypto::crypto_ccm::CryptoCcmTagLen; +/// Builds a `TLS_PSK_WITH_AES_128_CCM_8` cipher suite. pub fn new_cipher_suite_tls_psk_with_aes_128_ccm8() -> CipherSuiteAes128Ccm { CipherSuiteAes128Ccm::new( ClientCertificateType::Unsupported, diff --git a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs index cb0f3e51..d3a0c286 100644 --- a/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs +++ b/rtc-dtls/src/cipher_suite/cipher_suite_tls_psk_with_aes_128_gcm_sha256.rs @@ -2,7 +2,8 @@ use super::*; use crate::crypto::crypto_gcm::*; use crate::prf::*; -#[derive(Clone, Default)] +#[derive(Default)] +/// The shared `TLS_PSK_WITH_AES_128_GCM_SHA256` implementation, parameterized over the key exchange and signature. pub struct CipherSuiteTlsPskWithAes128GcmSha256 { gcm: Option, } @@ -40,49 +41,55 @@ impl CipherSuite for CipherSuiteTlsPskWithAes128GcmSha256 { fn init( &mut self, + provider: Arc, master_secret: &[u8], client_random: &[u8], server_random: &[u8], is_client: bool, ) -> Result<()> { let keys = prf_encryption_keys( + provider.crypto(), master_secret, client_random, server_random, - CipherSuiteTlsPskWithAes128GcmSha256::PRF_MAC_LEN, - CipherSuiteTlsPskWithAes128GcmSha256::PRF_KEY_LEN, - CipherSuiteTlsPskWithAes128GcmSha256::PRF_IV_LEN, + EncryptionKeyLengths { + mac: CipherSuiteTlsPskWithAes128GcmSha256::PRF_MAC_LEN, + key: CipherSuiteTlsPskWithAes128GcmSha256::PRF_KEY_LEN, + iv: CipherSuiteTlsPskWithAes128GcmSha256::PRF_IV_LEN, + }, self.hash_func(), )?; if is_client { self.gcm = Some(CryptoGcm::new( + provider, &keys.client_write_key, &keys.client_write_iv, &keys.server_write_key, &keys.server_write_iv, - )); + )?); } else { self.gcm = Some(CryptoGcm::new( + provider, &keys.server_write_key, &keys.server_write_iv, &keys.client_write_key, &keys.client_write_iv, - )); + )?); } Ok(()) } - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { - let cg = self.gcm.as_ref().ok_or(Error::Other( + fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + let cg = self.gcm.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to encrypt".to_owned(), ))?; cg.encrypt(pkt_rlh, raw) } - fn decrypt(&self, input: &[u8]) -> Result> { - let cg = self.gcm.as_ref().ok_or(Error::Other( + fn decrypt(&mut self, input: &[u8]) -> Result> { + let cg = self.gcm.as_mut().ok_or(Error::Other( "CipherSuite has not been initialized, unable to decrypt".to_owned(), ))?; cg.decrypt(input) diff --git a/rtc-dtls/src/cipher_suite/mod.rs b/rtc-dtls/src/cipher_suite/mod.rs index 07d6f229..4810d214 100644 --- a/rtc-dtls/src/cipher_suite/mod.rs +++ b/rtc-dtls/src/cipher_suite/mod.rs @@ -1,14 +1,29 @@ +/// Shared implementation for the AES-128-CCM suites. pub mod cipher_suite_aes_128_ccm; +/// Shared implementation for the AES-128-GCM-SHA256 suites. pub mod cipher_suite_aes_128_gcm_sha256; +/// Shared implementation for the AES-256-CBC-SHA suites. pub mod cipher_suite_aes_256_cbc_sha; +/// Shared implementation for the ChaCha20-Poly1305-SHA256 suites. pub mod cipher_suite_chacha20_poly1305_sha256; +/// An ECDHE-ECDSA suite with AES-128, the pairing WebRTC normally negotiates. +/// An ECDHE-ECDSA suite with AES-128-CCM and a truncated 8-byte tag. pub mod cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm; +/// An ECDHE-ECDSA suite with AES-128-CCM and a full 16-byte tag. pub mod cipher_suite_tls_ecdhe_ecdsa_with_aes_128_ccm8; +/// `TLS_PSK_WITH_AES_128_CCM`, for pre-shared-key handshakes. pub mod cipher_suite_tls_psk_with_aes_128_ccm; +/// `TLS_PSK_WITH_AES_128_CCM_8`, with a truncated 8-byte tag. pub mod cipher_suite_tls_psk_with_aes_128_ccm8; +/// `TLS_PSK_WITH_AES_128_GCM_SHA256`, for pre-shared-key handshakes. pub mod cipher_suite_tls_psk_with_aes_128_gcm_sha256; +use crypto::RTCCryptoProvider; +use crypto::{ + AeadAlgorithm, CbcAlgorithm, CryptoAlgorithm, HashAlgorithm, HmacAlgorithm, RTCCrypto, +}; use std::fmt; +use std::sync::Arc; use super::client_certificate_type::*; use super::record_layer::record_layer_header::*; @@ -27,27 +42,41 @@ use cipher_suite_tls_psk_with_aes_128_gcm_sha256::*; // Supported Cipher Suites #[allow(non_camel_case_types)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// The cipher suites this crate can negotiate, by their IANA code points. +#[non_exhaustive] pub enum CipherSuiteId { // AES-128-CCM + /// `TLS_ECDHE_ECDSA_WITH_AES_128_CCM` (`0xc0ac`). Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm = 0xc0ac, + /// `TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8` (`0xc0ae`). Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 = 0xc0ae, // AES-128-GCM-SHA256 + /// `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` (`0xc02b`). Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 = 0xc02b, + /// `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` (`0xc02f`). Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 = 0xc02f, // AES-256-CBC-SHA + /// `TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA` (`0xc00a`). Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha = 0xc00a, + /// `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA` (`0xc014`). Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha = 0xc014, + /// `TLS_PSK_WITH_AES_128_CCM` (`0xc0a4`). Tls_Psk_With_Aes_128_Ccm = 0xc0a4, + /// `TLS_PSK_WITH_AES_128_CCM_8` (`0xc0a8`). Tls_Psk_With_Aes_128_Ccm_8 = 0xc0a8, + /// `TLS_PSK_WITH_AES_128_GCM_SHA256` (`0x00a8`). Tls_Psk_With_Aes_128_Gcm_Sha256 = 0x00a8, // CHACHA20_POLY1305_SHA256 + /// `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` (`0xcca8`). Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 = 0xcca8, + /// `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` (`0xcca9`). Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 = 0xcca9, + /// A code point this crate does not implement. Unsupported, } @@ -150,8 +179,43 @@ impl From<&str> for CipherSuiteId { } } +impl CipherSuiteId { + pub(crate) fn supported_by(self, crypto: &dyn RTCCrypto) -> bool { + let record_algorithm = match self { + Self::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm | Self::Tls_Psk_With_Aes_128_Ccm => { + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Ccm) + } + Self::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8 | Self::Tls_Psk_With_Aes_128_Ccm_8 => { + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Ccm8) + } + Self::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256 + | Self::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 + | Self::Tls_Psk_With_Aes_128_Gcm_Sha256 => { + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Gcm) + } + Self::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha + | Self::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha => { + CryptoAlgorithm::Cbc(CbcAlgorithm::Aes256Cbc) + } + Self::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256 + | Self::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 => { + CryptoAlgorithm::Aead(AeadAlgorithm::ChaCha20Poly1305) + } + Self::Unsupported => return false, + }; + crypto.supports(CryptoAlgorithm::Hash(HashAlgorithm::Sha256)) + && crypto.supports(CryptoAlgorithm::Hmac(HmacAlgorithm::Sha256)) + && crypto.supports(record_algorithm) + && (!matches!(record_algorithm, CryptoAlgorithm::Cbc(_)) + || crypto.supports(CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1))) + } +} + #[derive(Copy, Clone, Debug)] +/// The hash a suite uses in its PRF and `Finished` computation. +#[non_exhaustive] pub enum CipherSuiteHash { + /// SHA-256. Sha256, } @@ -163,30 +227,59 @@ impl CipherSuiteHash { } } -pub trait CipherSuite: Send + Sync { +/// A negotiated cipher suite: its identity, and the record encryption it performs once keys +/// are installed. +pub trait CipherSuite: Send { + /// The suite's IANA name. fn to_string(&self) -> String; + /// The suite's code point. fn id(&self) -> CipherSuiteId; + /// The certificate type this suite requires of a peer. fn certificate_type(&self) -> ClientCertificateType; + /// The hash used for the PRF and `Finished`. fn hash_func(&self) -> CipherSuiteHash; + /// Whether this suite authenticates with a pre-shared key rather than certificates. fn is_psk(&self) -> bool; + /// Whether keys have been installed, so records can be protected. fn is_initialized(&self) -> bool; // Generate the internal encryption state + /// Installs the keying material derived from the handshake. + /// + /// # Errors + /// + /// Fails if the key or salt lengths do not match what this suite expects. fn init( &mut self, + provider: Arc, master_secret: &[u8], client_random: &[u8], server_random: &[u8], is_client: bool, ) -> Result<()>; - fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result>; - fn decrypt(&self, input: &[u8]) -> Result>; + /// Protects one record, returning the encrypted record including its header. + /// + /// # Errors + /// + /// Fails if keys are not installed, or the cipher rejects the input. + fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result>; + /// Unprotects one record. + /// + /// # Errors + /// + /// Fails if authentication fails, or the record is malformed. + fn decrypt(&mut self, input: &[u8]) -> Result>; } // Taken from https://www.iana.org/assignments/tls-parameters/tls-parameters.xml // A cipher_suite is a specific combination of key agreement, cipher and MAC // function. +/// Builds the [`CipherSuite`] implementation for `id`. +/// +/// # Errors +/// +/// Fails if the id is not one this crate implements. pub fn cipher_suite_for_id(id: CipherSuiteId) -> Result> { match id { CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm => { diff --git a/rtc-dtls/src/client_certificate_type.rs b/rtc-dtls/src/client_certificate_type.rs index d87aacb6..6b064fb7 100644 --- a/rtc-dtls/src/client_certificate_type.rs +++ b/rtc-dtls/src/client_certificate_type.rs @@ -1,7 +1,12 @@ #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// The certificate types a server may request from a client. +#[non_exhaustive] pub enum ClientCertificateType { + /// `RSA_SIGN` (`1`). RsaSign = 1, + /// `ECDSA_SIGN` (`64`). EcdsaSign = 64, + /// A type this crate does not implement. Unsupported, } diff --git a/rtc-dtls/src/compression_methods.rs b/rtc-dtls/src/compression_methods.rs index b15f6350..f6c8b8e6 100644 --- a/rtc-dtls/src/compression_methods.rs +++ b/rtc-dtls/src/compression_methods.rs @@ -4,8 +4,12 @@ use byteorder::{ReadBytesExt, WriteBytesExt}; use std::io::{Read, Write}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// Compression methods. DTLS in WebRTC always negotiates `Null`. +#[non_exhaustive] pub enum CompressionMethodId { + /// `NULL` (`0`). Null = 0, + /// A method this crate does not implement. Unsupported, } @@ -19,15 +23,23 @@ impl From for CompressionMethodId { } #[derive(Clone, Debug, PartialEq, Eq)] +/// The compression-methods list offered or selected in a hello message. pub struct CompressionMethods { + /// The methods, in preference order. pub ids: Vec, } impl CompressionMethods { + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 1 + self.ids.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(self.ids.len() as u8)?; @@ -38,6 +50,11 @@ impl CompressionMethods { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let compression_methods_count = reader.read_u8()? as usize; let mut ids = vec![]; @@ -52,6 +69,7 @@ impl CompressionMethods { } } +/// The default list: null compression only. pub fn default_compression_methods() -> CompressionMethods { CompressionMethods { ids: vec![CompressionMethodId::Null], diff --git a/rtc-dtls/src/config.rs b/rtc-dtls/src/config.rs index 3a48ea45..38780782 100644 --- a/rtc-dtls/src/config.rs +++ b/rtc-dtls/src/config.rs @@ -1,13 +1,30 @@ +//! Handshake configuration. +//! +//! [`ConfigBuilder`](crate::config::ConfigBuilder) is what a caller supplies: certificates, the client/server role, which cipher +//! suites and curves to offer, the SRTP protection profiles to negotiate through `use_srtp`, +//! and how strictly to require the extended master secret +//! ([`ExtendedMasterSecretType`](crate::config::ExtendedMasterSecretType)). +//! +//! WebRTC authenticates peers by comparing the certificate fingerprint against the one +//! signalled in SDP, not against a CA chain — so certificates here are normally self-signed +//! (see [`gen_self_signed_root_cert`](crate::config::gen_self_signed_root_cert)) and the check is implemented by supplying a +//! [`VerifyPeerCertificateFn`](crate::config::VerifyPeerCertificateFn). +//! +//! [`HandshakeConfig`](crate::config::HandshakeConfig) is the resolved form the handshake +//! actually runs with, produced by [`ConfigBuilder::build`](crate::config::ConfigBuilder::build). + #[cfg(test)] mod config_test; use crate::cipher_suite::*; use crate::conn::{DEFAULT_REPLAY_PROTECTION_WINDOW, INITIAL_TICKER_INTERVAL}; use crate::crypto::*; +use crate::curve::named_curve::NamedCurve; use crate::extension::extension_use_srtp::SrtpProtectionProfile; use crate::signature_hash_algorithm::{ SignatureHashAlgorithm, SignatureScheme, parse_signature_schemes, }; +use crypto::RTCCryptoProvider; use log::warn; use shared::error::*; use std::collections::HashMap; @@ -20,10 +37,76 @@ use rustls::client::danger::ServerCertVerifier; use rustls::pki_types::CertificateDer; use rustls::server::danger::ClientCertVerifier; +/// Explicit rustls/webpki backend used only for CA-chain and hostname verification. +/// +/// This policy adapter is separate from [`RTCCryptoProvider`]. Applications authenticating with +/// SDP fingerprints do not need it, while applications enabling WebPKI validation can select its +/// backend without changing their primitive RTC provider. +#[derive(Clone)] +pub struct RustlsVerifierAdapter { + provider: Arc, +} + +impl RustlsVerifierAdapter { + /// Wraps a rustls crypto provider for WebPKI verification. + #[must_use] + pub fn new(provider: Arc) -> Self { + Self { provider } + } + + /// Uses rustls's ring verification backend. + #[cfg(feature = "crypto-ring")] + #[must_use] + pub fn ring() -> Self { + Self::new(Arc::new(rustls::crypto::ring::default_provider())) + } + + /// Uses rustls's AWS-LC-RS verification backend. + #[cfg(feature = "crypto-aws-lc-rs")] + #[must_use] + pub fn aws_lc_rs() -> Self { + Self::new(Arc::new(rustls::crypto::aws_lc_rs::default_provider())) + } +} + +fn default_verifier_adapter() -> Option { + #[cfg(feature = "crypto-ring")] + { + Some(RustlsVerifierAdapter::ring()) + } + #[cfg(all(not(feature = "crypto-ring"), feature = "crypto-aws-lc-rs"))] + { + Some(RustlsVerifierAdapter::aws_lc_rs()) + } + #[cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] + { + None + } +} + +/// Builds the default server-certificate verifier, with an explicit provider where we have one. +/// +/// # Errors +/// +/// Fails if the root store holds no usable trust anchors. +fn server_cert_verifier( + roots: std::sync::Arc, + adapter: &RustlsVerifierAdapter, +) -> Result> { + let verifier = rustls::client::WebPkiServerVerifier::builder_with_provider( + roots, + adapter.provider.clone(), + ) + .build() + .map_err(|err| Error::Other(format!("rustls server cert verifier: {err}")))?; + Ok(verifier) +} + /// Config is used to configure a DTLS client or server. /// After a Config is passed to a DTLS function it must not be modified. #[derive(Clone)] pub struct ConfigBuilder { + crypto_provider: Option>, certificates: Vec, cipher_suites: Vec, signature_schemes: Vec, @@ -39,6 +122,7 @@ pub struct ConfigBuilder { verify_peer_certificate: Option, roots_cas: rustls::RootCertStore, client_cas: rustls::RootCertStore, + verifier_adapter: Option, server_name: String, mtu: usize, replay_protection_window: usize, @@ -47,6 +131,7 @@ pub struct ConfigBuilder { impl Default for ConfigBuilder { fn default() -> Self { Self { + crypto_provider: None, certificates: vec![], cipher_suites: vec![], signature_schemes: vec![], @@ -62,6 +147,7 @@ impl Default for ConfigBuilder { verify_peer_certificate: None, roots_cas: rustls::RootCertStore::empty(), client_cas: rustls::RootCertStore::empty(), + verifier_adapter: default_verifier_adapter(), server_name: String::default(), mtu: 0, replay_protection_window: 0, @@ -70,6 +156,15 @@ impl Default for ConfigBuilder { } impl ConfigBuilder { + /// Selects the cryptography and CSPRNG implementation for this DTLS association. + /// + /// The provider is resolved while building the handshake configuration and is reused for the + /// entire handshake and record lifetime. No global registration is required. + pub fn with_crypto_provider(mut self, provider: Arc) -> Self { + self.crypto_provider = Some(provider); + self + } + /// certificates contains certificate chain to present to the other side of the connection. /// Server MUST set this if psk is non-nil /// client SHOULD sets this so CertificateRequests can be handled if psk is non-nil @@ -201,6 +296,12 @@ impl ConfigBuilder { self } + /// Selects the rustls/webpki adapter used for optional CA-chain and hostname verification. + pub fn with_rustls_verifier_adapter(mut self, adapter: RustlsVerifierAdapter) -> Self { + self.verifier_adapter = Some(adapter); + self + } + /// server_name is used to verify the hostname on the returned /// certificates unless insecure_skip_verify is given. pub fn with_server_name(mut self, server_name: String) -> Self { @@ -234,22 +335,32 @@ pub(crate) type PskCallback = Arc Result>) + Send + Sy /// ClientAuthType declares the policy the server will follow for /// TLS Client Authentication. #[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum ClientAuthType { #[default] + /// `NO_CLIENT_CERT` (`0`). NoClientCert = 0, + /// `REQUEST_CLIENT_CERT` (`1`). RequestClientCert = 1, + /// `REQUIRE_ANY_CLIENT_CERT` (`2`). RequireAnyClientCert = 2, + /// `VERIFY_CLIENT_CERT_IF_GIVEN` (`3`). VerifyClientCertIfGiven = 3, + /// `REQUIRE_AND_VERIFY_CLIENT_CERT` (`4`). RequireAndVerifyClientCert = 4, } // ExtendedMasterSecretType declares the policy the client and server // will follow for the Extended Master Secret extension #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] +/// How strictly to require the extended master secret extension ([RFC 7627]). pub enum ExtendedMasterSecretType { #[default] + /// `REQUEST` (`0`). Request = 0, + /// `REQUIRE` (`1`). Require = 1, + /// `DISABLE` (`2`). Disable = 2, } @@ -271,15 +382,6 @@ impl ConfigBuilder { return Err(Error::ErrIdentityNoPsk); } - for cert in &self.certificates { - match cert.private_key.kind { - CryptoPrivateKeyKind::Ed25519(_) => {} - CryptoPrivateKeyKind::Ecdsa256(_) => {} - CryptoPrivateKeyKind::Custom(_) => {} - _ => return Err(Error::ErrInvalidPrivateKey), - } - } - parse_cipher_suites(&self.cipher_suites, self.psk.is_none(), self.psk.is_some())?; Ok(()) @@ -291,16 +393,81 @@ impl ConfigBuilder { is_client: bool, remote_addr: Option, ) -> Result { + // The caller supplies the provider; this crate never resolves a default. An application + // that wants the feature-selected built-in passes `crypto::default_provider()?`. + let crypto_provider = self.crypto_provider.take().ok_or_else(|| { + Error::Crypto( + "no crypto provider configured: call ConfigBuilder::with_crypto_provider" + .to_owned(), + ) + })?; self.validate(is_client)?; - let local_cipher_suites: Vec = + let mut local_cipher_suites: Vec = parse_cipher_suites(&self.cipher_suites, self.psk.is_none(), self.psk.is_some())? .iter() .map(|cs| cs.id()) + .filter(|id| id.supported_by(crypto_provider.crypto())) .collect(); + if local_cipher_suites.is_empty() { + return Err(Error::ErrNoAvailableCipherSuites); + } let sigs: Vec = self.signature_schemes.iter().map(|x| *x as u16).collect(); - let local_signature_schemes = parse_signature_schemes(&sigs, self.insecure_hashes)?; + let local_signature_schemes: Vec<_> = parse_signature_schemes(&sigs, self.insecure_hashes)? + .into_iter() + .filter(|algorithm| { + algorithm.crypto_scheme().is_ok_and(|scheme| { + crypto_provider + .crypto() + .supports(crypto::CryptoAlgorithm::Signature(scheme)) + }) + }) + .collect(); + if self.psk.is_none() && local_signature_schemes.is_empty() { + return Err(Error::ErrNoAvailableSignatureSchemes); + } + + let local_named_curves: Vec<_> = [NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384] + .into_iter() + .filter(|curve| { + curve.crypto_algorithm().is_ok_and(|algorithm| { + crypto_provider + .crypto() + .supports(crypto::CryptoAlgorithm::KeyExchange(algorithm)) + }) + }) + .collect(); + if self.psk.is_none() && local_named_curves.is_empty() { + return Err(Error::ErrNoAvailableCipherSuites); + } + + if !is_client && self.psk.is_none() { + let signing_key = &self.certificates[0].private_key.signing_key; + local_cipher_suites.retain(|id| { + local_signature_schemes.iter().any(|algorithm| { + let signature_family_matches = match id { + CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_128_Gcm_Sha256 + | CipherSuiteId::Tls_Ecdhe_Rsa_With_Aes_256_Cbc_Sha + | CipherSuiteId::Tls_Ecdhe_Rsa_With_ChaCha20_Poly1305_Sha256 => { + algorithm.signature + == crate::signature_hash_algorithm::SignatureAlgorithm::Rsa + } + _ => { + algorithm.signature + == crate::signature_hash_algorithm::SignatureAlgorithm::Ecdsa + } + }; + signature_family_matches + && algorithm + .crypto_scheme() + .is_ok_and(|scheme| signing_key.supports(scheme)) + }) + }); + if local_cipher_suites.is_empty() { + return Err(Error::ErrNoAvailableCipherSuites); + } + } let retransmit_interval = if self.flight_interval != Duration::from_secs(0) { self.flight_interval @@ -330,57 +497,108 @@ impl ConfigBuilder { } } + let server_cert_verifier = if self.insecure_skip_verify { + None + } else { + let adapter = self.verifier_adapter.as_ref().ok_or_else(|| { + Error::Crypto("CA-chain verification requires a RustlsVerifierAdapter".to_owned()) + })?; + let roots = if self.roots_cas.is_empty() { + gen_self_signed_root_cert() + } else { + self.roots_cas.clone() + }; + Some(server_cert_verifier(Arc::new(roots), adapter)?) + }; + + let client_cert_verifier = if self.client_auth as u8 + >= ClientAuthType::VerifyClientCertIfGiven as u8 + { + let adapter = self.verifier_adapter.as_ref().ok_or_else(|| { + Error::Crypto( + "client-certificate verification requires a RustlsVerifierAdapter".to_owned(), + ) + })?; + Some( + rustls::server::WebPkiClientVerifier::builder_with_provider( + Arc::new(self.client_cas.clone()), + adapter.provider.clone(), + ) + .build() + .map_err(|err| Error::Other(format!("rustls client cert verifier: {err}")))? + as Arc, + ) + } else { + None + }; + + // Fields not derived from the builder come from `HandshakeConfig::new`, which also + // supplies `crypto_provider`. Ok(HandshakeConfig { + crypto_provider, local_psk_callback: self.psk.take(), local_psk_identity_hint: self.psk_identity_hint.take(), local_cipher_suites, + local_named_curves, local_signature_schemes, extended_master_secret: self.extended_master_secret, local_srtp_protection_profiles: self.srtp_protection_profiles, server_name, client_auth: self.client_auth, local_certificates: self.certificates, + name_to_certificate: Default::default(), insecure_skip_verify: self.insecure_skip_verify, insecure_verification: self.insecure_verification, verify_peer_certificate: self.verify_peer_certificate.take(), roots_cas: self.roots_cas, - server_cert_verifier: rustls::client::WebPkiServerVerifier::builder(Arc::new( - gen_self_signed_root_cert(), - )) - .build() - .unwrap(), - client_cert_verifier: None, + server_cert_verifier, + client_cert_verifier, retransmit_interval, initial_epoch: 0, maximum_transmission_unit, + maximum_retransmit_number: 0, replay_protection_window, - ..Default::default() }) } } +/// A callback that decides whether a peer's certificate chain is acceptable. +/// +/// WebRTC verifies the fingerprint from SDP instead of a CA chain, so this is where that check +/// goes. pub type VerifyPeerCertificateFn = Arc], &[CertificateDer<'static>]) -> Result<()>) + Send + Sync>; +/// Generates a self-signed certificate, as WebRTC endpoints use. pub fn gen_self_signed_root_cert() -> rustls::RootCertStore { - let mut certs = rustls::RootCertStore::empty(); - certs - .add( - rcgen::generate_simple_self_signed(vec![]) - .unwrap() - .cert - .der() - .to_owned(), - ) - .unwrap(); - certs + #[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] + { + let mut certs = rustls::RootCertStore::empty(); + certs + .add( + rcgen::generate_simple_self_signed(vec![]) + .unwrap() + .cert + .der() + .to_owned(), + ) + .unwrap(); + certs + } + #[cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] + { + rustls::RootCertStore::empty() + } } #[derive(Clone)] +/// The resolved configuration a handshake runs with, produced by [`ConfigBuilder::build`]. pub struct HandshakeConfig { + pub(crate) crypto_provider: Arc, pub(crate) local_psk_callback: Option, pub(crate) local_psk_identity_hint: Option>, pub(crate) local_cipher_suites: Vec, // Available CipherSuites + pub(crate) local_named_curves: Vec, pub(crate) local_signature_schemes: Vec, // Available signature schemes pub(crate) extended_master_secret: ExtendedMasterSecretType, // Policy for the Extended Master Support extension pub(crate) local_srtp_protection_profiles: Vec, // Available SRTPProtectionProfiles, if empty no SRTP support @@ -392,9 +610,9 @@ pub struct HandshakeConfig { pub(crate) insecure_verification: bool, pub(crate) verify_peer_certificate: Option, pub(crate) roots_cas: rustls::RootCertStore, - pub(crate) server_cert_verifier: Arc, + pub(crate) server_cert_verifier: Option>, pub(crate) client_cert_verifier: Option>, - pub(crate) retransmit_interval: std::time::Duration, + pub(crate) retransmit_interval: Duration, pub(crate) initial_epoch: u16, pub(crate) maximum_transmission_unit: usize, pub(crate) maximum_retransmit_number: usize, @@ -404,8 +622,10 @@ pub struct HandshakeConfig { impl fmt::Debug for HandshakeConfig { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { fmt.debug_struct("HandshakeConfig") + .field("crypto_provider", &self.crypto_provider.name()) .field("local_psk_identity_hint", &self.local_psk_identity_hint) .field("local_cipher_suites", &self.local_cipher_suites) + .field("local_named_curves", &self.local_named_curves) .field("local_signature_schemes", &self.local_signature_schemes) .field("extended_master_secret", &self.extended_master_secret) .field( @@ -428,12 +648,14 @@ impl fmt::Debug for HandshakeConfig { } } -impl Default for HandshakeConfig { - fn default() -> Self { - HandshakeConfig { +impl HandshakeConfig { + pub(crate) fn new(crypto_provider: Arc) -> Self { + Self { + crypto_provider, local_psk_callback: None, local_psk_identity_hint: None, local_cipher_suites: vec![], + local_named_curves: vec![], local_signature_schemes: vec![], extended_master_secret: ExtendedMasterSecretType::Disable, local_srtp_protection_profiles: vec![], @@ -445,11 +667,9 @@ impl Default for HandshakeConfig { insecure_verification: false, verify_peer_certificate: None, roots_cas: rustls::RootCertStore::empty(), - server_cert_verifier: rustls::client::WebPkiServerVerifier::builder(Arc::new( - gen_self_signed_root_cert(), - )) - .build() - .unwrap(), + server_cert_verifier: default_verifier_adapter().and_then(|adapter| { + server_cert_verifier(Arc::new(gen_self_signed_root_cert()), &adapter).ok() + }), client_cert_verifier: None, retransmit_interval: std::time::Duration::from_secs(0), initial_epoch: 0, @@ -458,9 +678,11 @@ impl Default for HandshakeConfig { replay_protection_window: DEFAULT_REPLAY_PROTECTION_WINDOW, } } -} -impl HandshakeConfig { + pub(crate) fn provider(&self) -> &Arc { + &self.crypto_provider + } + pub(crate) fn get_certificate(&self, server_name: &str) -> Result { if self.local_certificates.is_empty() { return Err(Error::ErrNoCertificates); diff --git a/rtc-dtls/src/config/config_test.rs b/rtc-dtls/src/config/config_test.rs index e448f856..24c65c0d 100644 --- a/rtc-dtls/src/config/config_test.rs +++ b/rtc-dtls/src/config/config_test.rs @@ -1,37 +1,107 @@ use super::*; use shared::error::Result; +struct IncompleteProvider; + +impl crypto::RTCCryptoProvider for IncompleteProvider { + fn name(&self) -> &'static str { + "incomplete" + } + + fn crypto(&self) -> &dyn crypto::RTCCrypto { + self + } + + fn random(&self) -> &dyn crypto::RTCRandom { + self + } +} + +impl crypto::RTCCrypto for IncompleteProvider { + fn supports(&self, _algorithm: crypto::CryptoAlgorithm) -> bool { + false + } +} + +impl crypto::RTCRandom for IncompleteProvider { + fn fill(&self, _output: &mut [u8]) -> std::result::Result<(), crypto::CryptoError> { + Err(crypto::CryptoError::RandomnessFailed) + } +} + #[derive(Debug)] struct MockSigner; -impl CustomSigner for MockSigner { - fn sign(&self, _message: &[u8]) -> std::result::Result, String> { - Ok(vec![]) +impl crypto::SigningKey for MockSigner { + fn supports(&self, _scheme: crypto::SignatureScheme) -> bool { + true + } + + fn public_key(&self) -> crypto::PublicKey<'_> { + crypto::PublicKey { + encoding: crypto::PublicKeyEncoding::SubjectPublicKeyInfoDer, + bytes: &[], + } } - fn clone_box(&self) -> Box { - Box::new(MockSigner) + fn sign( + &self, + _scheme: crypto::SignatureScheme, + _message: &[u8], + ) -> std::result::Result, crypto::CryptoError> { + Ok(vec![]) } } #[test] -fn test_config_accepts_custom_signer() -> Result<()> { +fn test_config_accepts_external_signing_key() -> Result<()> { let cert = Certificate { certificate: vec![], - private_key: CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Custom(Box::new(MockSigner)), - serialized_der: vec![], - }, + private_key: CryptoPrivateKey::from_signing_key(std::sync::Arc::new(MockSigner)), }; let handshake = ConfigBuilder::default() + .with_crypto_provider(crypto::default_provider().map_err(crypto_error)?) .with_certificates(vec![cert]) .build(false, None)?; - assert!(matches!( - handshake.local_certificates[0].private_key.kind, - CryptoPrivateKeyKind::Custom(_) - )); + assert!( + handshake.local_certificates[0] + .private_key + .signing_key + .supports(crypto::SignatureScheme::EcdsaP256Sha256) + ); + + Ok(()) +} + +#[test] +fn test_config_rejects_incomplete_provider() { + let result = ConfigBuilder::default() + .with_crypto_provider(Arc::new(IncompleteProvider)) + .build(true, None); + + assert!(matches!(result, Err(Error::ErrNoAvailableCipherSuites))); +} + +#[cfg(feature = "crypto-ring")] +#[test] +fn test_config_accepts_ring_provider() -> Result<()> { + let handshake = ConfigBuilder::default() + .with_crypto_provider(Arc::new(crypto::providers::RingProvider::new())) + .build(true, None)?; + + assert_eq!(handshake.provider().name(), "ring"); + Ok(()) +} + +#[cfg(feature = "crypto-aws-lc-rs")] +#[test] +fn test_config_accepts_aws_lc_rs_provider() -> Result<()> { + let handshake = ConfigBuilder::default() + .with_crypto_provider(Arc::new(crypto::providers::AwsLcRsProvider::new())) + .build(true, None)?; + assert_eq!(handshake.provider().name(), "aws-lc-rs"); Ok(()) } diff --git a/rtc-dtls/src/conn/conn_test.rs b/rtc-dtls/src/conn/conn_test.rs index 960c0bb8..dbbfcbb4 100644 --- a/rtc-dtls/src/conn/conn_test.rs +++ b/rtc-dtls/src/conn/conn_test.rs @@ -20,7 +20,6 @@ use shared::error::*;*/ //use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; //use rand::Rng; -//use shared::crypto::KeyingMaterialExporter; const ERR_TEST_PSK_INVALID_IDENTITY: &str = "TestPSK: Server got invalid identity"; const ERR_PSK_REJECTED: &str = "PSK Rejected"; @@ -309,7 +308,9 @@ async fn test_export_keying_material() -> shared::error::Result<()> { closed: AtomicBool::new(false), current_flight: Box::new(Flight0 {}) as Box, flights: None, - cfg: HandshakeConfig::default(), + cfg: HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + ), retransmit: false, handshake_rx: None, @@ -2384,7 +2385,9 @@ async fn test_renegotation_info() -> Result<()> { /// is completed (RFC 6347: buffer only until Finished is received). #[test] fn test_read_does_not_enqueue_after_handshake_completed() { - let config = Arc::new(HandshakeConfig::default()); + let config = Arc::new(HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + )); let mut conn = DTLSConn::new(config, false, None); // Mark handshake as completed. @@ -2415,7 +2418,9 @@ fn test_read_does_not_enqueue_after_handshake_completed() { /// is in progress (needed to handle Finished arriving before ChangeCipherSpec). #[test] fn test_read_enqueues_during_handshake() { - let config = Arc::new(HandshakeConfig::default()); + let config = Arc::new(HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + )); let mut conn = DTLSConn::new(config, false, None); assert!(!conn.is_handshake_completed()); @@ -2448,7 +2453,9 @@ fn test_handle_incoming_queued_packets_drains_when_cipher_ready() { use crate::cipher_suite::CipherSuite; use crate::cipher_suite::cipher_suite_aes_128_gcm_sha256::CipherSuiteAes128GcmSha256; - let config = Arc::new(HandshakeConfig::default()); + let config = Arc::new(HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + )); let mut conn = DTLSConn::new(config, false, None); assert!(!conn.is_handshake_completed()); @@ -2478,7 +2485,13 @@ fn test_handle_incoming_queued_packets_drains_when_cipher_ready() { // Now initialize the cipher suite (simulates ChangeCipherSpec having been processed). let mut cs = Box::new(CipherSuiteAes128GcmSha256::new(false)); // init() with dummy key material — will produce an initialized cipher suite. - let _ = cs.init(&[0u8; 48], &[0u8; 32], &[0u8; 32], false); + let _ = cs.init( + crypto::default_provider().unwrap(), + &[0u8; 48], + &[0u8; 32], + &[0u8; 32], + false, + ); assert!(cs.is_initialized()); conn.state.cipher_suite = Some(cs); @@ -2498,12 +2511,20 @@ fn test_handle_incoming_queued_packets_sets_handshake_rx() { use crate::cipher_suite::CipherSuite; use crate::cipher_suite::cipher_suite_aes_128_gcm_sha256::CipherSuiteAes128GcmSha256; - let config = Arc::new(HandshakeConfig::default()); + let config = Arc::new(HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + )); let mut conn = DTLSConn::new(config, false, None); // Initialize cipher suite. let mut cs = Box::new(CipherSuiteAes128GcmSha256::new(false)); - let _ = cs.init(&[0u8; 48], &[0u8; 32], &[0u8; 32], false); + let _ = cs.init( + crypto::default_provider().unwrap(), + &[0u8; 48], + &[0u8; 32], + &[0u8; 32], + false, + ); conn.state.cipher_suite = Some(cs); conn.state.remote_epoch = 0; conn.handshake_rx = None; @@ -2537,7 +2558,9 @@ fn test_handle_incoming_queued_packets_sets_handshake_rx() { } fn setup_dtls_conn_server_handshake_in_progress() -> DTLSConn { - let handshake_config = Arc::new(HandshakeConfig::default()); + let handshake_config = Arc::new(HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + )); // is_client=false (server), no initial_state → handshake not completed DTLSConn::new(handshake_config, false, None) } @@ -2575,7 +2598,13 @@ fn test_queued_packets_drained_when_cipher_ready() { // Initialize cipher suite (simulates ChangeCipherSpec having been processed). let mut cs = Box::new(CipherSuiteAes128GcmSha256::new(false)); - let _ = cs.init(&[0u8; 48], &[0u8; 32], &[0u8; 32], false); + let _ = cs.init( + crypto::default_provider().unwrap(), + &[0u8; 48], + &[0u8; 32], + &[0u8; 32], + false, + ); conn.state.cipher_suite = Some(cs); conn.state.remote_epoch = 1; // ChangeCipherSpec has been processed @@ -2602,7 +2631,13 @@ fn test_handshake_rx_set_after_queue_processing() { // Initialize cipher suite so the queue-drain guard passes. let mut cs = Box::new(CipherSuiteAes128GcmSha256::new(false)); - let _ = cs.init(&[0u8; 48], &[0u8; 32], &[0u8; 32], false); + let _ = cs.init( + crypto::default_provider().unwrap(), + &[0u8; 48], + &[0u8; 32], + &[0u8; 32], + false, + ); conn.state.cipher_suite = Some(cs); assert!(conn.handshake_rx.is_none()); diff --git a/rtc-dtls/src/conn/mod.rs b/rtc-dtls/src/conn/mod.rs index ba66d38a..a2c3fa8d 100644 --- a/rtc-dtls/src/conn/mod.rs +++ b/rtc-dtls/src/conn/mod.rs @@ -1,3 +1,12 @@ +//! The DTLS association. +//! +//! [`DTLSConn`](crate::conn::DTLSConn) joins the handshake state machine to the record layer for one peer: inbound +//! datagrams go in through [`read`](crate::conn::DTLSConn::read), application data comes out through +//! [`incoming_application_data`](crate::conn::DTLSConn::incoming_application_data), and whatever should go on +//! the wire is collected from [`outgoing_raw_packet`](crate::conn::DTLSConn::outgoing_raw_packet). +//! +//! Normally an application drives [`Endpoint`](crate::endpoint::Endpoint) instead, which owns one +//! of these per remote address. #[cfg(test)] mod conn_test; @@ -45,6 +54,7 @@ pub(crate) static INVALID_KEYING_LABELS: &[&str] = &[ ]; // Conn represents a DTLS connection +/// One DTLS association: the handshake state machine plus the record layer around it. pub struct DTLSConn { is_client: bool, maximum_transmission_unit: usize, @@ -89,6 +99,12 @@ pub struct DTLSConn { } impl DTLSConn { + /// Creates a connection in the given role, ready to handshake. + /// + /// # Errors + /// + /// Fails if the configuration is invalid — no certificate for a server, or an unusable cipher + /// suite list. pub fn new( handshake_config: Arc, is_client: bool, @@ -110,10 +126,7 @@ impl DTLSConn { }; ( - State { - is_client, - ..Default::default() - }, + State::new(handshake_config.crypto_provider.clone(), is_client), flight, HandshakeState::Preparing, ) @@ -151,6 +164,7 @@ impl DTLSConn { } // Read reads data from the connection. + /// Takes the next decrypted application payload, if one is ready. pub fn incoming_application_data(&mut self) -> Option { if !self.is_handshake_completed() { None @@ -159,6 +173,7 @@ impl DTLSConn { } } + /// Takes the next datagram the caller should send. pub fn outgoing_raw_packet(&mut self) -> Option { if let Err(err) = self.handle_outgoing_packets() { warn!( @@ -171,6 +186,11 @@ impl DTLSConn { } // Write writes p to the DTLS connection + /// Queues `p` as application data, encrypting it once the handshake has completed. + /// + /// # Errors + /// + /// Fails if the connection is closed or the handshake has not finished. pub fn write(&mut self, p: &[u8]) -> Result<()> { if self.is_connection_closed() { return Err(Error::ErrConnClosed); @@ -198,6 +218,7 @@ impl DTLSConn { } // Close closes the connection. + /// Begins an orderly shutdown by queueing a `close_notify` alert. pub fn close(&mut self) { if !self.closed { self.closed = true; @@ -324,7 +345,7 @@ impl DTLSConn { p.record.marshal(&mut raw_packet)?; if p.should_encrypt - && let Some(cipher_suite) = &self.state.cipher_suite + && let Some(cipher_suite) = &mut self.state.cipher_suite { raw_packet = cipher_suite.encrypt(&p.record.record_layer_header, &raw_packet)?; } @@ -373,7 +394,7 @@ impl DTLSConn { raw_packet.extend_from_slice(&record_layer_header_bytes); raw_packet.extend_from_slice(handshake_fragment); if p.should_encrypt - && let Some(cipher_suite) = &self.state.cipher_suite + && let Some(cipher_suite) = &mut self.state.cipher_suite { raw_packet = cipher_suite.encrypt(&record_layer_header, &raw_packet)?; } @@ -437,6 +458,11 @@ impl DTLSConn { self.handshake_completed } + /// Feeds one received datagram into the connection. + /// + /// # Errors + /// + /// Fails if the record is malformed or fails authentication. pub fn read(&mut self, buf: &[u8]) -> Result<()> { // Per RFC 6347: buffer future-epoch packets only until Finished is received // (i.e. until handshake completes). After that, discard them. @@ -607,7 +633,7 @@ impl DTLSConn { return (false, None, None); } - if let Some(cipher_suite) = &self.state.cipher_suite { + if let Some(cipher_suite) = &mut self.state.cipher_suite { pkt = match cipher_suite.decrypt(&pkt) { Ok(pkt) => pkt, Err(err) => { diff --git a/rtc-dtls/src/content.rs b/rtc-dtls/src/content.rs index 3e2b0b28..5567f749 100644 --- a/rtc-dtls/src/content.rs +++ b/rtc-dtls/src/content.rs @@ -12,12 +12,18 @@ use shared::error::*; /// /// [RFC 4346 §6.2.1]: https://tools.ietf.org/html/rfc4346#section-6.2.1 #[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] +#[non_exhaustive] pub enum ContentType { + /// `CHANGE_CIPHER_SPEC` (`20`). ChangeCipherSpec = 20, + /// `ALERT` (`21`). Alert = 21, + /// `HANDSHAKE` (`22`). Handshake = 22, + /// `APPLICATION_DATA` (`23`). ApplicationData = 23, #[default] + /// A content type this crate does not recognise. Invalid, } @@ -34,14 +40,21 @@ impl From for ContentType { } #[derive(PartialEq, Debug, Clone)] +/// The parsed body of a DTLS record. +#[non_exhaustive] pub enum Content { + /// A ChangeCipherSpec record. ChangeCipherSpec(ChangeCipherSpec), + /// An alert record. Alert(Alert), + /// A handshake record. Handshake(Handshake), + /// An application data record. ApplicationData(ApplicationData), } impl Content { + /// The record content type this message is carried in. pub fn content_type(&self) -> ContentType { match self { Content::ChangeCipherSpec(c) => c.content_type(), @@ -51,6 +64,7 @@ impl Content { } } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { match self { Content::ChangeCipherSpec(c) => c.size(), @@ -60,6 +74,11 @@ impl Content { } } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { match self { Content::ChangeCipherSpec(c) => c.marshal(writer), @@ -69,6 +88,11 @@ impl Content { } } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(content_type: ContentType, reader: &mut R) -> Result { match content_type { ContentType::ChangeCipherSpec => Ok(Content::ChangeCipherSpec( diff --git a/rtc-dtls/src/crypto/crypto_cbc.rs b/rtc-dtls/src/crypto/crypto_cbc.rs index 461cdecb..02695f85 100644 --- a/rtc-dtls/src/crypto/crypto_cbc.rs +++ b/rtc-dtls/src/crypto/crypto_cbc.rs @@ -6,51 +6,71 @@ // Removed in TLS 1.3 year 2018. // RFC 3268 year 2002 https://tools.ietf.org/html/rfc3268 -// https://github.com/RustCrypto/block-ciphers - -use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; -use p256::elliptic_curve::subtle::ConstantTimeEq; -use rand::RngExt; +use crypto::{CbcAlgorithm, CbcCipher, HmacAlgorithm, Mac, RTCCryptoProvider, constant_time_eq}; use std::io::Cursor; -use std::ops::Not; +use std::sync::Arc; -use super::padding::DtlsPadding; use crate::content::*; +use crate::crypto::{authentication_error, crypto_error}; use crate::prf::*; use crate::record_layer::record_layer_header::*; use shared::error::*; -type Aes256CbcEnc = cbc::Encryptor; -type Aes256CbcDec = cbc::Decryptor; -// State needed to handle encrypted input/output -#[derive(Clone)] +/// AES-CBC encryption with a separate HMAC for DTLS records, holding the per-direction keys. pub struct CryptoCbc { - local_key: Vec, - remote_key: Vec, - write_mac: Vec, - read_mac: Vec, + provider: Arc, + local_cipher: Box, + remote_cipher: Box, + /// Keyed once per epoch. Re-deriving the HMAC key schedule per record measured ~2x + /// slower on this path; see `rtc-srtp/benches/README.md` for the equivalent SRTP data. + write_mac: Box, + read_mac: Box, } impl CryptoCbc { const BLOCK_SIZE: usize = 16; const MAC_SIZE: usize = 20; + /// Builds the cipher from the local and remote keys and salts. pub fn new( + provider: Arc, local_key: &[u8], local_mac: &[u8], remote_key: &[u8], remote_mac: &[u8], ) -> Result { + let local_cipher = provider + .crypto() + .new_cbc(CbcAlgorithm::Aes256Cbc, local_key) + .map_err(crypto_error)?; + let remote_cipher = provider + .crypto() + .new_cbc(CbcAlgorithm::Aes256Cbc, remote_key) + .map_err(crypto_error)?; + // Key the record MACs once per epoch, alongside the ciphers. + let write_mac = provider + .crypto() + .new_hmac(HmacAlgorithm::Sha1, local_mac) + .map_err(crypto_error)?; + let read_mac = provider + .crypto() + .new_hmac(HmacAlgorithm::Sha1, remote_mac) + .map_err(crypto_error)?; Ok(CryptoCbc { - local_key: local_key.to_vec(), - write_mac: local_mac.to_vec(), - - remote_key: remote_key.to_vec(), - read_mac: remote_mac.to_vec(), + provider, + local_cipher, + write_mac, + remote_cipher, + read_mac, }) } - pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + /// Protects one record, returning header plus ciphertext. + /// + /// # Errors + /// + /// Fails if the cipher rejects the input. + pub fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { let mut payload = raw[RECORD_LAYER_HEADER_SIZE..].to_vec(); let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; @@ -58,26 +78,29 @@ impl CryptoCbc { let h = pkt_rlh; let mac = prf_mac( + self.write_mac.as_mut(), h.epoch, h.sequence_number, h.content_type, h.protocol_version, &payload, - &self.write_mac, )?; payload.extend_from_slice(&mac); - let mut iv: Vec = vec![0; Self::BLOCK_SIZE]; - rand::rng().fill(iv.as_mut_slice()); + let padding_len = Self::BLOCK_SIZE - (payload.len() % Self::BLOCK_SIZE); + payload.resize(payload.len() + padding_len, (padding_len - 1) as u8); - let write_cbc = Aes256CbcEnc::new_from_slices(&self.local_key, &iv)?; - let encrypted = write_cbc.encrypt_padded_vec_mut::(&payload); + let mut iv = [0; Self::BLOCK_SIZE]; + self.provider.random().fill(&mut iv).map_err(crypto_error)?; + self.local_cipher + .encrypt_blocks(&iv, &mut payload) + .map_err(crypto_error)?; // Prepend unencrypte header with encrypted payload let mut r = vec![]; r.extend_from_slice(raw); r.extend_from_slice(&iv); - r.extend_from_slice(&encrypted); + r.extend_from_slice(&payload); let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; r[RECORD_LAYER_HEADER_SIZE - 2..RECORD_LAYER_HEADER_SIZE] @@ -86,7 +109,12 @@ impl CryptoCbc { Ok(r) } - pub fn decrypt(&self, r: &[u8]) -> Result> { + /// Unprotects one record. + /// + /// # Errors + /// + /// Fails if authentication fails or the record is too short. + pub fn decrypt(&mut self, r: &[u8]) -> Result> { let mut reader = Cursor::new(r); let h = RecordLayerHeader::unmarshal(&mut reader)?; if h.content_type == ContentType::ChangeCipherSpec { @@ -106,11 +134,20 @@ impl CryptoCbc { return Err(Error::ErrInvalidPacketLength); } - let read_cbc = Aes256CbcDec::new_from_slices(&self.remote_key, iv)?; + let mut decrypted = body.to_vec(); + self.remote_cipher + .decrypt_blocks(iv, &mut decrypted) + .map_err(authentication_error)?; - let decrypted = read_cbc - .decrypt_padded_vec_mut::(body) - .map_err(|_| Error::ErrInvalidPacketLength)?; + let padding_value = decrypted.last().copied().ok_or(Error::ErrInvalidMac)?; + let padding_len = padding_value as usize + 1; + if padding_len > decrypted.len() { + return Err(Error::ErrInvalidMac); + } + let padding_start = decrypted.len() - padding_len; + let expected_padding = vec![padding_value; padding_len]; + let padding_valid = constant_time_eq(&decrypted[padding_start..], &expected_padding); + decrypted.truncate(padding_start); if decrypted.len() < Self::MAC_SIZE { return Err(Error::ErrInvalidMac); @@ -119,15 +156,15 @@ impl CryptoCbc { let recv_mac = &decrypted[decrypted.len() - Self::MAC_SIZE..]; let decrypted = &decrypted[0..decrypted.len() - Self::MAC_SIZE]; let mac = prf_mac( + self.read_mac.as_mut(), h.epoch, h.sequence_number, h.content_type, h.protocol_version, decrypted, - &self.read_mac, )?; - if recv_mac.ct_eq(&mac).not().into() { + if !padding_valid || !constant_time_eq(recv_mac, &mac) { return Err(Error::ErrInvalidMac); } @@ -138,3 +175,67 @@ impl CryptoCbc { Ok(d) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn cipher_pair() -> (CryptoCbc, CryptoCbc) { + let provider = crypto::default_provider().unwrap(); + let local_key = [0x11; 32]; + let remote_key = [0x22; 32]; + let local_mac = [0x33; 20]; + let remote_mac = [0x44; 20]; + let sender = CryptoCbc::new( + provider.clone(), + &local_key, + &local_mac, + &remote_key, + &remote_mac, + ) + .unwrap(); + let receiver = + CryptoCbc::new(provider, &remote_key, &remote_mac, &local_key, &local_mac).unwrap(); + (sender, receiver) + } + + fn record(payload: &[u8]) -> (RecordLayerHeader, Vec) { + let header = RecordLayerHeader { + content_type: ContentType::ApplicationData, + protocol_version: PROTOCOL_VERSION1_2, + epoch: 1, + sequence_number: 7, + content_len: payload.len() as u16, + }; + let mut raw = Vec::new(); + header.marshal(&mut raw).unwrap(); + raw.extend_from_slice(payload); + (header, raw) + } + + #[test] + fn roundtrip_and_authentication_failures() { + let (mut sender, mut receiver) = cipher_pair(); + let (header, raw) = record(b"CBC record payload"); + let encrypted = sender.encrypt(&header, &raw).unwrap(); + assert_eq!( + receiver.decrypt(&encrypted).unwrap()[RECORD_LAYER_HEADER_SIZE..], + raw[RECORD_LAYER_HEADER_SIZE..] + ); + + let mut wrong_mac = encrypted.clone(); + wrong_mac[RECORD_LAYER_HEADER_SIZE] ^= 1; + assert_eq!(receiver.decrypt(&wrong_mac), Err(Error::ErrInvalidMac)); + + let mut bad_padding = encrypted.clone(); + *bad_padding.last_mut().unwrap() ^= 1; + assert_eq!(receiver.decrypt(&bad_padding), Err(Error::ErrInvalidMac)); + + let mut truncated = encrypted; + truncated.pop(); + assert_eq!( + receiver.decrypt(&truncated), + Err(Error::ErrInvalidPacketLength) + ); + } +} diff --git a/rtc-dtls/src/crypto/crypto_ccm.rs b/rtc-dtls/src/crypto/crypto_ccm.rs index 79ea4c8c..3e9ee16f 100644 --- a/rtc-dtls/src/crypto/crypto_ccm.rs +++ b/rtc-dtls/src/crypto/crypto_ccm.rs @@ -9,130 +9,96 @@ // https://docs.rs/ccm/0.3.0/ccm/ Or https://crates.io/crates/aes-ccm? use std::io::Cursor; +use std::sync::Arc; -use aes::Aes128; -use ccm::Ccm; -use ccm::KeyInit; -use ccm::aead::AeadInPlace; -use ccm::aead::generic_array::GenericArray; -use ccm::consts::{U8, U12, U16}; -use rand::RngExt; +use crypto::{AeadAlgorithm, AeadCipher, RTCCryptoProvider}; use super::*; use crate::content::*; use crate::record_layer::record_layer_header::*; use shared::error::*; -const CRYPTO_CCM_8_TAG_LENGTH: usize = 8; -const CRYPTO_CCM_TAG_LENGTH: usize = 16; const CRYPTO_CCM_NONCE_LENGTH: usize = 12; -type AesCcm8 = Ccm; -type AesCcm = Ccm; - #[derive(Clone)] +/// The authentication tag length a CCM suite uses. pub enum CryptoCcmTagLen { + /// An 8-byte tag, as the `_CCM_8` suites use. CryptoCcm8TagLength, + /// The full 16-byte tag. CryptoCcmTagLength, } -enum CryptoCcmType { - CryptoCcm8(AesCcm8), - CryptoCcm(AesCcm), -} - -// State needed to handle encrypted input/output +/// AES-CCM authenticated encryption for DTLS records, holding the per-direction keys. pub struct CryptoCcm { - local_ccm: CryptoCcmType, - remote_ccm: CryptoCcmType, + provider: Arc, + local_ccm: Box, + remote_ccm: Box, local_write_iv: Vec, remote_write_iv: Vec, - // used by clone() - local_write_key: Vec, - remote_write_key: Vec, -} - -impl Clone for CryptoCcm { - fn clone(&self) -> Self { - match self.local_ccm { - CryptoCcmType::CryptoCcm(_) => Self::new( - &CryptoCcmTagLen::CryptoCcmTagLength, - &self.local_write_key, - &self.local_write_iv, - &self.remote_write_key, - &self.remote_write_iv, - ), - CryptoCcmType::CryptoCcm8(_) => Self::new( - &CryptoCcmTagLen::CryptoCcm8TagLength, - &self.local_write_key, - &self.local_write_iv, - &self.remote_write_key, - &self.remote_write_iv, - ), - } - } } impl CryptoCcm { + /// Builds the cipher from the local and remote keys and salts. pub fn new( + provider: Arc, tag_len: &CryptoCcmTagLen, local_key: &[u8], local_write_iv: &[u8], remote_key: &[u8], remote_write_iv: &[u8], - ) -> Self { - let key = GenericArray::from_slice(local_key); - let local_ccm = match tag_len { - CryptoCcmTagLen::CryptoCcmTagLength => CryptoCcmType::CryptoCcm(AesCcm::new(key)), - CryptoCcmTagLen::CryptoCcm8TagLength => CryptoCcmType::CryptoCcm8(AesCcm8::new(key)), - }; - - let key = GenericArray::from_slice(remote_key); - let remote_ccm = match tag_len { - CryptoCcmTagLen::CryptoCcmTagLength => CryptoCcmType::CryptoCcm(AesCcm::new(key)), - CryptoCcmTagLen::CryptoCcm8TagLength => CryptoCcmType::CryptoCcm8(AesCcm8::new(key)), + ) -> Result { + let algorithm = match tag_len { + CryptoCcmTagLen::CryptoCcmTagLength => AeadAlgorithm::Aes128Ccm, + CryptoCcmTagLen::CryptoCcm8TagLength => AeadAlgorithm::Aes128Ccm8, }; - - CryptoCcm { + let local_ccm = provider + .crypto() + .new_aead(algorithm, local_key) + .map_err(crypto_error)?; + let remote_ccm = provider + .crypto() + .new_aead(algorithm, remote_key) + .map_err(crypto_error)?; + Ok(CryptoCcm { + provider, local_ccm, - local_write_key: local_key.to_vec(), local_write_iv: local_write_iv.to_vec(), remote_ccm, - remote_write_key: remote_key.to_vec(), remote_write_iv: remote_write_iv.to_vec(), - } + }) } - pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + /// Protects one record, returning header plus ciphertext. + /// + /// # Errors + /// + /// Fails if the cipher rejects the input. + pub fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; - let mut nonce = vec![0u8; CRYPTO_CCM_NONCE_LENGTH]; + let mut nonce = [0u8; CRYPTO_CCM_NONCE_LENGTH]; nonce[..4].copy_from_slice(&self.local_write_iv[..4]); - rand::rng().fill(&mut nonce[4..]); - let nonce = GenericArray::from_slice(&nonce); + self.provider + .random() + .fill(&mut nonce[4..]) + .map_err(crypto_error)?; let additional_data = generate_aead_additional_data(pkt_rlh, payload.len()); - let mut buffer: Vec = Vec::new(); - buffer.extend_from_slice(payload); - - match &self.local_ccm { - CryptoCcmType::CryptoCcm(ccm) => { - ccm.encrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - } - CryptoCcmType::CryptoCcm8(ccm8) => { - ccm8.encrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - } - } + let mut buffer = payload.to_vec(); + let mut tag = vec![0; self.local_ccm.tag_len()]; + self.local_ccm + .seal_in_place(&nonce, &additional_data, &mut buffer, &mut tag) + .map_err(crypto_error)?; - let mut r = Vec::with_capacity(raw.len() + nonce.len() + buffer.len()); + let mut r = Vec::with_capacity(raw.len() + 8 + buffer.len() + tag.len()); r.extend_from_slice(raw); r.extend_from_slice(&nonce[4..]); r.extend_from_slice(&buffer); + r.extend_from_slice(&tag); // Update recordLayer size to include explicit nonce let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; @@ -142,7 +108,12 @@ impl CryptoCcm { Ok(r) } - pub fn decrypt(&self, r: &[u8]) -> Result> { + /// Unprotects one record. + /// + /// # Errors + /// + /// Fails if authentication fails or the record is too short. + pub fn decrypt(&mut self, r: &[u8]) -> Result> { let mut reader = Cursor::new(r); let h = RecordLayerHeader::unmarshal(&mut reader)?; if h.content_type == ContentType::ChangeCipherSpec { @@ -154,30 +125,22 @@ impl CryptoCcm { return Err(Error::ErrNotEnoughRoomForNonce); } - let mut nonce = vec![]; - nonce.extend_from_slice(&self.remote_write_iv[..4]); - nonce.extend_from_slice(&r[RECORD_LAYER_HEADER_SIZE..RECORD_LAYER_HEADER_SIZE + 8]); - let nonce = GenericArray::from_slice(&nonce); + let mut nonce = [0; CRYPTO_CCM_NONCE_LENGTH]; + nonce[..4].copy_from_slice(&self.remote_write_iv[..4]); + nonce[4..].copy_from_slice(&r[RECORD_LAYER_HEADER_SIZE..RECORD_LAYER_HEADER_SIZE + 8]); let out = &r[RECORD_LAYER_HEADER_SIZE + 8..]; - let mut buffer: Vec = Vec::new(); - buffer.extend_from_slice(out); - - match &self.remote_ccm { - CryptoCcmType::CryptoCcm(ccm) => { - let additional_data = - generate_aead_additional_data(&h, out.len() - CRYPTO_CCM_TAG_LENGTH); - ccm.decrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - } - CryptoCcmType::CryptoCcm8(ccm8) => { - let additional_data = - generate_aead_additional_data(&h, out.len() - CRYPTO_CCM_8_TAG_LENGTH); - ccm8.decrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; - } + let tag_len = self.remote_ccm.tag_len(); + if out.len() < tag_len { + return Err(Error::ErrInvalidMac); } + let tag_start = out.len() - tag_len; + let additional_data = generate_aead_additional_data(&h, tag_start); + let mut buffer = out[..tag_start].to_vec(); + self.remote_ccm + .open_in_place(&nonce, &additional_data, &mut buffer, &out[tag_start..]) + .map_err(authentication_error)?; let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len()); d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); diff --git a/rtc-dtls/src/crypto/crypto_chacha20.rs b/rtc-dtls/src/crypto/crypto_chacha20.rs index 6cd5b306..f00f4553 100644 --- a/rtc-dtls/src/crypto/crypto_chacha20.rs +++ b/rtc-dtls/src/crypto/crypto_chacha20.rs @@ -1,8 +1,7 @@ use std::io::Cursor; +use std::sync::Arc; -use chacha20poly1305::aead::AeadInPlace; -use chacha20poly1305::aead::generic_array::GenericArray; -use chacha20poly1305::{ChaCha20Poly1305, KeyInit}; +use crypto::{AeadAlgorithm, AeadCipher, RTCCryptoProvider}; use super::*; use crate::content::*; @@ -12,12 +11,10 @@ const CRYPTO_CHACHA20_TAG_LENGTH: usize = 16; const CRYPTO_CHACHA20_NONCE_LENGTH: usize = 12; // State needed to handle encrypted input/output -#[derive(Clone)] +/// ChaCha20-Poly1305 authenticated encryption for DTLS records, holding the per-direction keys. pub struct CryptoChaCha20 { - local_cc: ChaCha20Poly1305, - remote_cc: ChaCha20Poly1305, - local_key: Vec, - remote_key: Vec, + local_cc: Box, + remote_cc: Box, local_write_iv: Vec, remote_write_iv: Vec, } @@ -31,48 +28,53 @@ fn noncegen(nonce: &mut [u8], epoch: u16, seqnum: u64) { } impl CryptoChaCha20 { + /// Builds the cipher from the local and remote keys and salts. pub fn new( + provider: Arc, local_key: &[u8], local_write_iv: &[u8], remote_key: &[u8], remote_write_iv: &[u8], - ) -> Self { - let key = GenericArray::from_slice(local_key); - let local_cc = ChaCha20Poly1305::new(key); - - let key = GenericArray::from_slice(remote_key); - let remote_cc = ChaCha20Poly1305::new(key); - - CryptoChaCha20 { + ) -> Result { + let local_cc = provider + .crypto() + .new_aead(AeadAlgorithm::ChaCha20Poly1305, local_key) + .map_err(crypto_error)?; + let remote_cc = provider + .crypto() + .new_aead(AeadAlgorithm::ChaCha20Poly1305, remote_key) + .map_err(crypto_error)?; + Ok(CryptoChaCha20 { local_cc, local_write_iv: local_write_iv.to_vec(), remote_cc, - local_key: local_key.to_vec(), - remote_key: remote_key.to_vec(), remote_write_iv: remote_write_iv.to_vec(), - } + }) } - pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + /// Protects one record, returning header plus ciphertext. + /// + /// # Errors + /// + /// Fails if the cipher rejects the input. + pub fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; - let mut nonce = vec![0u8; CRYPTO_CHACHA20_NONCE_LENGTH]; + let mut nonce = [0u8; CRYPTO_CHACHA20_NONCE_LENGTH]; nonce[..CRYPTO_CHACHA20_NONCE_LENGTH] .copy_from_slice(&self.local_write_iv[..CRYPTO_CHACHA20_NONCE_LENGTH]); noncegen(&mut nonce[..], pkt_rlh.epoch, pkt_rlh.sequence_number); - let nonce = GenericArray::from_slice(&nonce); - let additional_data = generate_aead_additional_data(pkt_rlh, payload.len()); let mut buffer: Vec = Vec::new(); buffer.extend_from_slice(payload); - let tag = self - .local_cc - .encrypt_in_place_detached(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; + let mut tag = [0; CRYPTO_CHACHA20_TAG_LENGTH]; + self.local_cc + .seal_in_place(&nonce, &additional_data, &mut buffer, &mut tag) + .map_err(crypto_error)?; let mut r = Vec::with_capacity(raw.len() + buffer.len() + tag.len()); r.extend_from_slice(raw); @@ -87,7 +89,12 @@ impl CryptoChaCha20 { Ok(r) } - pub fn decrypt(&self, r: &[u8]) -> Result> { + /// Unprotects one record. + /// + /// # Errors + /// + /// Fails if authentication fails or the record is too short. + pub fn decrypt(&mut self, r: &[u8]) -> Result> { let mut reader = Cursor::new(r); let h = RecordLayerHeader::unmarshal(&mut reader)?; if h.content_type == ContentType::ChangeCipherSpec { @@ -95,23 +102,24 @@ impl CryptoChaCha20 { return Ok(r.to_vec()); } - let mut nonce = vec![]; - nonce.extend_from_slice(&self.remote_write_iv[..]); + let mut nonce = [0; CRYPTO_CHACHA20_NONCE_LENGTH]; + nonce.copy_from_slice(&self.remote_write_iv[..]); noncegen(&mut nonce[..], h.epoch, h.sequence_number); - let nonce = GenericArray::from_slice(&nonce); - let out = &r[RECORD_LAYER_HEADER_SIZE..]; + if out.len() < CRYPTO_CHACHA20_TAG_LENGTH { + return Err(Error::ErrInvalidMac); + } let additional_data = generate_aead_additional_data(&h, out.len() - CRYPTO_CHACHA20_TAG_LENGTH); - let mut buffer: Vec = Vec::new(); - buffer.extend_from_slice(out); + let tag_start = out.len() - CRYPTO_CHACHA20_TAG_LENGTH; + let mut buffer = out[..tag_start].to_vec(); self.remote_cc - .decrypt_in_place(nonce, &additional_data, &mut buffer) - .map_err(|e| Error::Other(e.to_string()))?; + .open_in_place(&nonce, &additional_data, &mut buffer, &out[tag_start..]) + .map_err(authentication_error)?; let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + buffer.len()); d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); diff --git a/rtc-dtls/src/crypto/crypto_gcm.rs b/rtc-dtls/src/crypto/crypto_gcm.rs index 08d79168..8f5828ae 100644 --- a/rtc-dtls/src/crypto/crypto_gcm.rs +++ b/rtc-dtls/src/crypto/crypto_gcm.rs @@ -4,11 +4,9 @@ // RFC 5288 year 2008 https://tools.ietf.org/html/rfc5288 use std::io::Cursor; -#[cfg(feature = "aws-lc-rs")] use std::sync::Arc; -use rand::RngExt; -use ring::aead::{AES_128_GCM, Aad, LessSafeKey, Nonce, UnboundKey}; +use crypto::{AeadAlgorithm, AeadCipher, RTCCryptoProvider}; use super::*; use crate::content::*; @@ -18,65 +16,56 @@ use shared::error::*; const CRYPTO_GCM_TAG_LENGTH: usize = 16; const CRYPTO_GCM_NONCE_LENGTH: usize = 12; -// State needed to handle encrypted input/output. -// -// The AES-128-GCM AEAD runs on ring's hardware-accelerated single-pass assembly -// (AES-NI + CLMUL on x86_64, ARMv8 AES + PMULL on aarch64) instead of the -// pure-Rust RustCrypto `aes-gcm`. ring is already a dependency of this crate -// (handshake signatures / key generation). `LessSafeKey` is `Clone`, so -// `CryptoGcm` stays cloneable for the cipher-suite state that embeds it. -#[derive(Clone)] +/// AES-GCM authenticated encryption for DTLS records, holding the per-direction keys. pub struct CryptoGcm { - #[cfg(feature = "ring")] - local_gcm: LessSafeKey, - #[cfg(feature = "ring")] - remote_gcm: LessSafeKey, - // Arc is needed until `Clone` is implemented - // https://github.com/aws/aws-lc-rs/issues/1165 - #[cfg(feature = "aws-lc-rs")] - local_gcm: Arc, - #[cfg(feature = "aws-lc-rs")] - remote_gcm: Arc, + provider: Arc, + local_gcm: Box, + remote_gcm: Box, local_write_iv: Vec, remote_write_iv: Vec, } impl CryptoGcm { + /// Builds the cipher from the local and remote keys and salts. pub fn new( + provider: Arc, local_key: &[u8], local_write_iv: &[u8], remote_key: &[u8], remote_write_iv: &[u8], - ) -> Self { - // Keys are exactly AES_128_GCM.key_len() (16) bytes as derived by the - // handshake; a wrong length here is a programming error. - let local_gcm = LessSafeKey::new( - UnboundKey::new(&AES_128_GCM, local_key).expect("valid AES-128-GCM local key"), - ); - let remote_gcm = LessSafeKey::new( - UnboundKey::new(&AES_128_GCM, remote_key).expect("valid AES-128-GCM remote key"), - ); - - #[cfg(feature = "aws-lc-rs")] - let local_gcm = Arc::new(local_gcm); - #[cfg(feature = "aws-lc-rs")] - let remote_gcm = Arc::new(remote_gcm); - - CryptoGcm { + ) -> Result { + let local_gcm = provider + .crypto() + .new_aead(AeadAlgorithm::Aes128Gcm, local_key) + .map_err(crypto_error)?; + let remote_gcm = provider + .crypto() + .new_aead(AeadAlgorithm::Aes128Gcm, remote_key) + .map_err(crypto_error)?; + Ok(CryptoGcm { + provider, local_gcm, local_write_iv: local_write_iv.to_vec(), remote_gcm, remote_write_iv: remote_write_iv.to_vec(), - } + }) } - pub fn encrypt(&self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { + /// Protects one record, returning header plus ciphertext. + /// + /// # Errors + /// + /// Fails if the cipher rejects the input. + pub fn encrypt(&mut self, pkt_rlh: &RecordLayerHeader, raw: &[u8]) -> Result> { let payload = &raw[RECORD_LAYER_HEADER_SIZE..]; let raw = &raw[..RECORD_LAYER_HEADER_SIZE]; let mut nonce = [0u8; CRYPTO_GCM_NONCE_LENGTH]; nonce[..4].copy_from_slice(&self.local_write_iv[..4]); - rand::rng().fill(&mut nonce[4..]); + self.provider + .random() + .fill(&mut nonce[4..]) + .map_err(crypto_error)?; let additional_data = generate_aead_additional_data(pkt_rlh, payload.len()); @@ -90,15 +79,16 @@ impl CryptoGcm { r.extend_from_slice(&nonce[4..]); r.extend_from_slice(payload); - let tag = self - .local_gcm - .seal_in_place_separate_tag( - Nonce::assume_unique_for_key(nonce), - Aad::from(&additional_data), + let mut tag = [0; CRYPTO_GCM_TAG_LENGTH]; + self.local_gcm + .seal_in_place( + &nonce, + &additional_data, &mut r[RECORD_LAYER_HEADER_SIZE + 8..], + &mut tag, ) - .map_err(|e| Error::Other(format!("DTLS AES-GCM seal failed: {e}")))?; - r.extend_from_slice(tag.as_ref()); + .map_err(crypto_error)?; + r.extend_from_slice(&tag); // Update recordLayer size to include explicit nonce let r_len = (r.len() - RECORD_LAYER_HEADER_SIZE) as u16; @@ -108,7 +98,12 @@ impl CryptoGcm { Ok(r) } - pub fn decrypt(&self, r: &[u8]) -> Result> { + /// Unprotects one record. + /// + /// # Errors + /// + /// Fails if authentication fails or the record is too short. + pub fn decrypt(&mut self, r: &[u8]) -> Result> { let mut reader = Cursor::new(r); let h = RecordLayerHeader::unmarshal(&mut reader)?; if h.content_type == ContentType::ChangeCipherSpec { @@ -135,24 +130,17 @@ impl CryptoGcm { let additional_data = generate_aead_additional_data(&h, tag_start); - // Copy header + ciphertext||tag once and decrypt the ciphertext+tag - // region in place. ring's `open_in_place` wants ciphertext and tag - // contiguous (they already are on the wire) and returns the plaintext - // slice; drop the trailing tag afterwards. - let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + out.len()); + let mut d = Vec::with_capacity(RECORD_LAYER_HEADER_SIZE + tag_start); d.extend_from_slice(&r[..RECORD_LAYER_HEADER_SIZE]); - d.extend_from_slice(out); - - let plaintext_len = self - .remote_gcm + d.extend_from_slice(&out[..tag_start]); + self.remote_gcm .open_in_place( - Nonce::assume_unique_for_key(nonce), - Aad::from(&additional_data), + &nonce, + &additional_data, &mut d[RECORD_LAYER_HEADER_SIZE..], + &out[tag_start..], ) - .map_err(|e| Error::Other(format!("DTLS AES-GCM open failed: {e}")))? - .len(); - d.truncate(RECORD_LAYER_HEADER_SIZE + plaintext_len); + .map_err(authentication_error)?; Ok(d) } @@ -183,8 +171,17 @@ mod tests { let local_iv = [0x22u8; 4]; let remote_key = [0x33u8; 16]; let remote_iv = [0x44u8; 4]; - let sender = CryptoGcm::new(&local_key, &local_iv, &remote_key, &remote_iv); - let receiver = CryptoGcm::new(&remote_key, &remote_iv, &local_key, &local_iv); + let provider = crypto::default_provider().unwrap(); + let mut sender = CryptoGcm::new( + provider.clone(), + &local_key, + &local_iv, + &remote_key, + &remote_iv, + ) + .unwrap(); + let mut receiver = + CryptoGcm::new(provider, &remote_key, &remote_iv, &local_key, &local_iv).unwrap(); let payload = b"application data!"; let (header, raw) = make_record(payload); @@ -217,7 +214,8 @@ mod tests { fn test_crypto_gcm_decrypt_too_short_for_tag() { let key = [0x11u8; 16]; let iv = [0x22u8; 4]; - let cg = CryptoGcm::new(&key, &iv, &key, &iv); + let mut cg = + CryptoGcm::new(crypto::default_provider().unwrap(), &key, &iv, &key, &iv).unwrap(); let (_, mut raw) = make_record(&[0u8; 0]); // 8-byte explicit nonce plus 10 bytes: less than the 16-byte tag. @@ -231,7 +229,8 @@ mod tests { fn test_crypto_gcm_decrypt_rejects_tampering() { let key = [0x11u8; 16]; let iv = [0x22u8; 4]; - let cg = CryptoGcm::new(&key, &iv, &key, &iv); + let mut cg = + CryptoGcm::new(crypto::default_provider().unwrap(), &key, &iv, &key, &iv).unwrap(); let (header, raw) = make_record(b"payload"); let mut encrypted = cg.encrypt(&header, &raw).unwrap(); diff --git a/rtc-dtls/src/crypto/crypto_test.rs b/rtc-dtls/src/crypto/crypto_test.rs index ac65d6f5..24ad7a1e 100644 --- a/rtc-dtls/src/crypto/crypto_test.rs +++ b/rtc-dtls/src/crypto/crypto_test.rs @@ -1,50 +1,19 @@ -use std::io::Cursor; - -use x509_parser::pem::Pem; - use super::crypto_ccm::*; use super::*; use crate::content::ContentType; use crate::record_layer::record_layer_header::{ProtocolVersion, RECORD_LAYER_HEADER_SIZE}; - -const RAW_PRIVATE_KEY: &str = " ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAxIA2BrrnR2sIlATsp7aRBD/3krwZ7vt9dNeoDQAee0s6SuYP -6MBx/HPnAkwNvPS90R05a7pwRkoT6Ur4PfPhCVlUe8lV+0Eto3ZSEeHz3HdsqlM3 -bso67L7Dqrc7MdVstlKcgJi8yeAoGOIL9/igOv0XBFCeznm9nznx6mnsR5cugw+1 -ypXelaHmBCLV7r5SeVSh57+KhvZGbQ2fFpUaTPegRpJZXBNS8lSeWvtOv9d6N5UB -ROTAJodMZT5AfX0jB0QB9IT/0I96H6BSENH08NXOeXApMuLKvnAf361rS7cRAfRL -rWZqERMP4u6Cnk0Cnckc3WcW27kGGIbtwbqUIQIDAQABAoIBAGF7OVIdZp8Hejn0 -N3L8HvT8xtUEe9kS6ioM0lGgvX5s035Uo4/T6LhUx0VcdXRH9eLHnLTUyN4V4cra -ZkxVsE3zAvZl60G6E+oDyLMWZOP6Wu4kWlub9597A5atT7BpMIVCdmFVZFLB4SJ3 -AXkC3nplFAYP+Lh1rJxRIrIn2g+pEeBboWbYA++oDNuMQffDZaokTkJ8Bn1JZYh0 -xEXKY8Bi2Egd5NMeZa1UFO6y8tUbZfwgVs6Enq5uOgtfayq79vZwyjj1kd29MBUD -8g8byV053ZKxbUOiOuUts97eb+fN3DIDRTcT2c+lXt/4C54M1FclJAbtYRK/qwsl -pYWKQAECgYEA4ZUbqQnTo1ICvj81ifGrz+H4LKQqe92Hbf/W51D/Umk2kP702W22 -HP4CvrJRtALThJIG9m2TwUjl/WAuZIBrhSAbIvc3Fcoa2HjdRp+sO5U1ueDq7d/S -Z+PxRI8cbLbRpEdIaoR46qr/2uWZ943PHMv9h4VHPYn1w8b94hwD6vkCgYEA3v87 -mFLzyM9ercnEv9zHMRlMZFQhlcUGQZvfb8BuJYl/WogyT6vRrUuM0QXULNEPlrin -mBQTqc1nCYbgkFFsD2VVt1qIyiAJsB9MD1LNV6YuvE7T2KOSadmsA4fa9PUqbr71 -hf3lTTq+LeR09LebO7WgSGYY+5YKVOEGpYMR1GkCgYEAxPVQmk3HKHEhjgRYdaG5 -lp9A9ZE8uruYVJWtiHgzBTxx9TV2iST+fd/We7PsHFTfY3+wbpcMDBXfIVRKDVwH -BMwchXH9+Ztlxx34bYJaegd0SmA0Hw9ugWEHNgoSEmWpM1s9wir5/ELjc7dGsFtz -uzvsl9fpdLSxDYgAAdzeGtkCgYBAzKIgrVox7DBzB8KojhtD5ToRnXD0+H/M6OKQ -srZPKhlb0V/tTtxrIx0UUEFLlKSXA6mPw6XDHfDnD86JoV9pSeUSlrhRI+Ysy6tq -eIE7CwthpPZiaYXORHZ7wCqcK/HcpJjsCs9rFbrV0yE5S3FMdIbTAvgXg44VBB7O -UbwIoQKBgDuY8gSrA5/A747wjjmsdRWK4DMTMEV4eCW1BEP7Tg7Cxd5n3xPJiYhr -nhLGN+mMnVIcv2zEMS0/eNZr1j/0BtEdx+3IC6Eq+ONY0anZ4Irt57/5QeKgKn/L -JPhfPySIPG4UmwE4gW8t79vfOKxnUu2fDD1ZXUYopan6EckACNH/ ------END RSA PRIVATE KEY----- -"; +use crate::signature_hash_algorithm::HashAlgorithm; +use crypto::RTCCryptoProvider; #[test] fn test_generate_key_signature() -> Result<()> { - let reader = Cursor::new(RAW_PRIVATE_KEY.as_bytes()); - let pem = match Pem::read(reader) { - Ok((pem, _)) => pem, - Err(_) => return Err(Error::Other("Pem::read error".to_owned())), - }; - //let private_key = rsa::RSAPrivateKey::from_pkcs1(&pem.contents)?; + let provider = crypto::default_provider().map_err(crypto_error)?; + let scheme = crypto::SignatureScheme::EcdsaP256Sha256; + let signing_key = provider + .crypto() + .generate_signing_key(scheme) + .map_err(crypto_error)?; + let private_key = CryptoPrivateKey::from_signing_key(signing_key.clone()); let client_random = vec![ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, @@ -61,45 +30,97 @@ fn test_generate_key_signature() -> Result<()> { 0xf9, 0x10, 0xa0, 0x53, 0x5b, 0x14, 0x88, 0xd7, 0xf8, 0xfa, 0xbb, 0x34, 0x9a, 0x98, 0x28, 0x80, 0xb6, 0x15, ]; - let expected_signature = vec![ - 0x6f, 0x47, 0x97, 0x85, 0xcc, 0x76, 0x50, 0x93, 0xbd, 0xe2, 0x6a, 0x69, 0x0b, 0xc3, 0x03, - 0xd1, 0xb7, 0xe4, 0xab, 0x88, 0x7b, 0xa6, 0x52, 0x80, 0xdf, 0xaa, 0x25, 0x7a, 0xdb, 0x29, - 0x32, 0xe4, 0xd8, 0x28, 0x28, 0xb3, 0xe8, 0x04, 0x3c, 0x38, 0x16, 0xfc, 0x78, 0xe9, 0x15, - 0x7b, 0xc5, 0xbd, 0x7d, 0xfc, 0xcd, 0x83, 0x00, 0x57, 0x4a, 0x3c, 0x23, 0x85, 0x75, 0x6b, - 0x37, 0xd5, 0x89, 0x72, 0x73, 0xf0, 0x44, 0x8c, 0x00, 0x70, 0x1f, 0x6e, 0xa2, 0x81, 0xd0, - 0x09, 0xc5, 0x20, 0x36, 0xab, 0x23, 0x09, 0x40, 0x1f, 0x4d, 0x45, 0x96, 0x62, 0xbb, 0x81, - 0xb0, 0x30, 0x72, 0xad, 0x3a, 0x0a, 0xac, 0x31, 0x63, 0x40, 0x52, 0x0a, 0x27, 0xf3, 0x34, - 0xde, 0x27, 0x7d, 0xb7, 0x54, 0xff, 0x0f, 0x9f, 0x5a, 0xfe, 0x07, 0x0f, 0x4e, 0x9f, 0x53, - 0x04, 0x34, 0x62, 0xf4, 0x30, 0x74, 0x83, 0x35, 0xfc, 0xe4, 0x7e, 0xbf, 0x5a, 0xc4, 0x52, - 0xd0, 0xea, 0xf9, 0x61, 0x4e, 0xf5, 0x1c, 0x0e, 0x58, 0x02, 0x71, 0xfb, 0x1f, 0x34, 0x55, - 0xe8, 0x36, 0x70, 0x3c, 0xc1, 0xcb, 0xc9, 0xb7, 0xbb, 0xb5, 0x1c, 0x44, 0x9a, 0x6d, 0x88, - 0x78, 0x98, 0xd4, 0x91, 0x2e, 0xeb, 0x98, 0x81, 0x23, 0x30, 0x73, 0x39, 0x43, 0xd5, 0xbb, - 0x70, 0x39, 0xba, 0x1f, 0xdb, 0x70, 0x9f, 0x91, 0x83, 0x56, 0xc2, 0xde, 0xed, 0x17, 0x6d, - 0x2c, 0x3e, 0x21, 0xea, 0x36, 0xb4, 0x91, 0xd8, 0x31, 0x05, 0x60, 0x90, 0xfd, 0xc6, 0x74, - 0xa9, 0x7b, 0x18, 0xfc, 0x1c, 0x6a, 0x1c, 0x6e, 0xec, 0xd3, 0xc1, 0xc0, 0x0d, 0x11, 0x25, - 0x48, 0x37, 0x3d, 0x45, 0x11, 0xa2, 0x31, 0x14, 0x0a, 0x66, 0x9f, 0xd8, 0xac, 0x74, 0xa2, - 0xcd, 0xc8, 0x79, 0xb3, 0x9e, 0xc6, 0x66, 0x25, 0xcf, 0x2c, 0x87, 0x5e, 0x5c, 0x36, 0x75, - 0x86, - ]; - let signature = generate_key_signature( &client_random, &server_random, &public_key, NamedCurve::X25519, - &CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Rsa256( - ring::rsa::KeyPair::from_der(&pem.contents) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der: pem.contents.clone(), - }, //hashAlgorithmSHA256, + &SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }, + &private_key, )?; - assert_eq!( - signature, expected_signature, - "Signature generation failed \nexp {expected_signature:?} \nactual {signature:?} " - ); + provider + .crypto() + .verify_signature( + scheme, + signing_key.public_key(), + &value_key_message( + &client_random, + &server_random, + &public_key, + NamedCurve::X25519, + ), + &signature, + ) + .map_err(crypto_error)?; + + Ok(()) +} + +#[test] +fn test_exported_signing_key_can_be_imported() -> Result<()> { + let provider = crypto::default_provider().map_err(crypto_error)?; + let scheme = crypto::SignatureScheme::EcdsaP256Sha256; + let generated = provider + .crypto() + .generate_signing_key(scheme) + .map_err(crypto_error)?; + let pkcs8 = generated + .to_pkcs8_der() + .map_err(crypto_error)? + .expect("built-in generated keys are exportable"); + let imported = provider + .crypto() + .import_signing_key(scheme, pkcs8.as_ref()) + .map_err(crypto_error)?; + let signature = imported + .sign(scheme, b"imported DTLS key") + .map_err(crypto_error)?; + + provider + .crypto() + .verify_signature( + scheme, + imported.public_key(), + b"imported DTLS key", + &signature, + ) + .map_err(crypto_error) +} + +#[cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] +#[test] +fn test_cross_provider_signature_verification() -> Result<()> { + let ring = crypto::providers::RingProvider::new(); + let aws = crypto::providers::AwsLcRsProvider::new(); + let scheme = crypto::SignatureScheme::EcdsaP256Sha256; + + for (signer, verifier) in [ + ( + ring.crypto() as &dyn crypto::RTCCrypto, + aws.crypto() as &dyn crypto::RTCCrypto, + ), + ( + aws.crypto() as &dyn crypto::RTCCrypto, + ring.crypto() as &dyn crypto::RTCCrypto, + ), + ] { + let key = signer.generate_signing_key(scheme).map_err(crypto_error)?; + let signature = key + .sign(scheme, b"cross-provider DTLS signature") + .map_err(crypto_error)?; + verifier + .verify_signature( + scheme, + key.public_key(), + b"cross-provider DTLS signature", + &signature, + ) + .map_err(crypto_error)?; + } Ok(()) } @@ -112,7 +133,14 @@ fn test_ccm_encryption_and_decryption() -> Result<()> { ]; let iv = vec![0x0e, 0xb2, 0x09, 0x06]; - let ccm = CryptoCcm::new(&CryptoCcmTagLen::CryptoCcmTagLength, &key, &iv, &key, &iv); + let mut ccm = CryptoCcm::new( + crypto::default_provider().map_err(crypto_error)?, + &CryptoCcmTagLen::CryptoCcmTagLength, + &key, + &iv, + &key, + &iv, + )?; let rlh = RecordLayerHeader { content_type: ContentType::ApplicationData, @@ -155,6 +183,7 @@ fn test_ccm_encryption_and_decryption() -> Result<()> { #[test] fn test_certificate_verify() -> Result<()> { + let provider = crypto::default_provider().map_err(crypto_error)?; let plain_text: Vec = vec![ 0x6f, 0x47, 0x97, 0x85, 0xcc, 0x76, 0x50, 0x93, 0xbd, 0xe2, 0x6a, 0x69, 0x0b, 0xc3, 0x03, 0xd1, 0xb7, 0xe4, 0xab, 0x88, 0x7b, 0xa6, 0x52, 0x80, 0xdf, 0xaa, 0x25, 0x7a, 0xdb, 0x29, @@ -177,15 +206,23 @@ fn test_certificate_verify() -> Result<()> { ]; //test ECDSA256 - let certificate_ecdsa256 = Certificate::generate_self_signed(vec!["localhost".to_owned()])?; - let cert_verify_ecdsa256 = - generate_certificate_verify(&plain_text, &certificate_ecdsa256.private_key)?; + let certificate_ecdsa256 = Certificate::generate_self_signed( + vec!["localhost".to_owned()], + crypto::default_provider().map_err(crypto_error)?.crypto(), + )?; + let ecdsa_algorithm = SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }; + let cert_verify_ecdsa256 = generate_certificate_verify( + &plain_text, + &ecdsa_algorithm, + &certificate_ecdsa256.private_key, + )?; verify_certificate_verify( + provider.crypto(), &plain_text, - &SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ecdsa, - }, + &ecdsa_algorithm, &cert_verify_ecdsa256, &certificate_ecdsa256 .certificate @@ -199,15 +236,21 @@ fn test_certificate_verify() -> Result<()> { let certificate_ed25519 = Certificate::generate_self_signed_with_alg( vec!["localhost".to_owned()], &rcgen::PKCS_ED25519, + crypto::default_provider().map_err(crypto_error)?.crypto(), + )?; + let ed25519_algorithm = SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ed25519, + }; + let cert_verify_ed25519 = generate_certificate_verify( + &plain_text, + &ed25519_algorithm, + &certificate_ed25519.private_key, )?; - let cert_verify_ed25519 = - generate_certificate_verify(&plain_text, &certificate_ed25519.private_key)?; verify_certificate_verify( + provider.crypto(), &plain_text, - &SignatureHashAlgorithm { - hash: HashAlgorithm::Sha256, - signature: SignatureAlgorithm::Ed25519, - }, + &ed25519_algorithm, &cert_verify_ed25519, &certificate_ed25519 .certificate @@ -227,36 +270,47 @@ struct MockSigner { signature: Vec, } -impl CustomSigner for MockSigner { - fn sign(&self, message: &[u8]) -> std::result::Result, String> { +impl SigningKey for MockSigner { + fn supports(&self, _scheme: CryptoSignatureScheme) -> bool { + true + } + + fn public_key(&self) -> PublicKey<'_> { + PublicKey { + encoding: PublicKeyEncoding::SubjectPublicKeyInfoDer, + bytes: &[], + } + } + + fn sign( + &self, + _scheme: CryptoSignatureScheme, + message: &[u8], + ) -> std::result::Result, crypto::CryptoError> { *self.call_count.lock().unwrap() += 1; *self.last_message.lock().unwrap() = message.to_vec(); Ok(self.signature.clone()) } - - fn clone_box(&self) -> Box { - Box::new(MockSigner { - call_count: std::sync::Arc::clone(&self.call_count), - last_message: std::sync::Arc::clone(&self.last_message), - signature: self.signature.clone(), - }) - } } #[test] -fn test_custom_signer_is_invoked_for_signing() -> Result<()> { +fn test_external_signing_key_is_invoked_for_signing() -> Result<()> { let expected_signature = vec![0xca, 0xfe, 0xba, 0xbe]; let call_count = std::sync::Arc::new(std::sync::Mutex::new(0usize)); let last_message = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); - let private_key = CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Custom(Box::new(MockSigner { - call_count: std::sync::Arc::clone(&call_count), - last_message: std::sync::Arc::clone(&last_message), - signature: expected_signature.clone(), - })), - serialized_der: vec![], - }; + let private_key = CryptoPrivateKey::from_signing_key(std::sync::Arc::new(MockSigner { + call_count: std::sync::Arc::clone(&call_count), + last_message: std::sync::Arc::clone(&last_message), + signature: expected_signature.clone(), + })); + assert!( + private_key + .signing_key + .to_pkcs8_der() + .map_err(crypto_error)? + .is_none() + ); let client_random = [0x01u8, 0x02, 0x03, 0x04]; let server_random = [0x05u8, 0x06, 0x07, 0x08]; @@ -264,12 +318,17 @@ fn test_custom_signer_is_invoked_for_signing() -> Result<()> { let named_curve = NamedCurve::X25519; let expected_key_message = value_key_message(&client_random, &server_random, &public_key, named_curve); + let algorithm = SignatureHashAlgorithm { + hash: HashAlgorithm::Sha256, + signature: SignatureAlgorithm::Ecdsa, + }; let key_signature = generate_key_signature( &client_random, &server_random, &public_key, named_curve, + &algorithm, &private_key, )?; @@ -278,7 +337,7 @@ fn test_custom_signer_is_invoked_for_signing() -> Result<()> { assert_eq!(key_signature, expected_signature); let handshake_bodies = b"certificate-verify-handshake-bodies"; - let cert_verify = generate_certificate_verify(handshake_bodies, &private_key)?; + let cert_verify = generate_certificate_verify(handshake_bodies, &algorithm, &private_key)?; assert_eq!(*call_count.lock().unwrap(), 2); assert_eq!(&*last_message.lock().unwrap(), handshake_bodies); diff --git a/rtc-dtls/src/crypto/mod.rs b/rtc-dtls/src/crypto/mod.rs index b9cf0154..89ffff11 100644 --- a/rtc-dtls/src/crypto/mod.rs +++ b/rtc-dtls/src/crypto/mod.rs @@ -1,11 +1,23 @@ +//! Cryptographic primitives for DTLS. +//! +//! The record ciphers ([`crypto_gcm`](crate::crypto::crypto_gcm), [`crypto_ccm`](crate::crypto::crypto_ccm), [`crypto_chacha20`](crate::crypto::crypto_chacha20), [`crypto_cbc`](crate::crypto::crypto_cbc)), plus +//! the certificate and signature handling the handshake needs. Which cipher is used is decided by +//! the negotiated cipher suite, so a caller normally reaches these only through +//! [`CipherSuite`](crate::cipher_suite::CipherSuite). +//! +//! Certificates here are usually self-signed: WebRTC authenticates a peer by comparing the +//! certificate fingerprint against the one signalled in SDP, not by validating a CA chain. #[cfg(test)] mod crypto_test; +/// AES-CBC with a separate HMAC, for the older CBC suites. pub mod crypto_cbc; +/// AES-CCM authenticated encryption. pub mod crypto_ccm; +/// ChaCha20-Poly1305 authenticated encryption. pub mod crypto_chacha20; +/// AES-GCM authenticated encryption. pub mod crypto_gcm; -pub mod padding; use std::convert::TryFrom; use std::sync::Arc; @@ -17,15 +29,33 @@ use rustls::client::danger::ServerCertVerifier; use rustls::pki_types::{CertificateDer, ServerName}; use rustls::server::danger::ClientCertVerifier; +use crypto::{ + PublicKey, PublicKeyEncoding, RTCCrypto, SignatureScheme as CryptoSignatureScheme, SigningKey, +}; +#[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] use rcgen::{CertifiedKey, KeyPair, generate_simple_self_signed}; -use ring::rand::SystemRandom; -use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; use crate::curve::named_curve::*; use crate::record_layer::record_layer_header::*; -use crate::signature_hash_algorithm::{HashAlgorithm, SignatureAlgorithm, SignatureHashAlgorithm}; +use crate::signature_hash_algorithm::{SignatureAlgorithm, SignatureHashAlgorithm}; use shared::error::*; +pub(crate) fn crypto_error(error: crypto::CryptoError) -> Error { + Error::Crypto(error.to_string()) +} + +pub(crate) fn authentication_error(_: crypto::CryptoError) -> Error { + Error::ErrInvalidMac +} + +fn signature_verification_error(error: crypto::CryptoError) -> Error { + match error { + crypto::CryptoError::InvalidSignature => Error::ErrKeySignatureMismatch, + crypto::CryptoError::UnsupportedAlgorithm(_) => Error::ErrKeySignatureVerifyUnimplemented, + error => crypto_error(error), + } +} + /// A X.509 certificate(s) used to authenticate a DTLS connection. #[derive(Clone, PartialEq, Debug)] pub struct Certificate { @@ -36,37 +66,43 @@ pub struct Certificate { } impl Certificate { - /// Generate a self-signed certificate. - /// - /// See [`rcgen::generate_simple_self_signed`]. - pub fn generate_self_signed(subject_alt_names: impl Into>) -> Result { - let CertifiedKey { cert, signing_key } = generate_simple_self_signed(subject_alt_names)?; + #[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] + /// Generates a self-signed certificate, importing its key into `provider`. + pub fn generate_self_signed( + subject_alt_names: impl Into>, + crypto: &dyn RTCCrypto, + ) -> Result { + let CertifiedKey { cert, signing_key } = generate_simple_self_signed(subject_alt_names) + .map_err(|error| Error::Other(error.to_string()))?; Ok(Certificate { certificate: vec![cert.der().to_owned()], - private_key: CryptoPrivateKey::try_from(&signing_key)?, + private_key: CryptoPrivateKey::from_key_pair(&signing_key, crypto)?, }) } - /// Generate a self-signed certificate with the given algorithm. - /// - /// See `rcgen::Certificate::self_signed`. + #[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] + /// Generates a self-signed certificate with `alg`, importing its key into `provider`. pub fn generate_self_signed_with_alg( subject_alt_names: impl Into>, alg: &'static rcgen::SignatureAlgorithm, + crypto: &dyn RTCCrypto, ) -> Result { - let params = rcgen::CertificateParams::new(subject_alt_names).unwrap(); - let key_pair = rcgen::KeyPair::generate_for(alg).unwrap(); - let cert = params.self_signed(&key_pair).unwrap(); + let params = rcgen::CertificateParams::new(subject_alt_names) + .map_err(|error| Error::Other(error.to_string()))?; + let key_pair = + rcgen::KeyPair::generate_for(alg).map_err(|error| Error::Other(error.to_string()))?; + let cert = params + .self_signed(&key_pair) + .map_err(|error| Error::Other(error.to_string()))?; Ok(Certificate { certificate: vec![cert.der().to_owned()], - private_key: CryptoPrivateKey::try_from(&key_pair)?, + private_key: CryptoPrivateKey::from_key_pair(&key_pair, crypto)?, }) } - /// Parses a certificate from the ASCII PEM format. - #[cfg(feature = "pem")] - pub fn from_pem(pem_str: &str) -> Result { + /// Parses a PEM certificate and imports its PKCS#8 key into `crypto`. + pub fn from_pem(pem_str: &str, crypto: &dyn RTCCrypto) -> Result { let mut pems = pem::parse_many(pem_str).map_err(|e| Error::InvalidPEM(e.to_string()))?; if pems.len() < 2 { return Err(Error::InvalidPEM(format!( @@ -81,8 +117,7 @@ impl Certificate { ))); } - let keypair = KeyPair::try_from(pems[0].contents()) - .map_err(|e| Error::InvalidPEM(format!("can't decode keypair: {e}")))?; + let private_key_der = pems[0].contents().to_vec(); let mut rustls_certs = Vec::new(); for p in pems.drain(1..) { @@ -95,18 +130,31 @@ impl Certificate { rustls_certs.push(CertificateDer::from(p.contents().to_vec())); } - Ok(Certificate { - certificate: rustls_certs, - private_key: CryptoPrivateKey::try_from(&keypair)?, - }) + let schemes = [ + CryptoSignatureScheme::Ed25519, + CryptoSignatureScheme::EcdsaP256Sha256, + CryptoSignatureScheme::RsaPkcs1Sha256, + ]; + let signing_key = schemes + .into_iter() + .filter(|scheme| crypto.supports(crypto::CryptoAlgorithm::SigningKeyImport(*scheme))) + .find_map(|scheme| crypto.import_signing_key(scheme, &private_key_der).ok()) + .ok_or_else(|| Error::InvalidPEM("can't decode PKCS#8 signing key".into()))?; + + Ok(Certificate::from_signing_key(rustls_certs, signing_key)) } /// Serializes the certificate (including the private key) in PKCS#8 format in PEM. - #[cfg(feature = "pem")] - pub fn serialize_pem(&self) -> String { + pub fn serialize_pem(&self) -> Result { + let private_key = self + .private_key + .signing_key + .to_pkcs8_der() + .map_err(crypto_error)? + .ok_or_else(|| Error::Other("the certificate signing key is not exportable".into()))?; let mut data = vec![pem::Pem::new( "PRIVATE_KEY".to_string(), - self.private_key.serialized_der.clone(), + private_key.as_ref(), )]; for rustls_cert in &self.certificate { data.push(pem::Pem::new( @@ -114,7 +162,18 @@ impl Certificate { rustls_cert.as_ref(), )); } - pem::encode_many(&data) + Ok(pem::encode_many(&data)) + } + + /// Builds a certificate chain around an application-owned signing key, including HSM/KMS keys. + pub fn from_signing_key( + certificate: Vec>, + signing_key: Arc, + ) -> Self { + Self { + certificate, + private_key: CryptoPrivateKey::from_signing_key(signing_key), + } } } @@ -138,143 +197,55 @@ pub(crate) fn value_key_message( plaintext } -/// Trait for delegating signing to an external service (e.g., HSM, TPM, cloud KMS). -/// -/// Implementations must be thread-safe and cloneable. Each DTLS handshake may -/// clone the signer, so `clone_box` must return a fresh instance that signs -/// with the same key. -pub trait CustomSigner: Send + Sync + std::fmt::Debug { - /// Sign the given message and return the raw signature bytes. - fn sign(&self, message: &[u8]) -> std::result::Result, String>; - - /// Clone this signer into a new boxed instance. - fn clone_box(&self) -> Box; -} - -/// Either ED25519, ECDSA, RSA keypair, or a custom external signer. -#[derive(Debug)] -pub enum CryptoPrivateKeyKind { - Ed25519(Ed25519KeyPair), - Ecdsa256(EcdsaKeyPair), - Rsa256(ring::rsa::KeyPair), - /// Delegate signing to an external provider. The signer receives the raw - /// message bytes and must return a signature in the format expected by the - /// negotiated signature algorithm (e.g., ASN.1 DER for ECDSA). - Custom(Box), -} - -/// Private key. -#[derive(Debug)] +/// Provider-neutral DTLS signing key. +#[derive(Clone)] pub struct CryptoPrivateKey { - /// Keypair. - pub kind: CryptoPrivateKeyKind, - /// DER-encoded keypair. - pub serialized_der: Vec, + /// Provider-owned signing key. It may be non-exportable. + pub signing_key: Arc, } impl PartialEq for CryptoPrivateKey { fn eq(&self, other: &Self) -> bool { - if self.serialized_der != other.serialized_der { - return false; - } - - matches!( - (&self.kind, &other.kind), - ( - CryptoPrivateKeyKind::Rsa256(_), - CryptoPrivateKeyKind::Rsa256(_) - ) | ( - CryptoPrivateKeyKind::Ecdsa256(_), - CryptoPrivateKeyKind::Ecdsa256(_) - ) | ( - CryptoPrivateKeyKind::Ed25519(_), - CryptoPrivateKeyKind::Ed25519(_) - ) | ( - CryptoPrivateKeyKind::Custom(_), - CryptoPrivateKeyKind::Custom(_) - ) - ) - } -} - -impl Clone for CryptoPrivateKey { - fn clone(&self) -> Self { - match self.kind { - CryptoPrivateKeyKind::Ed25519(_) => CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ed25519( - Ed25519KeyPair::from_pkcs8_maybe_unchecked(&self.serialized_der).unwrap(), - ), - serialized_der: self.serialized_der.clone(), - }, - CryptoPrivateKeyKind::Ecdsa256(_) => CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ecdsa256( - EcdsaKeyPair::from_pkcs8( - &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, - &self.serialized_der, - #[cfg(feature = "ring")] - &SystemRandom::new(), - ) - .unwrap(), - ), - serialized_der: self.serialized_der.clone(), - }, - CryptoPrivateKeyKind::Rsa256(_) => CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Rsa256( - ring::rsa::KeyPair::from_pkcs8(&self.serialized_der).unwrap(), - ), - serialized_der: self.serialized_der.clone(), - }, - CryptoPrivateKeyKind::Custom(ref signer) => CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Custom(signer.clone_box()), - serialized_der: self.serialized_der.clone(), - }, - } + let left = self.signing_key.public_key(); + let right = other.signing_key.public_key(); + left.encoding == right.encoding && left.bytes == right.bytes } } -impl TryFrom<&KeyPair> for CryptoPrivateKey { - type Error = Error; - - fn try_from(key_pair: &KeyPair) -> Result { - Self::from_key_pair(key_pair) +impl std::fmt::Debug for CryptoPrivateKey { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let public_key = self.signing_key.public_key(); + formatter + .debug_struct("CryptoPrivateKey") + .field("public_key_encoding", &public_key.encoding) + .field("public_key_len", &public_key.bytes.len()) + .finish() } } impl CryptoPrivateKey { - pub fn from_key_pair(key_pair: &KeyPair) -> Result { + /// Imports an rcgen key pair into an explicit provider. + #[cfg(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] + pub fn from_key_pair(key_pair: &KeyPair, crypto: &dyn RTCCrypto) -> Result { let serialized_der = key_pair.serialize_der(); - if key_pair.is_compatible(&rcgen::PKCS_ED25519) { - Ok(CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ed25519( - Ed25519KeyPair::from_pkcs8_maybe_unchecked(&serialized_der) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - }) + let scheme = if key_pair.is_compatible(&rcgen::PKCS_ED25519) { + CryptoSignatureScheme::Ed25519 } else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) { - Ok(CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ecdsa256( - EcdsaKeyPair::from_pkcs8( - &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, - &serialized_der, - #[cfg(feature = "ring")] - &SystemRandom::new(), - ) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - }) + CryptoSignatureScheme::EcdsaP256Sha256 } else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) { - Ok(CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Rsa256( - ring::rsa::KeyPair::from_pkcs8(&serialized_der) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - }) + CryptoSignatureScheme::RsaPkcs1Sha256 } else { - Err(Error::Other("Unsupported key_pair".to_owned())) - } + return Err(Error::Other("Unsupported key_pair".to_owned())); + }; + let signing_key = crypto + .import_signing_key(scheme, &serialized_der) + .map_err(crypto_error)?; + Ok(Self { signing_key }) + } + + /// Wraps a provider-neutral, potentially non-exportable signing key. + pub fn from_signing_key(signing_key: Arc) -> Self { + Self { signing_key } } } @@ -288,45 +259,24 @@ pub(crate) fn generate_key_signature( server_random: &[u8], public_key: &[u8], named_curve: NamedCurve, - private_key: &CryptoPrivateKey, /*, hash_algorithm: HashAlgorithm*/ + algorithm: &SignatureHashAlgorithm, + private_key: &CryptoPrivateKey, ) -> Result> { let msg = value_key_message(client_random, server_random, public_key, named_curve); - let signature = match &private_key.kind { - CryptoPrivateKeyKind::Ed25519(kp) => kp.sign(&msg).as_ref().to_vec(), - CryptoPrivateKeyKind::Ecdsa256(kp) => { - let system_random = SystemRandom::new(); - kp.sign(&system_random, &msg) - .map_err(|e| Error::Other(e.to_string()))? - .as_ref() - .to_vec() - } - CryptoPrivateKeyKind::Rsa256(kp) => { - let system_random = SystemRandom::new(); - #[cfg(feature = "ring")] - let mut signature = vec![0; kp.public().modulus_len()]; - #[cfg(feature = "aws-lc-rs")] - let mut signature = vec![0; kp.public_modulus_len()]; - kp.sign( - &ring::signature::RSA_PKCS1_SHA256, - &system_random, - &msg, - &mut signature, - ) - .map_err(|e| Error::Other(e.to_string()))?; - - signature - } - CryptoPrivateKeyKind::Custom(signer) => signer.sign(&msg).map_err(Error::Other)?, - }; - - Ok(signature) + private_key + .signing_key + .sign(algorithm.crypto_scheme()?, &msg) + .map_err(crypto_error) } // add OID_ED25519 which is not defined in x509_parser +/// The X.509 algorithm OID for Ed25519. pub const OID_ED25519: Oid<'static> = oid!(1.3.101.112); +/// The X.509 algorithm OID for ECDSA with a named curve. pub const OID_ECDSA: Oid<'static> = oid!(1.2.840.10045.2.1); fn verify_signature( + crypto: &dyn crypto::RTCCrypto, message: &[u8], hash_algorithm: &SignatureHashAlgorithm, remote_key_signature: &[u8], @@ -340,56 +290,37 @@ fn verify_signature( let (_, certificate) = x509_parser::parse_x509_certificate(&raw_certificates[0]) .map_err(|e| Error::Other(e.to_string()))?; - let verify_alg: &dyn ring::signature::VerificationAlgorithm = match hash_algorithm.signature { - SignatureAlgorithm::Ed25519 => &ring::signature::ED25519, - SignatureAlgorithm::Ecdsa if hash_algorithm.hash == HashAlgorithm::Sha256 => { - &ring::signature::ECDSA_P256_SHA256_ASN1 - } - SignatureAlgorithm::Ecdsa if hash_algorithm.hash == HashAlgorithm::Sha384 => { - &ring::signature::ECDSA_P384_SHA384_ASN1 - } - SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha1 => { - &ring::signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY - } - SignatureAlgorithm::Rsa if (hash_algorithm.hash == HashAlgorithm::Sha256) => { - if remote_key_signature.len() < 256 && insecure_verification { - &ring::signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY - } else { - &ring::signature::RSA_PKCS1_2048_8192_SHA256 - } - } - SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha384 => { - &ring::signature::RSA_PKCS1_2048_8192_SHA384 - } - SignatureAlgorithm::Rsa if hash_algorithm.hash == HashAlgorithm::Sha512 => { - if remote_key_signature.len() < 256 && insecure_verification { - &ring::signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY - } else { - &ring::signature::RSA_PKCS1_2048_8192_SHA512 - } - } - _ => return Err(Error::ErrKeySignatureVerifyUnimplemented), + let encoding = match hash_algorithm.signature { + SignatureAlgorithm::Ed25519 => PublicKeyEncoding::Ed25519Raw, + SignatureAlgorithm::Ecdsa => PublicKeyEncoding::EcUncompressedPoint, + SignatureAlgorithm::Rsa => PublicKeyEncoding::RsaPkcs1Der, + SignatureAlgorithm::Unsupported => return Err(Error::ErrKeySignatureVerifyUnimplemented), }; - - log::trace!("Picked an algorithm {verify_alg:?}"); - - let public_key = ring::signature::UnparsedPublicKey::new( - verify_alg, - certificate - .tbs_certificate - .subject_pki - .subject_public_key - .data, - ); - - public_key - .verify(message, remote_key_signature) - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(()) + if hash_algorithm.signature == SignatureAlgorithm::Rsa + && remote_key_signature.len() < 256 + && !insecure_verification + { + return Err(Error::ErrKeySignatureMismatch); + } + crypto + .verify_signature( + hash_algorithm.crypto_scheme()?, + PublicKey { + encoding, + bytes: &certificate + .tbs_certificate + .subject_pki + .subject_public_key + .data, + }, + message, + remote_key_signature, + ) + .map_err(signature_verification_error) } pub(crate) fn verify_key_signature( + crypto: &dyn crypto::RTCCrypto, message: &[u8], hash_algorithm: &SignatureHashAlgorithm, remote_key_signature: &[u8], @@ -397,6 +328,7 @@ pub(crate) fn verify_key_signature( insecure_verification: bool, ) -> Result<()> { verify_signature( + crypto, message, hash_algorithm, remote_key_signature, @@ -415,42 +347,17 @@ pub(crate) fn verify_key_signature( // https://tools.ietf.org/html/rfc5246#section-7.3 pub(crate) fn generate_certificate_verify( handshake_bodies: &[u8], - private_key: &CryptoPrivateKey, /*, hashAlgorithm hashAlgorithm*/ + algorithm: &SignatureHashAlgorithm, + private_key: &CryptoPrivateKey, ) -> Result> { - let signature = match &private_key.kind { - CryptoPrivateKeyKind::Ed25519(kp) => kp.sign(handshake_bodies).as_ref().to_vec(), - CryptoPrivateKeyKind::Ecdsa256(kp) => { - let system_random = SystemRandom::new(); - kp.sign(&system_random, handshake_bodies) - .map_err(|e| Error::Other(e.to_string()))? - .as_ref() - .to_vec() - } - CryptoPrivateKeyKind::Rsa256(kp) => { - let system_random = SystemRandom::new(); - #[cfg(feature = "ring")] - let mut signature = vec![0; kp.public().modulus_len()]; - #[cfg(feature = "aws-lc-rs")] - let mut signature = vec![0; kp.public_modulus_len()]; - kp.sign( - &ring::signature::RSA_PKCS1_SHA256, - &system_random, - handshake_bodies, - &mut signature, - ) - .map_err(|e| Error::Other(e.to_string()))?; - - signature - } - CryptoPrivateKeyKind::Custom(signer) => { - signer.sign(handshake_bodies).map_err(Error::Other)? - } - }; - - Ok(signature) + private_key + .signing_key + .sign(algorithm.crypto_scheme()?, handshake_bodies) + .map_err(crypto_error) } pub(crate) fn verify_certificate_verify( + crypto: &dyn crypto::RTCCrypto, handshake_bodies: &[u8], hash_algorithm: &SignatureHashAlgorithm, remote_key_signature: &[u8], @@ -458,6 +365,7 @@ pub(crate) fn verify_certificate_verify( insecure_verification: bool, ) -> Result<()> { verify_signature( + crypto, handshake_bodies, hash_algorithm, remote_key_signature, @@ -546,16 +454,16 @@ pub(crate) fn generate_aead_additional_data(h: &RecordLayerHeader, payload_len: #[cfg(test)] mod test { - #[cfg(feature = "pem")] use super::*; - #[cfg(feature = "pem")] #[test] fn test_certificate_serialize_pem_and_from_pem() -> Result<()> { - let cert = Certificate::generate_self_signed(vec!["webrtc.rs".to_owned()])?; + let provider = crypto::default_provider().map_err(crypto_error)?; + let cert = + Certificate::generate_self_signed(vec!["webrtc.rs".to_owned()], provider.crypto())?; - let pem = cert.serialize_pem(); - let loaded_cert = Certificate::from_pem(&pem)?; + let pem = cert.serialize_pem()?; + let loaded_cert = Certificate::from_pem(&pem, provider.crypto())?; assert_eq!(loaded_cert, cert); diff --git a/rtc-dtls/src/crypto/padding.rs b/rtc-dtls/src/crypto/padding.rs deleted file mode 100644 index 05e12dbc..00000000 --- a/rtc-dtls/src/crypto/padding.rs +++ /dev/null @@ -1,122 +0,0 @@ -use cbc::cipher::block_padding::{PadType, RawPadding, UnpadError}; -use core::panic; - -pub enum DtlsPadding {} -/// Reference: RFC5246, 6.2.3.2 -impl RawPadding for DtlsPadding { - const TYPE: PadType = PadType::Reversible; - - fn raw_pad(block: &mut [u8], pos: usize) { - if pos >= block.len() { - panic!("`pos` is bigger or equal to block size"); - } - - let padding_length = block.len() - pos - 1; - if padding_length > 255 { - panic!("block size is too big for DTLS"); - } - - set(&mut block[pos..], padding_length as u8); - } - - fn raw_unpad(data: &[u8]) -> Result<&[u8], UnpadError> { - let padding_length = data.last().copied().unwrap_or(1) as usize; - if padding_length + 1 > data.len() { - return Err(UnpadError); - } - - let padding_begin = data.len() - padding_length - 1; - - if data[padding_begin..data.len() - 1] - .iter() - .any(|&byte| byte as usize != padding_length) - { - return Err(UnpadError); - } - - Ok(&data[0..padding_begin]) - } -} - -/// Sets all bytes in `dst` equal to `value` -#[inline(always)] -fn set(dst: &mut [u8], value: u8) { - // SAFETY: we overwrite valid memory behind `dst` - // note: loop is not used here because it produces - // unnecessary branch which tests for zero-length slices - unsafe { - core::ptr::write_bytes(dst.as_mut_ptr(), value, dst.len()); - } -} - -#[cfg(test)] -pub mod tests { - use rand::RngExt; - - use super::*; - - #[test] - fn padding_length_is_amount_of_bytes_excluding_the_padding_length_itself() -> Result<(), ()> { - for original_length in 0..128 { - for padding_length in 0..(256 - original_length) { - let mut block = vec![0; original_length + padding_length + 1]; - rand::rng().fill(&mut block[0..original_length]); - let original = block[0..original_length].to_vec(); - DtlsPadding::raw_pad(&mut block, original_length); - - for byte in block[original_length..].iter() { - assert_eq!(*byte as usize, padding_length); - } - assert_eq!(block[0..original_length], original); - } - } - - Ok(()) - } - - #[test] - #[should_panic] - fn full_block_is_padding_error() { - for original_length in 0..256 { - let mut block = vec![0; original_length]; - DtlsPadding::raw_pad(&mut block, original_length); - } - } - - #[test] - #[should_panic] - fn padding_length_bigger_than_255_is_a_pad_error() { - let padding_length = 256; - for original_length in 0..128 { - let mut block = vec![0; original_length + padding_length + 1]; - DtlsPadding::raw_pad(&mut block, original_length); - } - } - - #[test] - fn empty_block_is_unpadding_error() { - let r = DtlsPadding::raw_unpad(&[]); - assert!(r.is_err()); - } - - #[test] - fn padding_too_big_for_block_is_unpadding_error() { - let r = DtlsPadding::raw_unpad(&[1]); - assert!(r.is_err()); - } - - #[test] - fn one_of_the_padding_bytes_with_value_different_than_padding_length_is_unpadding_error() { - for padding_length in 0..16 { - for invalid_byte in 0..padding_length { - let mut block = vec![0; padding_length + 1]; - DtlsPadding::raw_pad(&mut block, 0); - - assert_eq!(DtlsPadding::raw_unpad(&block).ok(), Some(&[][..])); - block[invalid_byte] = (padding_length - 1) as u8; - let r = DtlsPadding::raw_unpad(&block); - assert!(r.is_err()); - } - } - } -} diff --git a/rtc-dtls/src/curve/mod.rs b/rtc-dtls/src/curve/mod.rs index dd48d5ff..5dafb80a 100644 --- a/rtc-dtls/src/curve/mod.rs +++ b/rtc-dtls/src/curve/mod.rs @@ -1,9 +1,14 @@ +/// The named elliptic curves and key generation over them. pub mod named_curve; // https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-10 #[derive(Copy, Clone, PartialEq, Eq, Debug)] +/// How an elliptic curve is identified in a key exchange — by name, or explicitly. +#[non_exhaustive] pub enum EllipticCurveType { + /// `NAMED_CURVE` (`0x03`). NamedCurve = 0x03, + /// A curve type this crate does not implement. Unsupported, } diff --git a/rtc-dtls/src/curve/named_curve.rs b/rtc-dtls/src/curve/named_curve.rs index 1e2fe0b3..6843bedc 100644 --- a/rtc-dtls/src/curve/named_curve.rs +++ b/rtc-dtls/src/curve/named_curve.rs @@ -1,14 +1,19 @@ -use rand_core::OsRng; // requires 'getrandom' feature - +use crypto::{ActiveKeyExchange, KeyExchangeAlgorithm, RTCCrypto}; use shared::error::*; // https://www.iana.org/assignments/tls-parameters/tls-parameters.xml#tls-parameters-8 #[repr(u16)] #[derive(Copy, Clone, PartialEq, Eq, Debug)] +/// The named elliptic curves this crate can perform ECDHE over. +#[non_exhaustive] pub enum NamedCurve { + /// `UNSUPPORTED` (`0x0000`). Unsupported = 0x0000, + /// `P256` (`0x0017`). P256 = 0x0017, + /// `P384` (`0x0018`). P384 = 0x0018, + /// `X25519` (`0x001d`). X25519 = 0x001d, } @@ -23,61 +28,48 @@ impl From for NamedCurve { } } -pub(crate) enum NamedCurvePrivateKey { - EphemeralSecretP256(p256::ecdh::EphemeralSecret), - EphemeralSecretP384(p384::ecdh::EphemeralSecret), - StaticSecretX25519(x25519_dalek::StaticSecret), -} - +/// An ephemeral ECDHE key pair, with the curve it belongs to. pub struct NamedCurveKeypair { pub(crate) curve: NamedCurve, pub(crate) public_key: Vec, - pub(crate) private_key: NamedCurvePrivateKey, + active: Option>, } -fn elliptic_curve_keypair(curve: NamedCurve) -> Result { - let (public_key, private_key) = match curve { - NamedCurve::P256 => { - let secret_key = p256::ecdh::EphemeralSecret::random(&mut OsRng); - let public_key = p256::EncodedPoint::from(secret_key.public_key()); - ( - public_key.as_bytes().to_vec(), - NamedCurvePrivateKey::EphemeralSecretP256(secret_key), - ) - } - NamedCurve::P384 => { - let secret_key = p384::ecdh::EphemeralSecret::random(&mut OsRng); - let public_key = p384::EncodedPoint::from(secret_key.public_key()); - ( - public_key.as_bytes().to_vec(), - NamedCurvePrivateKey::EphemeralSecretP384(secret_key), - ) - } - NamedCurve::X25519 => { - let secret_key = x25519_dalek::StaticSecret::random_from_rng(OsRng); - let public_key = x25519_dalek::PublicKey::from(&secret_key); - ( - public_key.as_bytes().to_vec(), - NamedCurvePrivateKey::StaticSecretX25519(secret_key), - ) - } - _ => return Err(Error::ErrInvalidNamedCurve), - }; - - Ok(NamedCurveKeypair { - curve, - public_key, - private_key, - }) +impl NamedCurveKeypair { + pub(crate) fn complete(&mut self, peer_public_key: &[u8]) -> Result> { + let active = self + .active + .take() + .ok_or(Error::ErrNamedCurveAndPrivateKeyMismatch)?; + active + .complete(peer_public_key) + .map(|secret| secret.into_bytes()) + .map_err(|error| Error::Crypto(error.to_string())) + } } impl NamedCurve { - pub fn generate_keypair(&self) -> Result { - match *self { - NamedCurve::X25519 => elliptic_curve_keypair(NamedCurve::X25519), - NamedCurve::P256 => elliptic_curve_keypair(NamedCurve::P256), - NamedCurve::P384 => elliptic_curve_keypair(NamedCurve::P384), - _ => Err(Error::ErrInvalidNamedCurve), + pub(crate) const fn crypto_algorithm(self) -> Result { + match self { + Self::P256 => Ok(KeyExchangeAlgorithm::P256), + Self::P384 => Ok(KeyExchangeAlgorithm::P384), + Self::X25519 => Ok(KeyExchangeAlgorithm::X25519), + Self::Unsupported => Err(Error::ErrInvalidNamedCurve), } } + + pub(crate) fn generate_keypair_with_crypto( + self, + crypto: &dyn RTCCrypto, + ) -> Result { + let active = crypto + .start_key_exchange(self.crypto_algorithm()?) + .map_err(|error| Error::Crypto(error.to_string()))?; + let public_key = active.public_key().to_vec(); + Ok(NamedCurveKeypair { + curve: self, + public_key, + active: Some(active), + }) + } } diff --git a/rtc-dtls/src/endpoint.rs b/rtc-dtls/src/endpoint.rs index e7c82d5a..fb609861 100644 --- a/rtc-dtls/src/endpoint.rs +++ b/rtc-dtls/src/endpoint.rs @@ -1,3 +1,12 @@ +//! The Sans-I/O DTLS endpoint. +//! +//! An [`Endpoint`](crate::endpoint::Endpoint) multiplexes several DTLS associations by remote address. Feed it inbound +//! datagrams, poll it for the datagrams to send and for [`EndpointEvent`](crate::endpoint::EndpointEvent)s, and drive its timers +//! with `handle_timeout`/`poll_timeout`. It owns no sockets and reads no clock. +//! +//! [`EndpointEvent::HandshakeComplete`](crate::endpoint::EndpointEvent::HandshakeComplete) is the signal an application waits for: from that point +//! application data can be written, and the SRTP keying material can be exported from the +//! completed handshake. use crate::conn::DTLSConn; use shared::error::{Error, Result}; use shared::{EcnCodepoint, TransportContext}; @@ -12,8 +21,12 @@ use std::net::SocketAddr; use std::sync::Arc; use std::time::Instant; +/// What the endpoint reports to its caller. +#[non_exhaustive] pub enum EndpointEvent { + /// The handshake finished; application data may now be sent, and SRTP keys can be exported. HandshakeComplete, + /// Decrypted application data arrived. ApplicationData(BytesMut), } @@ -205,6 +218,11 @@ impl Endpoint { Ok(messages) } + /// Queues application data for `remote`. + /// + /// # Errors + /// + /// Fails if there is no association with `remote`, or its handshake has not completed. pub fn write(&mut self, remote: SocketAddr, data: &[u8]) -> Result<()> { if let Some(conn) = self.connections.get_mut(&remote) { conn.write(data)?; @@ -226,6 +244,11 @@ impl Endpoint { } } + /// Advances `remote`'s association to `now`, driving handshake retransmissions. + /// + /// # Errors + /// + /// Fails if the handshake has exhausted its retransmissions. pub fn handle_timeout(&mut self, remote: SocketAddr, now: Instant) -> Result<()> { if let Some(conn) = self.connections.get_mut(&remote) { if let Some(current_retransmit_timer) = &conn.current_retransmit_timer @@ -254,16 +277,214 @@ impl Endpoint { } } - pub fn poll_timeout(&self, remote: SocketAddr, eto: &mut Instant) -> Result<()> { - if let Some(conn) = self.connections.get(&remote) { - if let Some(current_retransmit_timer) = &conn.current_retransmit_timer - && *current_retransmit_timer < *eto - { - *eto = *current_retransmit_timer; - } - Ok(()) + /// When `remote`'s association next needs [`Self::handle_timeout`]. + pub fn poll_timeout(&self, remote: &SocketAddr) -> Option { + if let Some(conn) = self.connections.get(remote) { + conn.current_retransmit_timer } else { - Err(Error::InvalidRemoteAddress(remote)) + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cipher_suite::CipherSuiteId; + use crate::config::ConfigBuilder; + use crate::crypto::Certificate; + use crypto::{CryptoError, RTCCrypto, RTCCryptoProvider, RTCRandom}; + + fn client_addr() -> SocketAddr { + SocketAddr::from(([127, 0, 0, 1], 4444)) + } + + fn server_addr() -> SocketAddr { + SocketAddr::from(([127, 0, 0, 1], 4445)) + } + + struct FailingRandom; + + impl RTCRandom for FailingRandom { + fn fill(&self, _output: &mut [u8]) -> std::result::Result<(), CryptoError> { + Err(CryptoError::RandomnessFailed) + } + } + + struct FailingRandomProvider { + provider: Arc, + } + + impl RTCCryptoProvider for FailingRandomProvider { + fn name(&self) -> &'static str { + "failing-random" + } + + fn crypto(&self) -> &dyn RTCCrypto { + self.provider.crypto() + } + + fn random(&self) -> &dyn RTCRandom { + &FailingRandom } } + + fn config( + provider: Arc, + is_client: bool, + suite: CipherSuiteId, + ) -> Result> { + let mut builder = ConfigBuilder::default() + .with_crypto_provider(provider.clone()) + .with_cipher_suites(vec![suite]) + .with_insecure_skip_verify(true); + let is_psk = matches!( + suite, + CipherSuiteId::Tls_Psk_With_Aes_128_Ccm + | CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8 + | CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256 + ); + if is_psk { + builder = builder.with_psk(Some(Arc::new(|_| Ok(vec![0xab, 0xcd, 0xef])))); + if is_client { + builder = builder.with_psk_identity_hint(Some(b"rtc-dtls-test".to_vec())); + } + } else if !is_client { + builder = builder.with_certificates(vec![Certificate::generate_self_signed( + vec!["localhost".to_owned()], + provider.crypto(), + )?]); + } + Ok(Arc::new(builder.build(is_client, None)?)) + } + + fn transfer( + source: &mut Endpoint, + destination: &mut Endpoint, + source_addr: SocketAddr, + ) -> Result> { + let mut events = Vec::new(); + while let Some(transmit) = source.poll_transmit() { + events.extend(destination.read( + Instant::now(), + source_addr, + transmit.transport.ecn, + transmit.message, + )?); + } + Ok(events) + } + + fn handshake_and_exchange( + client_provider: Arc, + server_provider: Arc, + suite: CipherSuiteId, + ) -> Result<()> { + let client_config = config(client_provider, true, suite)?; + let server_config = config(server_provider, false, suite)?; + let mut client = Endpoint::new(client_addr(), TransportProtocol::UDP, None); + let mut server = Endpoint::new(server_addr(), TransportProtocol::UDP, Some(server_config)); + client.connect(server_addr(), client_config, None)?; + + let mut client_complete = false; + let mut server_complete = false; + for _ in 0..32 { + for event in transfer(&mut client, &mut server, client_addr())? { + server_complete |= matches!(event, EndpointEvent::HandshakeComplete); + } + for event in transfer(&mut server, &mut client, server_addr())? { + client_complete |= matches!(event, EndpointEvent::HandshakeComplete); + } + if client_complete && server_complete { + break; + } + } + assert!( + client_complete && server_complete, + "DTLS handshake did not complete" + ); + + client.write(server_addr(), b"provider-backed DTLS")?; + let transmit = client + .poll_transmit() + .expect("application write produces a DTLS record"); + let replay = transmit.message.clone(); + let events = server.read( + Instant::now(), + client_addr(), + transmit.transport.ecn, + transmit.message, + )?; + assert!(events.into_iter().any(|event| matches!( + event, + EndpointEvent::ApplicationData(data) if data.as_ref() == b"provider-backed DTLS" + ))); + assert!( + server + .read(Instant::now(), client_addr(), None, replay)? + .is_empty() + ); + Ok(()) + } + + #[cfg(feature = "crypto-ring")] + #[test] + fn ring_provider_completes_handshake_and_record_exchange() -> Result<()> { + let provider: Arc = Arc::new(crypto::providers::RingProvider::new()); + handshake_and_exchange( + provider.clone(), + provider, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + ) + } + + #[cfg(feature = "crypto-aws-lc-rs")] + #[test] + fn aws_lc_rs_provider_completes_handshake_and_record_exchange() -> Result<()> { + let provider: Arc = + Arc::new(crypto::providers::AwsLcRsProvider::new()); + handshake_and_exchange( + provider.clone(), + provider, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + ) + } + + #[cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] + #[test] + fn ring_and_aws_lc_rs_complete_cross_provider_handshakes() -> Result<()> { + let ring: Arc = Arc::new(crypto::providers::RingProvider::new()); + let aws: Arc = Arc::new(crypto::providers::AwsLcRsProvider::new()); + for suite in [ + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Ccm_8, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256, + CipherSuiteId::Tls_Psk_With_Aes_128_Gcm_Sha256, + CipherSuiteId::Tls_Psk_With_Aes_128_Ccm, + CipherSuiteId::Tls_Psk_With_Aes_128_Ccm_8, + ] { + handshake_and_exchange(ring.clone(), aws.clone(), suite)?; + handshake_and_exchange(aws.clone(), ring.clone(), suite)?; + } + Ok(()) + } + + #[test] + fn failing_random_provider_aborts_client_hello_cleanly() -> Result<()> { + let base = crypto::default_provider().map_err(|error| Error::Crypto(error.to_string()))?; + let provider: Arc = + Arc::new(FailingRandomProvider { provider: base }); + let config = config( + provider, + true, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + )?; + let mut endpoint = Endpoint::new(client_addr(), TransportProtocol::UDP, None); + + let result = endpoint.connect(server_addr(), config, None); + assert!(matches!(result, Err(Error::Crypto(_)))); + Ok(()) + } } diff --git a/rtc-dtls/src/extension/extension_server_name.rs b/rtc-dtls/src/extension/extension_server_name.rs index c54a6506..55f9277e 100644 --- a/rtc-dtls/src/extension/extension_server_name.rs +++ b/rtc-dtls/src/extension/extension_server_name.rs @@ -9,20 +9,28 @@ use std::io::{Read, Write}; const EXTENSION_SERVER_NAME_TYPE_DNSHOST_NAME: u8 = 0; #[derive(Clone, Debug, PartialEq, Eq)] +/// The Server Name Indication extension, naming the host the client meant to reach. pub struct ExtensionServerName { pub(crate) server_name: String, } impl ExtensionServerName { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::ServerName } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { //TODO: check how to do cryptobyte? 2 + 2 + 1 + 2 + self.server_name.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { //TODO: check how to do cryptobyte? writer.write_u16::(2 + 1 + 2 + self.server_name.len() as u16)?; @@ -34,6 +42,11 @@ impl ExtensionServerName { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { //TODO: check how to do cryptobyte? let _ = reader.read_u16::()? as usize; diff --git a/rtc-dtls/src/extension/extension_supported_elliptic_curves.rs b/rtc-dtls/src/extension/extension_supported_elliptic_curves.rs index 64fe8084..b7c4d583 100644 --- a/rtc-dtls/src/extension/extension_supported_elliptic_curves.rs +++ b/rtc-dtls/src/extension/extension_supported_elliptic_curves.rs @@ -13,18 +13,26 @@ const EXTENSION_SUPPORTED_GROUPS_HEADER_SIZE: usize = 6; /// [RFC 8422 §5.1.1]: https://tools.ietf.org/html/rfc8422#section-5.1.1 #[derive(Clone, Debug, PartialEq, Eq)] pub struct ExtensionSupportedEllipticCurves { + /// The curves the sender accepts, in preference order. pub elliptic_curves: Vec, } impl ExtensionSupportedEllipticCurves { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::SupportedEllipticCurves } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 2 + 2 + self.elliptic_curves.len() * 2 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u16::(2 + 2 * self.elliptic_curves.len() as u16)?; writer.write_u16::(2 * self.elliptic_curves.len() as u16)?; @@ -35,6 +43,11 @@ impl ExtensionSupportedEllipticCurves { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let _ = reader.read_u16::()?; diff --git a/rtc-dtls/src/extension/extension_supported_point_formats.rs b/rtc-dtls/src/extension/extension_supported_point_formats.rs index 60e5df94..7a8dde6d 100644 --- a/rtc-dtls/src/extension/extension_supported_point_formats.rs +++ b/rtc-dtls/src/extension/extension_supported_point_formats.rs @@ -5,8 +5,10 @@ use super::*; const EXTENSION_SUPPORTED_POINT_FORMATS_SIZE: usize = 5; +/// An EC point format code point. pub type EllipticCurvePointFormat = u8; +/// Uncompressed point format, the only one WebRTC uses. pub const ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED: EllipticCurvePointFormat = 0; /// ## Specifications @@ -20,14 +22,21 @@ pub struct ExtensionSupportedPointFormats { } impl ExtensionSupportedPointFormats { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::SupportedPointFormats } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 2 + 1 + self.point_formats.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u16::(1 + self.point_formats.len() as u16)?; writer.write_u8(self.point_formats.len() as u8)?; @@ -38,6 +47,11 @@ impl ExtensionSupportedPointFormats { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let _ = reader.read_u16::()?; diff --git a/rtc-dtls/src/extension/extension_supported_signature_algorithms.rs b/rtc-dtls/src/extension/extension_supported_signature_algorithms.rs index 24ca158d..21f1e43d 100644 --- a/rtc-dtls/src/extension/extension_supported_signature_algorithms.rs +++ b/rtc-dtls/src/extension/extension_supported_signature_algorithms.rs @@ -17,14 +17,21 @@ pub struct ExtensionSupportedSignatureAlgorithms { } impl ExtensionSupportedSignatureAlgorithms { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::SupportedSignatureAlgorithms } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 2 + 2 + self.signature_hash_algorithms.len() * 2 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u16::(2 + 2 * self.signature_hash_algorithms.len() as u16)?; writer.write_u16::(2 * self.signature_hash_algorithms.len() as u16)?; @@ -36,6 +43,11 @@ impl ExtensionSupportedSignatureAlgorithms { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let _ = reader.read_u16::()?; diff --git a/rtc-dtls/src/extension/extension_use_extended_master_secret.rs b/rtc-dtls/src/extension/extension_use_extended_master_secret.rs index d6a1d0a4..d9c8298d 100644 --- a/rtc-dtls/src/extension/extension_use_extended_master_secret.rs +++ b/rtc-dtls/src/extension/extension_use_extended_master_secret.rs @@ -16,14 +16,21 @@ pub struct ExtensionUseExtendedMasterSecret { } impl ExtensionUseExtendedMasterSecret { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::UseExtendedMasterSecret } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 2 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { // length writer.write_u16::(0)?; @@ -31,6 +38,11 @@ impl ExtensionUseExtendedMasterSecret { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let _ = reader.read_u16::()?; diff --git a/rtc-dtls/src/extension/extension_use_srtp.rs b/rtc-dtls/src/extension/extension_use_srtp.rs index 5b2dd8b0..eeb5fa88 100644 --- a/rtc-dtls/src/extension/extension_use_srtp.rs +++ b/rtc-dtls/src/extension/extension_use_srtp.rs @@ -11,11 +11,17 @@ use super::*; /// [RFC 5764 §4.1.2]: https://tools.ietf.org/html/rfc5764#section-4.1.2 #[allow(non_camel_case_types)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum SrtpProtectionProfile { + /// `SRTP_AES128_CM_HMAC_SHA1_80` (`0x0001`). Srtp_Aes128_Cm_Hmac_Sha1_80 = 0x0001, + /// `SRTP_AES128_CM_HMAC_SHA1_32` (`0x0002`). Srtp_Aes128_Cm_Hmac_Sha1_32 = 0x0002, + /// `SRTP_AEAD_AES_128_GCM` (`0x0007`). Srtp_Aead_Aes_128_Gcm = 0x0007, + /// `SRTP_AEAD_AES_256_GCM` (`0x0008`). Srtp_Aead_Aes_256_Gcm = 0x0008, + /// A protection profile this crate does not implement. Unsupported, } @@ -45,14 +51,21 @@ pub struct ExtensionUseSrtp { } impl ExtensionUseSrtp { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::UseSrtp } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 2 + 2 + self.protection_profiles.len() * 2 + 1 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u16::( 2 + /* MKI Length */ 1 + 2 * self.protection_profiles.len() as u16, @@ -68,6 +81,11 @@ impl ExtensionUseSrtp { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let _ = reader.read_u16::()?; diff --git a/rtc-dtls/src/extension/mod.rs b/rtc-dtls/src/extension/mod.rs index 8dc73b0f..6d5fe678 100644 --- a/rtc-dtls/src/extension/mod.rs +++ b/rtc-dtls/src/extension/mod.rs @@ -1,9 +1,18 @@ +/// Server Name Indication (SNI). pub mod extension_server_name; +/// The curves a client will accept for ECDHE. pub mod extension_supported_elliptic_curves; +/// The EC point formats a client will accept; WebRTC uses uncompressed. pub mod extension_supported_point_formats; +/// The signature and hash algorithm pairs a client will accept. pub mod extension_supported_signature_algorithms; +/// The extended master secret extension ([RFC 7627]), which binds the master secret to the +/// whole handshake. pub mod extension_use_extended_master_secret; +/// The `use_srtp` extension, which negotiates SRTP protection profiles during the DTLS +/// handshake ([RFC 5764]). pub mod extension_use_srtp; +/// The renegotiation info extension, sent empty to signal renegotiation is not supported. pub mod renegotiation_info; use extension_server_name::*; @@ -21,14 +30,24 @@ use std::io::{Read, Write}; // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml #[derive(Clone, Debug, PartialEq, Eq)] +/// The extension type code points this crate understands. +#[non_exhaustive] pub enum ExtensionValue { + /// `SERVER_NAME` (`0`). ServerName = 0, + /// `SUPPORTED_ELLIPTIC_CURVES` (`10`). SupportedEllipticCurves = 10, + /// `SUPPORTED_POINT_FORMATS` (`11`). SupportedPointFormats = 11, + /// `SUPPORTED_SIGNATURE_ALGORITHMS` (`13`). SupportedSignatureAlgorithms = 13, + /// `USE_SRTP` (`14`). UseSrtp = 14, + /// `USE_EXTENDED_MASTER_SECRET` (`23`). UseExtendedMasterSecret = 23, + /// `RENEGOTIATION_INFO` (`65281`). RenegotiationInfo = 65281, + /// An extension this crate does not implement, which is ignored. Unsupported, } @@ -48,17 +67,27 @@ impl From for ExtensionValue { } #[derive(PartialEq, Eq, Debug, Clone)] +/// A parsed hello extension. +#[non_exhaustive] pub enum Extension { + /// Server Name Indication. ServerName(ExtensionServerName), + /// The curves the sender accepts for ECDHE. SupportedEllipticCurves(ExtensionSupportedEllipticCurves), + /// The EC point formats the sender accepts. SupportedPointFormats(ExtensionSupportedPointFormats), + /// The signature and hash pairs the sender accepts. SupportedSignatureAlgorithms(ExtensionSupportedSignatureAlgorithms), + /// The SRTP protection profiles offered or selected. UseSrtp(ExtensionUseSrtp), + /// The extended master secret extension. UseExtendedMasterSecret(ExtensionUseExtendedMasterSecret), + /// The renegotiation info extension. RenegotiationInfo(ExtensionRenegotiationInfo), } impl Extension { + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { match self { Extension::ServerName(ext) => ext.extension_value(), @@ -71,6 +100,7 @@ impl Extension { } } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { let mut len = 2; @@ -87,6 +117,11 @@ impl Extension { len } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u16::(self.extension_value() as u16)?; match self { @@ -100,6 +135,11 @@ impl Extension { } } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let extension_value: ExtensionValue = reader.read_u16::()?.into(); match extension_value { diff --git a/rtc-dtls/src/extension/renegotiation_info.rs b/rtc-dtls/src/extension/renegotiation_info.rs index 6e3d1c2d..b937a3d9 100644 --- a/rtc-dtls/src/extension/renegotiation_info.rs +++ b/rtc-dtls/src/extension/renegotiation_info.rs @@ -21,10 +21,12 @@ pub struct ExtensionRenegotiationInfo { impl ExtensionRenegotiationInfo { // TypeValue returns the extension TypeValue + /// The extension type this value is carried under. pub fn extension_value(&self) -> ExtensionValue { ExtensionValue::RenegotiationInfo } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 3 } diff --git a/rtc-dtls/src/flight/flight0.rs b/rtc-dtls/src/flight/flight0.rs index 7f8362d3..24c9d7ff 100644 --- a/rtc-dtls/src/flight/flight0.rs +++ b/rtc-dtls/src/flight/flight0.rs @@ -2,7 +2,6 @@ use super::flight2::*; use super::*; use crate::config::*; use crate::conn::*; -use crate::curve::named_curve::NamedCurve; use crate::extension::*; use crate::handshake::*; use crate::record_layer::record_layer_header::*; @@ -10,7 +9,6 @@ use crate::*; use shared::error::Error; use log::debug; -use rand::RngExt; use std::fmt; #[derive(Debug, PartialEq)] @@ -106,7 +104,7 @@ impl Flight for Flight0 { )); } for curve in e.elliptic_curves.iter() { - if curve != &NamedCurve::Unsupported { + if cfg.local_named_curves.contains(curve) { state.named_curve = *curve; break; } @@ -153,7 +151,10 @@ impl Flight for Flight0 { } if state.local_keypair.is_none() { - state.local_keypair = match state.named_curve.generate_keypair() { + state.local_keypair = match state + .named_curve + .generate_keypair_with_crypto(cfg.provider().crypto()) + { Ok(local_keypar) => Some(local_keypar), Err(err) => { return Err(( @@ -183,17 +184,24 @@ impl Flight for Flight0 { &self, state: &mut State, _cache: &HandshakeCache, - _cfg: &HandshakeConfig, + cfg: &HandshakeConfig, ) -> Result, (Option, Option)> { // Initialize state.cookie = vec![0; COOKIE_LENGTH]; - rand::rng().fill(state.cookie.as_mut_slice()); + if let Err(error) = cfg.provider().random().fill(state.cookie.as_mut_slice()) { + return Err((None, Some(Error::Crypto(error.to_string())))); + } state.local_epoch = 0; state.remote_epoch = 0; state.named_curve = DEFAULT_NAMED_CURVE; - state.local_random.populate(); + if cfg.local_psk_callback.is_none() { + state.named_curve = cfg.local_named_curves[0]; + } + if let Err(error) = state.local_random.populate(cfg.provider().random()) { + return Err((None, Some(error))); + } Ok(vec![]) } diff --git a/rtc-dtls/src/flight/flight1.rs b/rtc-dtls/src/flight/flight1.rs index 786c8e62..85333be6 100644 --- a/rtc-dtls/src/flight/flight1.rs +++ b/rtc-dtls/src/flight/flight1.rs @@ -4,7 +4,6 @@ use crate::compression_methods::*; use crate::config::*; use crate::conn::*; use crate::content::*; -use crate::curve::named_curve::*; use crate::extension::extension_server_name::*; use crate::extension::extension_supported_elliptic_curves::*; use crate::extension::extension_supported_point_formats::*; @@ -120,7 +119,9 @@ impl Flight for Flight1 { state.named_curve = DEFAULT_NAMED_CURVE; state.cookie = vec![]; - state.local_random.populate(); + if let Err(error) = state.local_random.populate(cfg.provider().random()) { + return Err((None, Some(error))); + } let mut extensions = vec![ Extension::SupportedSignatureAlgorithms(ExtensionSupportedSignatureAlgorithms { @@ -134,7 +135,7 @@ impl Flight for Flight1 { if cfg.local_psk_callback.is_none() { extensions.extend_from_slice(&[ Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { - elliptic_curves: vec![NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384], + elliptic_curves: cfg.local_named_curves.clone(), }), Extension::SupportedPointFormats(ExtensionSupportedPointFormats { point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], diff --git a/rtc-dtls/src/flight/flight3.rs b/rtc-dtls/src/flight/flight3.rs index 27fc918d..83e89d45 100644 --- a/rtc-dtls/src/flight/flight3.rs +++ b/rtc-dtls/src/flight/flight3.rs @@ -3,7 +3,6 @@ use super::*; use crate::compression_methods::*; use crate::config::*; use crate::content::*; -use crate::curve::named_curve::*; use crate::extension::extension_server_name::*; use crate::extension::extension_supported_elliptic_curves::*; use crate::extension::extension_supported_point_formats::*; @@ -350,7 +349,7 @@ impl Flight for Flight3 { if cfg.local_psk_callback.is_none() { extensions.extend_from_slice(&[ Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { - elliptic_curves: vec![NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384], + elliptic_curves: cfg.local_named_curves.clone(), }), Extension::SupportedPointFormats(ExtensionSupportedPointFormats { point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], @@ -422,7 +421,10 @@ pub(crate) fn handle_server_key_exchange( state.identity_hint.clone_from(&h.identity_hint); state.pre_master_secret = prf_psk_pre_master_secret(&psk); } else { - let local_keypair = match h.named_curve.generate_keypair() { + let mut local_keypair = match h + .named_curve + .generate_keypair_with_crypto(cfg.provider().crypto()) + { Ok(local_keypair) => local_keypair, Err(err) => { return Err(( @@ -435,22 +437,20 @@ pub(crate) fn handle_server_key_exchange( } }; - state.pre_master_secret = match prf_pre_master_secret( - &h.public_key, - &local_keypair.private_key, - local_keypair.curve, - ) { - Ok(pre_master_secret) => pre_master_secret, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::InternalError, - }), - Some(err), - )); - } - }; + let curve = local_keypair.curve; + state.pre_master_secret = + match prf_pre_master_secret(&h.public_key, &mut local_keypair, curve) { + Ok(pre_master_secret) => pre_master_secret, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::InternalError, + }), + Some(err), + )); + } + }; state.local_keypair = Some(local_keypair); } diff --git a/rtc-dtls/src/flight/flight4.rs b/rtc-dtls/src/flight/flight4.rs index edf22c09..3aa3e411 100644 --- a/rtc-dtls/src/flight/flight4.rs +++ b/rtc-dtls/src/flight/flight4.rs @@ -30,6 +30,8 @@ use crate::extension::renegotiation_info::ExtensionRenegotiationInfo; use log::*; use std::fmt; use std::io::BufWriter; +#[cfg(test)] +use std::sync::Arc; #[derive(Debug, PartialEq)] pub(crate) struct Flight4; @@ -204,6 +206,7 @@ impl Flight for Flight4 { } if let Err(err) = verify_certificate_verify( + cfg.provider().crypto(), &plain_text, &h.algorithm, &h.signature, @@ -305,11 +308,12 @@ impl Flight for Flight4 { .identity_hint .clone_from(&client_key_exchange.identity_hint); pre_master_secret = prf_psk_pre_master_secret(&psk); - } else if let Some(local_keypair) = &state.local_keypair { + } else if let Some(local_keypair) = &mut state.local_keypair { + let curve = local_keypair.curve; pre_master_secret = match prf_pre_master_secret( &client_key_exchange.public_key, - &local_keypair.private_key, - local_keypair.curve, + local_keypair, + curve, ) { Ok(pre_master_secret) => pre_master_secret, Err(err) => { @@ -326,7 +330,12 @@ impl Flight for Flight4 { if state.extended_master_secret { let hf = cipher_suite_hash_func; - let session_hash = match cache.session_hash(hf, cfg.initial_epoch, &[]) { + let session_hash = match cache.session_hash( + cfg.provider().crypto(), + hf, + cfg.initial_epoch, + &[], + ) { Ok(s) => s, Err(err) => { return Err(( @@ -340,6 +349,7 @@ impl Flight for Flight4 { }; state.master_secret = match prf_extended_master_secret( + cfg.provider().crypto(), &pre_master_secret, &session_hash, cipher_suite_hash_func, @@ -357,6 +367,7 @@ impl Flight for Flight4 { }; } else { state.master_secret = match prf_master_secret( + cfg.provider().crypto(), &pre_master_secret, &client_random, &server_random, @@ -377,6 +388,7 @@ impl Flight for Flight4 { if let Some(cipher_suite) = &mut state.cipher_suite && let Err(err) = cipher_suite.init( + cfg.provider().clone(), &state.master_secret, &client_random, &server_random, @@ -520,7 +532,7 @@ impl Flight for Flight4 { if cfg.local_psk_callback.is_none() { extensions.extend_from_slice(&[ Extension::SupportedEllipticCurves(ExtensionSupportedEllipticCurves { - elliptic_curves: vec![NamedCurve::P256, NamedCurve::X25519, NamedCurve::P384], + elliptic_curves: cfg.local_named_curves.clone(), }), Extension::SupportedPointFormats(ExtensionSupportedPointFormats { point_formats: vec![ELLIPTIC_CURVE_POINT_FORMAT_UNCOMPRESSED], @@ -618,7 +630,8 @@ impl Flight for Flight4 { &server_random, &local_keypair.public_key, state.named_curve, - &certificate.private_key, /*, signature_hash_algo.hash*/ + &signature_hash_algo, + &certificate.private_key, ) { Ok(s) => s, Err(err) => { @@ -751,6 +764,7 @@ mod tests { // Generate the internal encryption state fn init( &mut self, + _provider: Arc, _master_secret: &[u8], _client_random: &[u8], _server_random: &[u8], @@ -759,10 +773,10 @@ mod tests { unimplemented!(); } - fn encrypt(&self, _pkt_rlh: &RecordLayerHeader, _raw: &[u8]) -> Result> { + fn encrypt(&mut self, _pkt_rlh: &RecordLayerHeader, _raw: &[u8]) -> Result> { unimplemented!(); } - fn decrypt(&self, _input: &[u8]) -> Result> { + fn decrypt(&mut self, _input: &[u8]) -> Result> { unimplemented!(); } } @@ -772,9 +786,11 @@ mod tests { // is missing. #[test] fn test_flight4_process_certificateverify() { + let provider = + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"); let mut state = State { cipher_suite: Some(Box::new(MockCipherSuite {})), - ..Default::default() + ..State::new(provider, false) }; let raw_certificate = vec![ @@ -828,7 +844,9 @@ mod tests { true, ); - let cfg = HandshakeConfig::default(); + let cfg = HandshakeConfig::new( + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"), + ); let f = Flight4 {}; let res = f.parse(&mut state, &cache, &cfg); diff --git a/rtc-dtls/src/flight/flight5.rs b/rtc-dtls/src/flight/flight5.rs index c738cd28..daf25650 100644 --- a/rtc-dtls/src/flight/flight5.rs +++ b/rtc-dtls/src/flight/flight5.rs @@ -134,6 +134,7 @@ impl Flight for Flight5 { { if let Some(cipher_suite) = &state.cipher_suite { let expected_verify_data = match prf_verify_data_server( + cfg.provider().crypto(), &state.master_secret, &plain_text, cipher_suite.hash_func(), @@ -414,7 +415,8 @@ impl Flight for Flight5 { let cert_verify = match generate_certificate_verify( &plain_text, - &certificate.as_ref().unwrap().private_key, /*, signature_hash_algo.hash*/ + &signature_hash_algo, + &certificate.as_ref().unwrap().private_key, ) { Ok(cert) => cert, Err(err) => { @@ -556,6 +558,7 @@ impl Flight for Flight5 { if let Some(cipher_suite) = &state.cipher_suite { state.local_verify_data = match prf_verify_data_client( + cfg.provider().crypto(), &state.master_secret, &plain_text, cipher_suite.hash_func(), @@ -624,6 +627,7 @@ fn initalize_cipher_suite( if let Some((_, cipher_suite_hash_func)) = cipher_suite { if state.extended_master_secret { let session_hash = match cache.session_hash( + cfg.provider().crypto(), cipher_suite_hash_func, cfg.initial_epoch, sending_plain_text, @@ -641,6 +645,7 @@ fn initalize_cipher_suite( }; state.master_secret = match prf_extended_master_secret( + cfg.provider().crypto(), &state.pre_master_secret, &session_hash, cipher_suite_hash_func, @@ -658,6 +663,7 @@ fn initalize_cipher_suite( }; } else { state.master_secret = match prf_master_secret( + cfg.provider().crypto(), &state.pre_master_secret, &client_random, &server_random, @@ -699,6 +705,7 @@ fn initalize_cipher_suite( let expected_msg = value_key_message(&client_random, &server_random, &h.public_key, h.named_curve); if let Err(err) = verify_key_signature( + cfg.provider().crypto(), &expected_msg, &h.algorithm, &h.signature, @@ -716,22 +723,31 @@ fn initalize_cipher_suite( let mut chains = vec![]; if !cfg.insecure_skip_verify { - chains = match verify_server_cert( - &state.peer_certificates, - &cfg.server_cert_verifier, - &cfg.server_name, - ) { - Ok(chains) => chains, - Err(err) => { - return Err(( - Some(Alert { - alert_level: AlertLevel::Fatal, - alert_description: AlertDescription::BadCertificate, - }), - Some(err), - )); + let cert_verifier = cfg.server_cert_verifier.as_ref().ok_or_else(|| { + ( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(Error::Crypto( + "CA-chain verification has no configured verifier adapter".to_owned(), + )), + ) + })?; + chains = + match verify_server_cert(&state.peer_certificates, cert_verifier, &cfg.server_name) + { + Ok(chains) => chains, + Err(err) => { + return Err(( + Some(Alert { + alert_level: AlertLevel::Fatal, + alert_description: AlertDescription::BadCertificate, + }), + Some(err), + )); + } } - } } if let Some(verify_peer_certificate) = &cfg.verify_peer_certificate && let Err(err) = verify_peer_certificate(&state.peer_certificates, &chains) @@ -747,8 +763,13 @@ fn initalize_cipher_suite( } if let Some(cipher_suite) = &mut state.cipher_suite - && let Err(err) = - cipher_suite.init(&state.master_secret, &client_random, &server_random, true) + && let Err(err) = cipher_suite.init( + cfg.provider().clone(), + &state.master_secret, + &client_random, + &server_random, + true, + ) { return Err(( Some(Alert { diff --git a/rtc-dtls/src/flight/flight6.rs b/rtc-dtls/src/flight/flight6.rs index c4bc65ec..f7efa2d8 100644 --- a/rtc-dtls/src/flight/flight6.rs +++ b/rtc-dtls/src/flight/flight6.rs @@ -145,6 +145,7 @@ impl Flight for Flight6 { if let Some(cipher_suite) = &state.cipher_suite { state.local_verify_data = match prf_verify_data_server( + cfg.provider().crypto(), &state.master_secret, &plain_text, cipher_suite.hash_func(), diff --git a/rtc-dtls/src/handshake/handshake_cache.rs b/rtc-dtls/src/handshake/handshake_cache.rs index b38636a3..18d83060 100644 --- a/rtc-dtls/src/handshake/handshake_cache.rs +++ b/rtc-dtls/src/handshake/handshake_cache.rs @@ -7,8 +7,6 @@ use crate::handshake::*; use std::collections::HashMap; use std::io::BufReader; -use sha2::{Digest, Sha256}; - #[derive(Clone, Debug)] pub(crate) struct HandshakeCacheItem { typ: HandshakeType, @@ -154,6 +152,7 @@ impl HandshakeCache { // https://tools.ietf.org/html/draft-ietf-tls-session-hash-06#section-4 pub(crate) fn session_hash( &self, + crypto: &dyn crypto::RTCCrypto, hf: CipherSuiteHash, epoch: u16, additional: &[u8], @@ -218,12 +217,10 @@ impl HandshakeCache { merged.extend_from_slice(additional); - let mut hasher = match hf { - CipherSuiteHash::Sha256 => Sha256::new(), - }; - hasher.update(&merged); - let result = hasher.finalize(); - - Ok(result.as_slice().to_vec()) + match hf { + CipherSuiteHash::Sha256 => crypto + .hash(crypto::HashAlgorithm::Sha256, &merged) + .map_err(|error| Error::Crypto(error.to_string())), + } } } diff --git a/rtc-dtls/src/handshake/handshake_cache/handshake_cache_test.rs b/rtc-dtls/src/handshake/handshake_cache/handshake_cache_test.rs index 24a67608..c8667364 100644 --- a/rtc-dtls/src/handshake/handshake_cache/handshake_cache_test.rs +++ b/rtc-dtls/src/handshake/handshake_cache/handshake_cache_test.rs @@ -644,7 +644,8 @@ fn test_handshake_cache_session_hash() -> Result<()> { h.push(i.data, i.epoch, i.message_sequence, i.typ, i.is_client); } - let verify_data = h.session_hash(CipherSuiteHash::Sha256, 0, &[])?; + let provider = crypto::default_provider().map_err(|e| Error::Crypto(e.to_string()))?; + let verify_data = h.session_hash(provider.crypto(), CipherSuiteHash::Sha256, 0, &[])?; assert_eq!( verify_data, expected, diff --git a/rtc-dtls/src/handshake/handshake_header.rs b/rtc-dtls/src/handshake/handshake_header.rs index 72cb91fc..19eaab67 100644 --- a/rtc-dtls/src/handshake/handshake_header.rs +++ b/rtc-dtls/src/handshake/handshake_header.rs @@ -8,6 +8,8 @@ use std::io::{Read, Write}; pub(crate) const HANDSHAKE_HEADER_LENGTH: usize = 12; #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +/// The header on every handshake message: type, length, message sequence, and the fragment +/// offset and length that let one message span datagrams. pub struct HandshakeHeader { pub(crate) handshake_type: HandshakeType, pub(crate) length: u32, // uint24 in spec @@ -17,10 +19,16 @@ pub struct HandshakeHeader { } impl HandshakeHeader { + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 1 + 3 + 2 + 3 + 3 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(self.handshake_type as u8)?; writer.write_u24::(self.length)?; @@ -31,6 +39,11 @@ impl HandshakeHeader { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let handshake_type = reader.read_u8()?.into(); let length = reader.read_u24::()?; diff --git a/rtc-dtls/src/handshake/handshake_message_certificate.rs b/rtc-dtls/src/handshake/handshake_message_certificate.rs index 8e348d4f..649f69c3 100644 --- a/rtc-dtls/src/handshake/handshake_message_certificate.rs +++ b/rtc-dtls/src/handshake/handshake_message_certificate.rs @@ -9,15 +9,18 @@ mod handshake_message_certificate_test; const HANDSHAKE_MESSAGE_CERTIFICATE_LENGTH_FIELD_SIZE: usize = 3; #[derive(PartialEq, Eq, Debug, Clone)] +/// The sender's certificate chain. pub struct HandshakeMessageCertificate { pub(crate) certificate: Vec>, } impl HandshakeMessageCertificate { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::Certificate } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { let mut len = 3; @@ -28,6 +31,11 @@ impl HandshakeMessageCertificate { len } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { let mut payload_size = 0; for r in &self.certificate { @@ -48,6 +56,11 @@ impl HandshakeMessageCertificate { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let mut certificate: Vec> = vec![]; diff --git a/rtc-dtls/src/handshake/handshake_message_certificate_request.rs b/rtc-dtls/src/handshake/handshake_message_certificate_request.rs index 0e882794..1b5cbdd3 100644 --- a/rtc-dtls/src/handshake/handshake_message_certificate_request.rs +++ b/rtc-dtls/src/handshake/handshake_message_certificate_request.rs @@ -16,6 +16,8 @@ message (if it is sent; otherwise, this message follows the server's Certificate message). */ #[derive(Clone, Debug, PartialEq, Eq)] +/// The server's request that the client authenticate, listing acceptable certificate types +/// and signature algorithms. pub struct HandshakeMessageCertificateRequest { pub(crate) certificate_types: Vec, pub(crate) signature_hash_algorithms: Vec, @@ -24,14 +26,21 @@ pub struct HandshakeMessageCertificateRequest { const HANDSHAKE_MESSAGE_CERTIFICATE_REQUEST_MIN_LENGTH: usize = 5; impl HandshakeMessageCertificateRequest { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::CertificateRequest } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 1 + self.certificate_types.len() + 2 + self.signature_hash_algorithms.len() * 2 + 2 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(self.certificate_types.len() as u8)?; for v in &self.certificate_types { @@ -49,6 +58,11 @@ impl HandshakeMessageCertificateRequest { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let certificate_types_length = reader.read_u8()?; diff --git a/rtc-dtls/src/handshake/handshake_message_certificate_verify.rs b/rtc-dtls/src/handshake/handshake_message_certificate_verify.rs index 6e0f0953..251318be 100644 --- a/rtc-dtls/src/handshake/handshake_message_certificate_verify.rs +++ b/rtc-dtls/src/handshake/handshake_message_certificate_verify.rs @@ -8,6 +8,7 @@ use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use std::io::{Read, Write}; #[derive(Clone, Debug, PartialEq, Eq)] +/// A signature over the handshake so far, proving possession of the certificate's private key. pub struct HandshakeMessageCertificateVerify { pub(crate) algorithm: SignatureHashAlgorithm, pub(crate) signature: Vec, @@ -16,14 +17,21 @@ pub struct HandshakeMessageCertificateVerify { const HANDSHAKE_MESSAGE_CERTIFICATE_VERIFY_MIN_LENGTH: usize = 4; impl HandshakeMessageCertificateVerify { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::CertificateVerify } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 1 + 1 + 2 + self.signature.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(self.algorithm.hash as u8)?; writer.write_u8(self.algorithm.signature as u8)?; @@ -33,6 +41,11 @@ impl HandshakeMessageCertificateVerify { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let hash_algorithm = reader.read_u8()?.into(); let signature_algorithm = reader.read_u8()?.into(); diff --git a/rtc-dtls/src/handshake/handshake_message_client_hello.rs b/rtc-dtls/src/handshake/handshake_message_client_hello.rs index 78f91201..eb590b3d 100644 --- a/rtc-dtls/src/handshake/handshake_message_client_hello.rs +++ b/rtc-dtls/src/handshake/handshake_message_client_hello.rs @@ -20,6 +20,7 @@ initiative in order to renegotiate the security parameters in an existing connection. */ #[derive(Clone)] +/// The client's opening message: its random, cookie, offered cipher suites and extensions. pub struct HandshakeMessageClientHello { pub(crate) version: ProtocolVersion, pub(crate) random: HandshakeRandom, @@ -73,10 +74,12 @@ impl fmt::Debug for HandshakeMessageClientHello { const HANDSHAKE_MESSAGE_CLIENT_HELLO_VARIABLE_WIDTH_START: usize = 34; impl HandshakeMessageClientHello { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::ClientHello } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { let mut len = 0; @@ -100,6 +103,11 @@ impl HandshakeMessageClientHello { len } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { if self.cookie.len() > 255 { return Err(Error::ErrCookieTooLong); @@ -136,6 +144,11 @@ impl HandshakeMessageClientHello { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let major = reader.read_u8()?; let minor = reader.read_u8()?; diff --git a/rtc-dtls/src/handshake/handshake_message_client_key_exchange.rs b/rtc-dtls/src/handshake/handshake_message_client_key_exchange.rs index 6c23985d..1d23696f 100644 --- a/rtc-dtls/src/handshake/handshake_message_client_key_exchange.rs +++ b/rtc-dtls/src/handshake/handshake_message_client_key_exchange.rs @@ -7,16 +7,19 @@ use byteorder::{BigEndian, WriteBytesExt}; use std::io::{Read, Write}; #[derive(Clone, Debug, PartialEq, Eq)] +/// The client's half of the key agreement — its ECDHE public key, or a PSK identity. pub struct HandshakeMessageClientKeyExchange { pub(crate) identity_hint: Vec, pub(crate) public_key: Vec, } impl HandshakeMessageClientKeyExchange { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::ClientKeyExchange } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { if !self.public_key.is_empty() { 1 + self.public_key.len() @@ -25,6 +28,11 @@ impl HandshakeMessageClientKeyExchange { } } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { if (!self.identity_hint.is_empty() && !self.public_key.is_empty()) || (self.identity_hint.is_empty() && self.public_key.is_empty()) @@ -43,6 +51,11 @@ impl HandshakeMessageClientKeyExchange { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let mut data = vec![]; reader.read_to_end(&mut data)?; diff --git a/rtc-dtls/src/handshake/handshake_message_finished.rs b/rtc-dtls/src/handshake/handshake_message_finished.rs index a5bb190e..2c798991 100644 --- a/rtc-dtls/src/handshake/handshake_message_finished.rs +++ b/rtc-dtls/src/handshake/handshake_message_finished.rs @@ -6,25 +6,38 @@ use super::*; use std::io::{Read, Write}; #[derive(Clone, Debug, PartialEq, Eq)] +/// A hash over every preceding handshake message, which both sides compare to detect tampering. pub struct HandshakeMessageFinished { pub(crate) verify_data: Vec, } impl HandshakeMessageFinished { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::Finished } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { self.verify_data.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_all(&self.verify_data)?; Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let mut verify_data: Vec = vec![]; reader.read_to_end(&mut verify_data)?; diff --git a/rtc-dtls/src/handshake/handshake_message_hello_verify_request.rs b/rtc-dtls/src/handshake/handshake_message_hello_verify_request.rs index 2a3a915b..b6ff126e 100644 --- a/rtc-dtls/src/handshake/handshake_message_hello_verify_request.rs +++ b/rtc-dtls/src/handshake/handshake_message_hello_verify_request.rs @@ -34,14 +34,21 @@ pub struct HandshakeMessageHelloVerifyRequest { } impl HandshakeMessageHelloVerifyRequest { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::HelloVerifyRequest } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 1 + 1 + 1 + self.cookie.len() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { if self.cookie.len() > 255 { return Err(Error::ErrCookieTooLong); @@ -55,6 +62,11 @@ impl HandshakeMessageHelloVerifyRequest { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let major = reader.read_u8()?; let minor = reader.read_u8()?; diff --git a/rtc-dtls/src/handshake/handshake_message_server_hello.rs b/rtc-dtls/src/handshake/handshake_message_server_hello.rs index 3220d7dd..8fcb29e1 100644 --- a/rtc-dtls/src/handshake/handshake_message_server_hello.rs +++ b/rtc-dtls/src/handshake/handshake_message_server_hello.rs @@ -20,6 +20,7 @@ failure alert. https://tools.ietf.org/html/rfc5246#section-7.4.1.3 */ #[derive(Clone)] +/// The server's reply: its random, and the cipher suite and extensions it selected. pub struct HandshakeMessageServerHello { pub(crate) version: ProtocolVersion, pub(crate) random: HandshakeRandom, @@ -52,10 +53,12 @@ impl fmt::Debug for HandshakeMessageServerHello { } impl HandshakeMessageServerHello { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::ServerHello } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { let mut len = 2 + self.random.size(); @@ -74,6 +77,11 @@ impl HandshakeMessageServerHello { len } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { writer.write_u8(self.version.major)?; writer.write_u8(self.version.minor)?; @@ -100,6 +108,11 @@ impl HandshakeMessageServerHello { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let major = reader.read_u8()?; let minor = reader.read_u8()?; diff --git a/rtc-dtls/src/handshake/handshake_message_server_hello_done.rs b/rtc-dtls/src/handshake/handshake_message_server_hello_done.rs index ce6445e6..ccf48c91 100644 --- a/rtc-dtls/src/handshake/handshake_message_server_hello_done.rs +++ b/rtc-dtls/src/handshake/handshake_message_server_hello_done.rs @@ -6,21 +6,34 @@ use super::*; use std::io::{Read, Write}; #[derive(Clone, Debug, PartialEq, Eq)] +/// Marks the end of the server's first flight. Carries no fields. pub struct HandshakeMessageServerHelloDone; impl HandshakeMessageServerHelloDone { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::ServerHelloDone } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 0 } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, _writer: &mut W) -> Result<()> { Ok(()) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(_reader: &mut R) -> Result { Ok(HandshakeMessageServerHelloDone {}) } diff --git a/rtc-dtls/src/handshake/handshake_message_server_key_exchange.rs b/rtc-dtls/src/handshake/handshake_message_server_key_exchange.rs index 8062f015..7523599d 100644 --- a/rtc-dtls/src/handshake/handshake_message_server_key_exchange.rs +++ b/rtc-dtls/src/handshake/handshake_message_server_key_exchange.rs @@ -11,6 +11,7 @@ use std::io::{Read, Write}; // Structure supports ECDH and PSK #[derive(Clone, Debug, PartialEq, Eq)] +/// The server's half of the key agreement, signed so the client can authenticate it. pub struct HandshakeMessageServerKeyExchange { pub(crate) identity_hint: Vec, @@ -22,10 +23,12 @@ pub struct HandshakeMessageServerKeyExchange { } impl HandshakeMessageServerKeyExchange { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { HandshakeType::ServerKeyExchange } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { if !self.identity_hint.is_empty() { 2 + self.identity_hint.len() @@ -34,6 +37,11 @@ impl HandshakeMessageServerKeyExchange { } } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { if !self.identity_hint.is_empty() { writer.write_u16::(self.identity_hint.len() as u16)?; @@ -56,6 +64,11 @@ impl HandshakeMessageServerKeyExchange { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let mut data = vec![]; reader.read_to_end(&mut data)?; diff --git a/rtc-dtls/src/handshake/handshake_random.rs b/rtc-dtls/src/handshake/handshake_random.rs index a94c8c9e..bb2bd927 100644 --- a/rtc-dtls/src/handshake/handshake_random.rs +++ b/rtc-dtls/src/handshake/handshake_random.rs @@ -1,10 +1,10 @@ -use rand::RngExt; - use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use std::io::{self, Read, Write}; use std::time::{Duration, SystemTime}; +/// Bytes of randomness in a handshake random, excluding the timestamp. pub const RANDOM_BYTES_LENGTH: usize = 28; +/// Total length of a handshake random: 4 bytes of timestamp plus 28 random. pub const HANDSHAKE_RANDOM_LENGTH: usize = RANDOM_BYTES_LENGTH + 4; /// ## Specifications @@ -14,7 +14,9 @@ pub const HANDSHAKE_RANDOM_LENGTH: usize = RANDOM_BYTES_LENGTH + 4; /// [RFC 4346 §7.4.1.2]: https://tools.ietf.org/html/rfc4346#section-7.4.1.2 #[derive(Clone, Debug, PartialEq, Eq)] pub struct HandshakeRandom { + /// The sender's clock at the time the random was generated. pub gmt_unix_time: SystemTime, + /// 28 bytes of randomness, which feed key derivation. pub random_bytes: [u8; RANDOM_BYTES_LENGTH], } @@ -28,10 +30,16 @@ impl Default for HandshakeRandom { } impl HandshakeRandom { + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { 4 + RANDOM_BYTES_LENGTH } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> io::Result<()> { let secs = match self.gmt_unix_time.duration_since(SystemTime::UNIX_EPOCH) { Ok(d) => d.as_secs() as u32, @@ -43,6 +51,11 @@ impl HandshakeRandom { writer.flush() } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> io::Result { let secs = reader.read_u32::()?; let gmt_unix_time = if let Some(unix_time) = @@ -64,8 +77,12 @@ impl HandshakeRandom { // populate fills the HandshakeRandom with random values // may be called multiple times - pub fn populate(&mut self) { + /// Fills in the current time and fresh random bytes. + pub fn populate(&mut self, random: &dyn crypto::RTCRandom) -> shared::error::Result<()> { self.gmt_unix_time = SystemTime::now(); - rand::rng().fill(&mut self.random_bytes); + random + .fill(&mut self.random_bytes) + .map_err(|error| shared::error::Error::Crypto(error.to_string()))?; + Ok(()) } } diff --git a/rtc-dtls/src/handshake/mod.rs b/rtc-dtls/src/handshake/mod.rs index 4bb68366..22ed57e5 100644 --- a/rtc-dtls/src/handshake/mod.rs +++ b/rtc-dtls/src/handshake/mod.rs @@ -1,15 +1,41 @@ +//! DTLS handshake messages. +//! +//! Each message type has its own module; [`HandshakeMessage`](crate::handshake::HandshakeMessage) is the parsed union of them and +//! [`Handshake`](crate::handshake::Handshake) pairs one with its [`HandshakeHeader`](crate::handshake::handshake_header::HandshakeHeader). +//! +//! Two things distinguish this from a TLS handshake. Messages carry a sequence number and +//! fragment offsets, because a handshake message may be larger than a datagram and must be +//! reassembled. And the server may answer a ClientHello with a +//! [`HelloVerifyRequest`](crate::handshake::handshake_message_hello_verify_request) carrying a cookie, which the +//! client echoes — a cheap defence against using the handshake for amplification. +//! +//! [`handshake_cache`](crate::handshake::handshake_cache) retains the messages so the hash in `Finished` can be computed over +//! exactly what both sides saw. +/// Buffers handshake messages so their hash can be computed for `Finished` verification. pub mod handshake_cache; +/// The header prefixing every handshake message, including fragment offsets. pub mod handshake_header; +/// Certificate: the sender's certificate chain. pub mod handshake_message_certificate; +/// CertificateRequest: the server asks the client to authenticate. pub mod handshake_message_certificate_request; +/// CertificateVerify: proves possession of the certificate's private key. pub mod handshake_message_certificate_verify; +/// ClientHello: opens the handshake with the client's offers. pub mod handshake_message_client_hello; +/// ClientKeyExchange: the client's half of the key agreement. pub mod handshake_message_client_key_exchange; +/// Finished: a hash over the handshake, proving both sides saw the same messages. pub mod handshake_message_finished; +/// HelloVerifyRequest: DTLS's cookie exchange, which resists amplification attacks. pub mod handshake_message_hello_verify_request; +/// ServerHello: the server's chosen parameters. pub mod handshake_message_server_hello; +/// ServerHelloDone: the server has finished its first flight. pub mod handshake_message_server_hello_done; +/// ServerKeyExchange: the server's half of the key agreement. pub mod handshake_message_server_key_exchange; +/// The 32-byte random each side contributes to key derivation. pub mod handshake_random; #[cfg(test)] @@ -39,19 +65,32 @@ use handshake_message_server_key_exchange::*; /// /// [RFC 5246 §7.4]: https://tools.ietf.org/html/rfc5246#section-7.4 #[derive(Default, Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] pub enum HandshakeType { + /// `HELLO_REQUEST` (`0`). HelloRequest = 0, + /// `CLIENT_HELLO` (`1`). ClientHello = 1, + /// `SERVER_HELLO` (`2`). ServerHello = 2, + /// `HELLO_VERIFY_REQUEST` (`3`). HelloVerifyRequest = 3, + /// `CERTIFICATE` (`11`). Certificate = 11, + /// `SERVER_KEY_EXCHANGE` (`12`). ServerKeyExchange = 12, + /// `CERTIFICATE_REQUEST` (`13`). CertificateRequest = 13, + /// `SERVER_HELLO_DONE` (`14`). ServerHelloDone = 14, + /// `CERTIFICATE_VERIFY` (`15`). CertificateVerify = 15, + /// `CLIENT_KEY_EXCHANGE` (`16`). ClientKeyExchange = 16, + /// `FINISHED` (`20`). Finished = 20, #[default] + /// A handshake type this crate does not recognise. Invalid, } @@ -94,21 +133,34 @@ impl From for HandshakeType { } #[derive(PartialEq, Debug, Clone)] +/// A parsed handshake message. +#[non_exhaustive] pub enum HandshakeMessage { //HelloRequest(errNotImplemented), + /// ClientHello, which opens the handshake. ClientHello(HandshakeMessageClientHello), + /// ServerHello, carrying the server's chosen parameters. ServerHello(HandshakeMessageServerHello), + /// HelloVerifyRequest, DTLS's cookie challenge. HelloVerifyRequest(HandshakeMessageHelloVerifyRequest), + /// Certificate, carrying a certificate chain. Certificate(HandshakeMessageCertificate), + /// ServerKeyExchange, the server's key-agreement share. ServerKeyExchange(HandshakeMessageServerKeyExchange), + /// CertificateRequest, asking the client to authenticate. CertificateRequest(HandshakeMessageCertificateRequest), + /// ServerHelloDone, ending the server's first flight. ServerHelloDone(HandshakeMessageServerHelloDone), + /// CertificateVerify, proving possession of the certificate key. CertificateVerify(HandshakeMessageCertificateVerify), + /// ClientKeyExchange, the client's key-agreement share. ClientKeyExchange(HandshakeMessageClientKeyExchange), + /// Finished, a hash over the handshake that both sides verify. Finished(HandshakeMessageFinished), } impl HandshakeMessage { + /// The handshake type that identifies this message on the wire. pub fn handshake_type(&self) -> HandshakeType { match self { HandshakeMessage::ClientHello(msg) => msg.handshake_type(), @@ -124,6 +176,7 @@ impl HandshakeMessage { } } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { match self { HandshakeMessage::ClientHello(msg) => msg.size(), @@ -139,6 +192,11 @@ impl HandshakeMessage { } } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { match self { HandshakeMessage::ClientHello(msg) => msg.marshal(writer)?, @@ -164,12 +222,14 @@ impl HandshakeMessage { // certificates signed by a trusted certificate authority. // https://tools.ietf.org/html/rfc5246#section-7.3 #[derive(PartialEq, Debug, Clone)] +/// A handshake record: its header plus the message it carries. pub struct Handshake { pub(crate) handshake_header: HandshakeHeader, pub(crate) handshake_message: HandshakeMessage, } impl Handshake { + /// Wraps a message in a handshake record, filling in its header. pub fn new(handshake_message: HandshakeMessage) -> Self { Handshake { handshake_header: HandshakeHeader { @@ -183,20 +243,32 @@ impl Handshake { } } + /// The record content type this message is carried in. pub fn content_type(&self) -> ContentType { ContentType::Handshake } + /// The encoded size of this message in bytes. pub fn size(&self) -> usize { self.handshake_header.size() + self.handshake_message.size() } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { self.handshake_header.marshal(writer)?; self.handshake_message.marshal(writer)?; Ok(()) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let handshake_header = HandshakeHeader::unmarshal(reader)?; diff --git a/rtc-dtls/src/lib.rs b/rtc-dtls/src/lib.rs index 41a1020e..45bd9c0e 100644 --- a/rtc-dtls/src/lib.rs +++ b/rtc-dtls/src/lib.rs @@ -1,38 +1,112 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! DTLS 1.2 for the Sans-I/O WebRTC stack. +//! +//! An implementation of Datagram Transport Layer Security ([RFC 6347]) with the extensions +//! WebRTC requires: DTLS-SRTP key export ([RFC 5764]), extended master secret +//! ([RFC 7627]), and elliptic-curve cipher suites ([RFC 4492], [RFC 5289]). It secures the +//! media and data-channel path: SRTP keying material comes out of the DTLS handshake, and +//! SCTP data channels run over the DTLS association itself. +//! +//! # Structure +//! +//! * [`endpoint`] — the Sans-I/O entry point: feed it datagrams, poll it for the datagrams +//! it wants to send and the events it produces. No sockets, no timers of its own. +//! * [`config`] — certificates, cipher-suite and curve preferences, the client/server role, +//! and the SRTP protection profiles to negotiate. +//! * [`handshake`], [`flight`], [`state`] — the handshake message types and the flight state +//! machine that drives them, including retransmission. +//! * [`cipher_suite`], [`crypto`], [`curve`], [`signature_hash_algorithm`] — the +//! cryptographic primitives and the negotiated-suite abstraction. +//! * [`extension`] — the ClientHello/ServerHello extensions, including `use_srtp` and SNI. +//! * [`alert`], [`content`], [`record_layer`] — the record layer and its content types. +//! +//! # Example +//! +//! A WebRTC handshake is configured with a self-signed certificate and the SRTP profiles to +//! negotiate through `use_srtp`; the keys for those profiles are then exported from the +//! completed handshake rather than signalled: +//! +//! ``` +//! use rtc_dtls::config::{ConfigBuilder, ExtendedMasterSecretType}; +//! use rtc_dtls::extension::extension_use_srtp::SrtpProtectionProfile; +//! +//! let builder = ConfigBuilder::default() +//! .with_srtp_protection_profiles(vec![ +//! SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm, +//! SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, +//! ]) +//! .with_extended_master_secret(ExtendedMasterSecretType::Require); +//! # let _ = builder; +//! ``` +//! +//! Most applications do not depend on this crate directly — the +//! [`rtc`](https://docs.rs/rtc) crate drives it as one layer of the peer-connection +//! pipeline. +//! +//! [RFC 6347]: https://datatracker.ietf.org/doc/html/rfc6347 +//! [RFC 5764]: https://datatracker.ietf.org/doc/html/rfc5764 +//! [RFC 7627]: https://datatracker.ietf.org/doc/html/rfc7627 +//! [RFC 4492]: https://datatracker.ietf.org/doc/html/rfc4492 +//! [RFC 5289]: https://datatracker.ietf.org/doc/html/rfc5289 + +/// The crypto provider API. +/// +/// Re-exported because this crate's public constructors take an `Arc`, +/// which a caller must be able to name without adding — and version-matching — a direct +/// `rtc-crypto` dependency. Named `crypto_provider` here because this crate already has its own +/// [`crypto`] module for DTLS record ciphers and certificates. +pub use ::crypto as crypto_provider; + +/// Alert records: fatal errors and the orderly `close_notify`. pub mod alert; +/// Application data records — the payload DTLS carries once the handshake completes. pub mod application_data; +/// The ChangeCipherSpec record, which switches a side over to the negotiated keys. pub mod change_cipher_spec; +/// The negotiable cipher suites and the [`CipherSuite`] trait they +/// implement. pub mod cipher_suite; +/// Certificate types a server may request from a client. pub mod client_certificate_type; +/// The compression-methods field. DTLS in WebRTC always negotiates null compression. pub mod compression_methods; +/// Handshake configuration: certificates, roles, cipher-suite and SRTP profile preferences. pub mod config; +/// Connection state shared across the handshake and record layers. pub mod conn; +/// Record content types: handshake, alert, change-cipher-spec and application data. pub mod content; +/// Cryptographic primitives: the AEAD and CBC ciphers, certificates and signatures. pub mod crypto; +/// Elliptic curves and the key-exchange values exchanged over them. pub mod curve; +/// The Sans-I/O entry point: feed it datagrams, poll it for output and events. pub mod endpoint; +/// ClientHello and ServerHello extensions, including `use_srtp` and SNI. pub mod extension; +/// The flight state machine, which drives the handshake and its retransmissions. pub mod flight; +/// Reassembly of handshake messages fragmented across datagrams. pub mod fragment_buffer; +/// The handshake message types and the cache that hashes them for `Finished`. pub mod handshake; +/// Handshake orchestration: state, roles and the verification callbacks. pub mod handshaker; +/// The pseudo-random function that expands the master secret into keys. pub mod prf; +/// The record layer: framing, sequence numbers and epochs. pub mod record_layer; +/// Signature and hash algorithm pairs, as negotiated for certificate verification. pub mod signature_hash_algorithm; +/// The negotiated connection state: keys, sequence numbers and peer identity. pub mod state; use cipher_suite::*; use extension::extension_use_srtp::SrtpProtectionProfile; -#[cfg(all(feature = "aws-lc-rs", feature = "ring"))] -compile_error!("At most one of the features \"aws-lc-rs\" and \"ring\" can be enabled."); -#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))] -compile_error!("At least one of the features \"aws-lc-rs\" and \"ring\" must be enabled."); -#[cfg(feature = "aws-lc-rs")] -extern crate aws_lc_rs as ring; - pub(crate) fn find_matching_srtp_profile( a: &[SrtpProtectionProfile], b: &[SrtpProtectionProfile], diff --git a/rtc-dtls/src/prf/mod.rs b/rtc-dtls/src/prf/mod.rs index aebe02e2..74ec2346 100644 --- a/rtc-dtls/src/prf/mod.rs +++ b/rtc-dtls/src/prf/mod.rs @@ -1,16 +1,9 @@ #[cfg(test)] mod prf_test; -use std::convert::TryInto; use std::fmt; -use hmac::{Hmac, Mac}; -use sha1::Sha1; -use sha2::Digest; -use sha2::Sha256; - -type HmacSha256 = Hmac; -type HmacSha1 = Hmac; +use crypto::{HashAlgorithm as CryptoHashAlgorithm, HmacAlgorithm, Mac, RTCCrypto}; use crate::cipher_suite::CipherSuiteHash; use crate::content::ContentType; @@ -71,50 +64,13 @@ pub(crate) fn prf_psk_pre_master_secret(psk: &[u8]) -> Vec { pub(crate) fn prf_pre_master_secret( public_key: &[u8], - private_key: &NamedCurvePrivateKey, - curve: NamedCurve, -) -> Result> { - match curve { - NamedCurve::P256 => elliptic_curve_pre_master_secret(public_key, private_key, curve), - NamedCurve::P384 => elliptic_curve_pre_master_secret(public_key, private_key, curve), - NamedCurve::X25519 => elliptic_curve_pre_master_secret(public_key, private_key, curve), - _ => Err(Error::ErrInvalidNamedCurve), - } -} - -fn elliptic_curve_pre_master_secret( - public_key: &[u8], - private_key: &NamedCurvePrivateKey, + keypair: &mut NamedCurveKeypair, curve: NamedCurve, ) -> Result> { - match curve { - NamedCurve::P256 => { - let pub_key = p256::EncodedPoint::from_bytes(public_key)?; - let public = p256::PublicKey::from_sec1_bytes(pub_key.as_ref())?; - if let NamedCurvePrivateKey::EphemeralSecretP256(secret) = private_key { - return Ok(secret.diffie_hellman(&public).raw_secret_bytes().to_vec()); - } - } - NamedCurve::P384 => { - let pub_key = p384::EncodedPoint::from_bytes(public_key)?; - let public = p384::PublicKey::from_sec1_bytes(pub_key.as_ref())?; - if let NamedCurvePrivateKey::EphemeralSecretP384(secret) = private_key { - return Ok(secret.diffie_hellman(&public).raw_secret_bytes().to_vec()); - } - } - NamedCurve::X25519 => { - if public_key.len() != 32 { - return Err(Error::Other("Public key is not 32 len".into())); - } - let pub_key: [u8; 32] = public_key.try_into().unwrap(); - let public = x25519_dalek::PublicKey::from(pub_key); - if let NamedCurvePrivateKey::StaticSecretX25519(secret) = private_key { - return Ok(secret.diffie_hellman(&public).as_bytes().to_vec()); - } - } - _ => return Err(Error::ErrInvalidNamedCurve), + if keypair.curve != curve { + return Err(Error::ErrNamedCurveAndPrivateKeyMismatch); } - Err(Error::ErrNamedCurveAndPrivateKeyMismatch) + keypair.complete(public_key) } // This PRF with the SHA-256 hash function is used for all cipher suites @@ -140,19 +96,25 @@ fn elliptic_curve_pre_master_secret( // output data. // // https://tools.ietf.org/html/rfc4346w -fn hmac_sha(h: CipherSuiteHash, key: &[u8], data: &[u8]) -> Result> { - let mut mac = match h { - CipherSuiteHash::Sha256 => { - HmacSha256::new_from_slice(key).map_err(|e| Error::Other(e.to_string()))? - } +fn hmac_sha( + crypto: &dyn RTCCrypto, + h: CipherSuiteHash, + key: &[u8], + input: &[&[u8]], +) -> Result> { + let algorithm = match h { + CipherSuiteHash::Sha256 => HmacAlgorithm::Sha256, }; - mac.update(data); - let result = mac.finalize(); - let code_bytes = result.into_bytes(); - Ok(code_bytes.to_vec()) + let mut output = vec![0; algorithm.output_len()]; + crypto + .new_hmac(algorithm, key) + .and_then(|mut mac| mac.sign(input, &mut output)) + .map_err(|error| Error::Crypto(error.to_string()))?; + Ok(output) } pub(crate) fn prf_p_hash( + crypto: &dyn RTCCrypto, secret: &[u8], seed: &[u8], requested_length: usize, @@ -163,11 +125,8 @@ pub(crate) fn prf_p_hash( let iterations = ((requested_length as f64) / (h.size() as f64)).ceil() as usize; for _ in 0..iterations { - last_round = hmac_sha(h, secret, &last_round)?; - - let mut last_round_seed = last_round.clone(); - last_round_seed.extend_from_slice(seed); - let with_secret = hmac_sha(h, secret, &last_round_seed)?; + last_round = hmac_sha(crypto, h, secret, &[&last_round])?; + let with_secret = hmac_sha(crypto, h, secret, &[&last_round, seed])?; out.extend_from_slice(&with_secret); } @@ -176,16 +135,18 @@ pub(crate) fn prf_p_hash( } pub(crate) fn prf_extended_master_secret( + crypto: &dyn RTCCrypto, pre_master_secret: &[u8], session_hash: &[u8], h: CipherSuiteHash, ) -> Result> { let mut seed = PRF_EXTENDED_MASTER_SECRET_LABEL.as_bytes().to_vec(); seed.extend_from_slice(session_hash); - prf_p_hash(pre_master_secret, &seed, 48, h) + prf_p_hash(crypto, pre_master_secret, &seed, 48, h) } pub(crate) fn prf_master_secret( + crypto: &dyn RTCCrypto, pre_master_secret: &[u8], client_random: &[u8], server_random: &[u8], @@ -194,16 +155,21 @@ pub(crate) fn prf_master_secret( let mut seed = PRF_MASTER_SECRET_LABEL.as_bytes().to_vec(); seed.extend_from_slice(client_random); seed.extend_from_slice(server_random); - prf_p_hash(pre_master_secret, &seed, 48, h) + prf_p_hash(crypto, pre_master_secret, &seed, 48, h) +} + +pub(crate) struct EncryptionKeyLengths { + pub(crate) mac: usize, + pub(crate) key: usize, + pub(crate) iv: usize, } pub(crate) fn prf_encryption_keys( + crypto: &dyn RTCCrypto, master_secret: &[u8], client_random: &[u8], server_random: &[u8], - prf_mac_len: usize, - prf_key_len: usize, - prf_iv_len: usize, + lengths: EncryptionKeyLengths, h: CipherSuiteHash, ) -> Result { let mut seed = PRF_KEY_EXPANSION_LABEL.as_bytes().to_vec(); @@ -211,29 +177,30 @@ pub(crate) fn prf_encryption_keys( seed.extend_from_slice(client_random); let material = prf_p_hash( + crypto, master_secret, &seed, - (2 * prf_mac_len) + (2 * prf_key_len) + (2 * prf_iv_len), + (2 * lengths.mac) + (2 * lengths.key) + (2 * lengths.iv), h, )?; let mut key_material = &material[..]; - let client_mac_key = key_material[..prf_mac_len].to_vec(); - key_material = &key_material[prf_mac_len..]; + let client_mac_key = key_material[..lengths.mac].to_vec(); + key_material = &key_material[lengths.mac..]; - let server_mac_key = key_material[..prf_mac_len].to_vec(); - key_material = &key_material[prf_mac_len..]; + let server_mac_key = key_material[..lengths.mac].to_vec(); + key_material = &key_material[lengths.mac..]; - let client_write_key = key_material[..prf_key_len].to_vec(); - key_material = &key_material[prf_key_len..]; + let client_write_key = key_material[..lengths.key].to_vec(); + key_material = &key_material[lengths.key..]; - let server_write_key = key_material[..prf_key_len].to_vec(); - key_material = &key_material[prf_key_len..]; + let server_write_key = key_material[..lengths.key].to_vec(); + key_material = &key_material[lengths.key..]; - let client_write_iv = key_material[..prf_iv_len].to_vec(); - key_material = &key_material[prf_iv_len..]; + let client_write_iv = key_material[..lengths.iv].to_vec(); + key_material = &key_material[lengths.iv..]; - let server_write_iv = key_material[..prf_iv_len].to_vec(); + let server_write_iv = key_material[..lengths.iv].to_vec(); Ok(EncryptionKeys { master_secret: master_secret.to_vec(), @@ -247,28 +214,31 @@ pub(crate) fn prf_encryption_keys( } pub(crate) fn prf_verify_data( + crypto: &dyn RTCCrypto, master_secret: &[u8], handshake_bodies: &[u8], label: &str, h: CipherSuiteHash, ) -> Result> { - let mut hasher = match h { - CipherSuiteHash::Sha256 => Sha256::new(), + let result = match h { + CipherSuiteHash::Sha256 => crypto + .hash(CryptoHashAlgorithm::Sha256, handshake_bodies) + .map_err(|error| Error::Crypto(error.to_string()))?, }; - hasher.update(handshake_bodies); - let result = hasher.finalize(); let mut seed = label.as_bytes().to_vec(); seed.extend_from_slice(&result); - prf_p_hash(master_secret, &seed, 12, h) + prf_p_hash(crypto, master_secret, &seed, 12, h) } pub(crate) fn prf_verify_data_client( + crypto: &dyn RTCCrypto, master_secret: &[u8], handshake_bodies: &[u8], h: CipherSuiteHash, ) -> Result> { prf_verify_data( + crypto, master_secret, handshake_bodies, PRF_VERIFY_DATA_CLIENT_LABEL, @@ -277,11 +247,13 @@ pub(crate) fn prf_verify_data_client( } pub(crate) fn prf_verify_data_server( + crypto: &dyn RTCCrypto, master_secret: &[u8], handshake_bodies: &[u8], h: CipherSuiteHash, ) -> Result> { prf_verify_data( + crypto, master_secret, handshake_bodies, PRF_VERIFY_DATA_SERVER_LABEL, @@ -290,16 +262,18 @@ pub(crate) fn prf_verify_data_server( } // compute the MAC using HMAC-SHA1 +/// Computes the TLS 1.2 record MAC (RFC 5246 section 6.2.3.1) with an already-keyed MAC. +/// +/// Takes `&mut dyn Mac` rather than a crypto handle plus raw key bytes so the caller keys once +/// per epoch. Re-deriving the HMAC key schedule per record measured ~2x on the CBC record path. pub(crate) fn prf_mac( + mac: &mut dyn Mac, epoch: u16, sequence_number: u64, content_type: ContentType, protocol_version: ProtocolVersion, payload: &[u8], - key: &[u8], ) -> Result> { - let mut hmac = HmacSha1::new_from_slice(key).map_err(|e| Error::Other(e.to_string()))?; - let mut msg = vec![0u8; 13]; msg[..2].copy_from_slice(&epoch.to_be_bytes()); msg[2..8].copy_from_slice(&sequence_number.to_be_bytes()[2..]); @@ -308,9 +282,8 @@ pub(crate) fn prf_mac( msg[10] = protocol_version.minor; msg[11..].copy_from_slice(&(payload.len() as u16).to_be_bytes()); - hmac.update(&msg); - hmac.update(payload); - let result = hmac.finalize(); - - Ok(result.into_bytes().to_vec()) + let mut output = vec![0; mac.output_len()]; + mac.sign(&[&msg, payload], &mut output) + .map_err(|error| Error::Crypto(error.to_string()))?; + Ok(output) } diff --git a/rtc-dtls/src/prf/prf_test.rs b/rtc-dtls/src/prf/prf_test.rs index 33d5241f..7b479a06 100644 --- a/rtc-dtls/src/prf/prf_test.rs +++ b/rtc-dtls/src/prf/prf_test.rs @@ -1,33 +1,44 @@ use super::*; use crate::cipher_suite::CipherSuiteHash; +#[cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] +use crypto::RTCCryptoProvider; #[test] fn test_pre_master_secret() -> Result<()> { - let private_key: [u8; 32] = [ - 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, - 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, - 0x3e, 0x3f, - ]; - let private_key = - NamedCurvePrivateKey::StaticSecretX25519(x25519_dalek::StaticSecret::from(private_key)); - let public_key = [ - 0x9f, 0xd7, 0xad, 0x6d, 0xcf, 0xf4, 0x29, 0x8d, 0xd3, 0xf9, 0x6d, 0x5b, 0x1b, 0x2a, 0xf9, - 0x10, 0xa0, 0x53, 0x5b, 0x14, 0x88, 0xd7, 0xf8, 0xfa, 0xbb, 0x34, 0x9a, 0x98, 0x28, 0x80, - 0xb6, 0x15, - ]; + let provider = crypto::default_provider().unwrap(); + for curve in [NamedCurve::P256, NamedCurve::P384, NamedCurve::X25519] { + let mut left = curve.generate_keypair_with_crypto(provider.crypto())?; + let mut right = curve.generate_keypair_with_crypto(provider.crypto())?; + let left_public = left.public_key.clone(); + let right_public = right.public_key.clone(); + let left_secret = prf_pre_master_secret(&right_public, &mut left, curve)?; + let right_secret = prf_pre_master_secret(&left_public, &mut right, curve)?; + assert_eq!(left_secret, right_secret); + assert!(!left_secret.is_empty()); + assert!(matches!( + left.complete(&right_public), + Err(Error::ErrNamedCurveAndPrivateKeyMismatch) + )); + } - let expected_pre_master_secret = vec![ - 0xdf, 0x4a, 0x29, 0x1b, 0xaa, 0x1e, 0xb7, 0xcf, 0xa6, 0x93, 0x4b, 0x29, 0xb4, 0x74, 0xba, - 0xad, 0x26, 0x97, 0xe2, 0x9f, 0x1f, 0x92, 0x0d, 0xcc, 0x77, 0xc8, 0xa0, 0xa0, 0x88, 0x44, - 0x76, 0x24, - ]; + Ok(()) +} - let pre_master_secret = prf_pre_master_secret(&public_key, &private_key, NamedCurve::X25519)?; +#[cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] +#[test] +fn test_cross_provider_pre_master_secret() -> Result<()> { + let ring = crypto::providers::RingProvider::new(); + let aws = crypto::providers::AwsLcRsProvider::new(); - assert_eq!( - expected_pre_master_secret, pre_master_secret, - "PremasterSecret exp: {expected_pre_master_secret:?} actual: {pre_master_secret:?}" - ); + for curve in [NamedCurve::P256, NamedCurve::P384, NamedCurve::X25519] { + let mut left = curve.generate_keypair_with_crypto(ring.crypto())?; + let mut right = curve.generate_keypair_with_crypto(aws.crypto())?; + let left_public = left.public_key.clone(); + let right_public = right.public_key.clone(); + let left_secret = prf_pre_master_secret(&right_public, &mut left, curve)?; + let right_secret = prf_pre_master_secret(&left_public, &mut right, curve)?; + assert_eq!(left_secret, right_secret); + } Ok(()) } @@ -57,6 +68,7 @@ fn test_master_secret() -> Result<()> { ]; let master_secret = prf_master_secret( + crypto::default_provider().unwrap().crypto(), &pre_master_secret, &client_random, &server_random, @@ -107,12 +119,15 @@ fn test_encryption_keys() -> Result<()> { }; let keys = prf_encryption_keys( + crypto::default_provider().unwrap().crypto(), &master_secret, &client_random, &server_random, - 0, - 16, - 4, + EncryptionKeyLengths { + mac: 0, + key: 16, + iv: 4, + }, CipherSuiteHash::Sha256, )?; @@ -250,7 +265,12 @@ fn test_verify_data() -> Result<()> { 0xcf, 0x91, 0x96, 0x26, 0xf1, 0x36, 0x0c, 0x53, 0x6a, 0xaa, 0xd7, 0x3a, ]; - let verify_data = prf_verify_data_client(&master_secret, &final_msg, CipherSuiteHash::Sha256)?; + let verify_data = prf_verify_data_client( + crypto::default_provider().unwrap().crypto(), + &master_secret, + &final_msg, + CipherSuiteHash::Sha256, + )?; assert_eq!( expected_verify_data, verify_data, diff --git a/rtc-dtls/src/record_layer/mod.rs b/rtc-dtls/src/record_layer/mod.rs index 3438a597..b20c81bd 100644 --- a/rtc-dtls/src/record_layer/mod.rs +++ b/rtc-dtls/src/record_layer/mod.rs @@ -1,3 +1,4 @@ +/// The record header: content type, version, epoch and sequence number. pub mod record_layer_header; #[cfg(test)] @@ -35,11 +36,14 @@ use std::io::{Read, Write}; /// [RFC 4347 §4.1]: https://tools.ietf.org/html/rfc4347#section-4.1 #[derive(Debug, Clone, PartialEq)] pub struct RecordLayer { + /// The record's header. pub record_layer_header: RecordLayerHeader, + /// The record's parsed body. pub content: Content, } impl RecordLayer { + /// Builds a record around `content`, filling in its header. pub fn new(protocol_version: ProtocolVersion, epoch: u16, content: Content) -> Self { RecordLayer { record_layer_header: RecordLayerHeader { @@ -53,12 +57,22 @@ impl RecordLayer { } } + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { self.record_layer_header.marshal(writer)?; self.content.marshal(writer)?; Ok(()) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let record_layer_header = RecordLayerHeader::unmarshal(reader)?; let content = match record_layer_header.content_type { diff --git a/rtc-dtls/src/record_layer/record_layer_header.rs b/rtc-dtls/src/record_layer/record_layer_header.rs index 82413658..ceb7db01 100644 --- a/rtc-dtls/src/record_layer/record_layer_header.rs +++ b/rtc-dtls/src/record_layer/record_layer_header.rs @@ -1,3 +1,12 @@ +//! The DTLS record header. +//! +//! Thirteen bytes: content type, protocol version, a 16-bit epoch, a 48-bit sequence number, and +//! the body length. The epoch is what DTLS adds over TLS here — it increments on every +//! ChangeCipherSpec, so records protected with the old and new keys can be told apart while a +//! rekey is in flight. +//! +//! The sequence number is 48 bits on the wire but held as a `u64`; [`MAX_SEQUENCE_NUMBER`](crate::record_layer::record_layer_header::MAX_SEQUENCE_NUMBER) is the +//! largest value that fits. use crate::content::*; use shared::error::*; @@ -5,23 +14,32 @@ use shared::error::*; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use std::io::{Read, Write}; +/// Length of the DTLS record header in bytes. pub const RECORD_LAYER_HEADER_SIZE: usize = 13; +/// The largest sequence number the 48-bit field can hold. pub const MAX_SEQUENCE_NUMBER: u64 = 0x0000FFFFFFFFFFFF; +/// Major version byte for DTLS 1.2. pub const DTLS1_2MAJOR: u8 = 0xfe; +/// Minor version byte for DTLS 1.2. pub const DTLS1_2MINOR: u8 = 0xfd; +/// Major version byte for DTLS 1.0. pub const DTLS1_0MAJOR: u8 = 0xfe; +/// Minor version byte for DTLS 1.0. pub const DTLS1_0MINOR: u8 = 0xff; // VERSION_DTLS12 is the DTLS version in the same style as // VersionTLSXX from crypto/tls +/// DTLS 1.2 as a single 16-bit value. pub const VERSION_DTLS12: u16 = 0xfefd; +/// DTLS 1.0 as a [`ProtocolVersion`]. pub const PROTOCOL_VERSION1_0: ProtocolVersion = ProtocolVersion { major: DTLS1_0MAJOR, minor: DTLS1_0MINOR, }; +/// DTLS 1.2 as a [`ProtocolVersion`]. pub const PROTOCOL_VERSION1_2: ProtocolVersion = ProtocolVersion { major: DTLS1_2MAJOR, minor: DTLS1_2MINOR, @@ -34,20 +52,33 @@ pub const PROTOCOL_VERSION1_2: ProtocolVersion = ProtocolVersion { /// [RFC 4346 §6.2.1]: https://tools.ietf.org/html/rfc4346#section-6.2.1 #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] pub struct ProtocolVersion { + /// The major version byte. pub major: u8, + /// The minor version byte. pub minor: u8, } #[derive(Copy, Clone, PartialEq, Eq, Debug, Default)] +/// The header on every DTLS record. pub struct RecordLayerHeader { + /// What the record body holds. pub content_type: ContentType, + /// The record's protocol version. pub protocol_version: ProtocolVersion, + /// The key epoch, incremented on each ChangeCipherSpec so old and new keys can coexist. pub epoch: u16, + /// The record sequence number — a 48-bit field on the wire. pub sequence_number: u64, // uint48 in spec + /// The body length in bytes. pub content_len: u16, } impl RecordLayerHeader { + /// Encodes this message to `writer`. + /// + /// # Errors + /// + /// Fails on a write error, or if a field exceeds the length its wire format allows. pub fn marshal(&self, writer: &mut W) -> Result<()> { if self.sequence_number > MAX_SEQUENCE_NUMBER { return Err(Error::ErrSequenceNumberOverflow); @@ -66,6 +97,11 @@ impl RecordLayerHeader { Ok(writer.flush()?) } + /// Decodes one of these messages from `reader`. + /// + /// # Errors + /// + /// Fails if `reader` is truncated or its contents are not a valid encoding. pub fn unmarshal(reader: &mut R) -> Result { let content_type = reader.read_u8()?.into(); let major = reader.read_u8()?; diff --git a/rtc-dtls/src/signature_hash_algorithm/mod.rs b/rtc-dtls/src/signature_hash_algorithm/mod.rs index f16455d5..e1006411 100644 --- a/rtc-dtls/src/signature_hash_algorithm/mod.rs +++ b/rtc-dtls/src/signature_hash_algorithm/mod.rs @@ -10,15 +10,26 @@ use shared::error::*; // https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-18 // Supported hash hash algorithms #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// The hash algorithms that may be paired with a signature algorithm. +#[non_exhaustive] pub enum HashAlgorithm { - Md2 = 0, // Blacklisted - Md5 = 1, // Blacklisted + /// `MD2` (`0`). + Md2 = 0, // Blacklisted + /// `MD5` (`1`). + Md5 = 1, // Blacklisted + /// `SHA1` (`2`). Sha1 = 2, // Blacklisted + /// `SHA224` (`3`). Sha224 = 3, + /// `SHA256` (`4`). Sha256 = 4, + /// `SHA384` (`5`). Sha384 = 5, + /// `SHA512` (`6`). Sha512 = 6, + /// `ED25519` (`8`). Ed25519 = 8, + /// An algorithm this crate does not implement. Unsupported, } @@ -69,10 +80,16 @@ impl HashAlgorithm { // https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-16 #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// The signature algorithms this crate can verify and produce. +#[non_exhaustive] pub enum SignatureAlgorithm { + /// `RSA` (`1`). Rsa = 1, + /// `ECDSA` (`3`). Ecdsa = 3, + /// `ED25519` (`7`). Ed25519 = 7, + /// An algorithm this crate does not implement. Unsupported, } @@ -88,20 +105,44 @@ impl From for SignatureAlgorithm { } #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// A signature and hash pair, as negotiated for certificate verification. pub struct SignatureHashAlgorithm { + /// The hash to digest the signed data with. pub hash: HashAlgorithm, + /// The signature algorithm to apply. pub signature: SignatureAlgorithm, } impl SignatureHashAlgorithm { + pub(crate) fn crypto_scheme(&self) -> Result { + match (self.signature, self.hash) { + (SignatureAlgorithm::Ed25519, _) => Ok(crypto::SignatureScheme::Ed25519), + (SignatureAlgorithm::Ecdsa, HashAlgorithm::Sha256) => { + Ok(crypto::SignatureScheme::EcdsaP256Sha256) + } + (SignatureAlgorithm::Ecdsa, HashAlgorithm::Sha384) => { + Ok(crypto::SignatureScheme::EcdsaP384Sha384) + } + (SignatureAlgorithm::Rsa, HashAlgorithm::Sha1) => { + Ok(crypto::SignatureScheme::RsaPkcs1Sha1) + } + (SignatureAlgorithm::Rsa, HashAlgorithm::Sha256) => { + Ok(crypto::SignatureScheme::RsaPkcs1Sha256) + } + (SignatureAlgorithm::Rsa, HashAlgorithm::Sha384) => { + Ok(crypto::SignatureScheme::RsaPkcs1Sha384) + } + (SignatureAlgorithm::Rsa, HashAlgorithm::Sha512) => { + Ok(crypto::SignatureScheme::RsaPkcs1Sha512) + } + _ => Err(Error::ErrKeySignatureVerifyUnimplemented), + } + } + // is_compatible checks that given private key is compatible with the signature scheme. pub(crate) fn is_compatible(&self, private_key: &CryptoPrivateKey) -> bool { - match &private_key.kind { - CryptoPrivateKeyKind::Ed25519(_) => self.signature == SignatureAlgorithm::Ed25519, - CryptoPrivateKeyKind::Ecdsa256(_) => self.signature == SignatureAlgorithm::Ecdsa, - CryptoPrivateKeyKind::Rsa256(_) => self.signature == SignatureAlgorithm::Rsa, - CryptoPrivateKeyKind::Custom(_) => true, - } + self.crypto_scheme() + .is_ok_and(|scheme| private_key.signing_key.supports(scheme)) } } @@ -155,27 +196,41 @@ pub(crate) fn select_signature_scheme( // SignatureScheme identifies a signature algorithm supported by TLS. See // RFC 8446, Section 4.2.3. #[derive(Copy, Clone, Debug, PartialEq, Eq)] +/// A TLS signature scheme, which names a signature and hash together ([RFC 8446] §4.2.3). +#[non_exhaustive] pub enum SignatureScheme { // RSASSA-PKCS1-v1_5 algorithms. + /// `PKCS1_WITH_SHA256` (`0x0401`). Pkcs1WithSha256 = 0x0401, + /// `PKCS1_WITH_SHA384` (`0x0501`). Pkcs1WithSha384 = 0x0501, + /// `PKCS1_WITH_SHA512` (`0x0601`). Pkcs1WithSha512 = 0x0601, // RSASSA-PSS algorithms with public key OID rsaEncryption. + /// `PSS_WITH_SHA256` (`0x0804`). PssWithSha256 = 0x0804, + /// `PSS_WITH_SHA384` (`0x0805`). PssWithSha384 = 0x0805, + /// `PSS_WITH_SHA512` (`0x0806`). PssWithSha512 = 0x0806, // ECDSA algorithms. Only constrained to a specific curve in TLS 1.3. + /// `ECDSA_WITH_P256_AND_SHA256` (`0x0403`). EcdsaWithP256AndSha256 = 0x0403, + /// `ECDSA_WITH_P384_AND_SHA384` (`0x0503`). EcdsaWithP384AndSha384 = 0x0503, + /// `ECDSA_WITH_P521_AND_SHA512` (`0x0603`). EcdsaWithP521AndSha512 = 0x0603, // EdDSA algorithms. + /// `ED25519` (`0x0807`). Ed25519 = 0x0807, // Legacy signature and hash algorithms for TLS 1.2. + /// `PKCS1_WITH_SHA1` (`0x0201`). Pkcs1WithSha1 = 0x0201, + /// `ECDSA_WITH_SHA1` (`0x0203`). EcdsaWithSha1 = 0x0203, } diff --git a/rtc-dtls/src/state.rs b/rtc-dtls/src/state.rs index 8ecf653c..409a79ef 100644 --- a/rtc-dtls/src/state.rs +++ b/rtc-dtls/src/state.rs @@ -4,13 +4,18 @@ use super::curve::named_curve::*; use super::extension::extension_use_srtp::SrtpProtectionProfile; use super::handshake::handshake_random::*; use super::prf::*; +use crypto::RTCCryptoProvider; +use crypto::SecretVec; use rkyv::{Archive, Deserialize, Serialize}; -use shared::crypto::KeyingMaterialExporter; use shared::error::*; use std::io::{BufWriter, Cursor}; +use std::sync::Arc; // State holds the dtls connection state and implements both encoding.BinaryMarshaler and encoding.BinaryUnmarshaler +/// The negotiated connection state: keys, sequence numbers, peer identity and the active +/// cipher suite. pub struct State { + pub(crate) crypto_provider: Arc, pub(crate) local_epoch: u16, pub(crate) remote_epoch: u16, pub(crate) local_sequence_number: Vec, // uint48 @@ -20,7 +25,11 @@ pub struct State { pub(crate) cipher_suite: Option>, // nil if a cipher_suite hasn't been chosen pub(crate) srtp_protection_profile: SrtpProtectionProfile, // Negotiated srtp_protection_profile + /// The peer's certificate chain, DER-encoded. + /// + /// WebRTC checks its fingerprint against the one signalled in SDP. pub peer_certificates: Vec>, + /// The PSK identity hint, for pre-shared-key handshakes. pub identity_hint: Vec, pub(crate) is_client: bool, @@ -57,22 +66,28 @@ struct SerializedState { is_client: bool, } -impl Default for State { - fn default() -> Self { +impl State { + /// Creates the initial handshake state for a connection. + /// + /// The crypto provider comes from the caller — `rtc-dtls` never resolves a default. This + /// replaces the former `Default` impl, which resolved one implicitly only for `DTLSConn::new` + /// to overwrite it a few lines later. + pub fn new(crypto_provider: Arc, is_client: bool) -> Self { State { + crypto_provider, local_epoch: 0, remote_epoch: 0, local_sequence_number: vec![], local_random: HandshakeRandom::default(), remote_random: HandshakeRandom::default(), master_secret: vec![], - cipher_suite: None, // nil if a cipher_suite hasn't been chosen + cipher_suite: None, - srtp_protection_profile: SrtpProtectionProfile::Unsupported, // Negotiated srtp_protection_profile + srtp_protection_profile: SrtpProtectionProfile::Unsupported, peer_certificates: vec![], identity_hint: vec![], - is_client: false, + is_client, pre_master_secret: vec![], extended_master_secret: false, @@ -83,12 +98,11 @@ impl Default for State { handshake_send_sequence: 0, handshake_recv_sequence: 0, server_name: "".to_string(), - remote_requested_certificate: false, // Did we get a CertificateRequest - local_certificates_verify: vec![], // cache CertificateVerify - local_verify_data: vec![], // cached VerifyData - local_key_signature: vec![], // cached keySignature + remote_requested_certificate: false, + local_certificates_verify: vec![], + local_verify_data: vec![], + local_key_signature: vec![], peer_certificates_verified: false, - //replay_detector: vec![], } } } @@ -174,6 +188,11 @@ impl State { Ok(()) } + /// Installs the negotiated keys into the cipher suite. + /// + /// # Errors + /// + /// Fails if the master secret or randoms are not yet available. pub fn init_cipher_suite(&mut self) -> Result<()> { if let Some(cipher_suite) = &mut self.cipher_suite { if cipher_suite.is_initialized() { @@ -192,9 +211,21 @@ impl State { } if self.is_client { - cipher_suite.init(&self.master_secret, &local_random, &remote_random, true) + cipher_suite.init( + self.crypto_provider.clone(), + &self.master_secret, + &local_random, + &remote_random, + true, + ) } else { - cipher_suite.init(&self.master_secret, &remote_random, &local_random, false) + cipher_suite.init( + self.crypto_provider.clone(), + &self.master_secret, + &remote_random, + &local_random, + false, + ) } } else { Err(Error::ErrCipherSuiteUnset) @@ -202,6 +233,11 @@ impl State { } // marshal_binary is a binary.BinaryMarshaler.marshal_binary implementation + /// Serializes the state, so a connection can be resumed or migrated. + /// + /// # Errors + /// + /// Fails if the state is incomplete. pub fn marshal_binary(&self) -> Result> { let serialized = self.serialize()?; @@ -212,6 +248,11 @@ impl State { } // unmarshal_binary is a binary.BinaryUnmarshaler.unmarshal_binary implementation + /// Restores state previously produced by [`Self::marshal_binary`]. + /// + /// # Errors + /// + /// Fails if `data` is truncated or malformed. pub fn unmarshal_binary(&mut self, data: &[u8]) -> Result<()> { let serialized: SerializedState = match rkyv::access::(data) @@ -226,30 +267,34 @@ impl State { Ok(()) } + /// The SRTP protection profile negotiated through `use_srtp`. pub fn srtp_protection_profile(&self) -> SrtpProtectionProfile { self.srtp_protection_profile } + /// Whether this endpoint took the client role. pub fn is_client(&self) -> bool { self.is_client } + /// The active cipher suite, once one has been negotiated. pub fn cipher_suite(&self) -> Option<&dyn CipherSuite> { self.cipher_suite.as_deref() } -} -impl KeyingMaterialExporter for State { - /// export_keying_material returns length bytes of exported key material in a new - /// slice as defined in RFC 5705. - /// This allows protocols to use DTLS for key establishment, but - /// then use some of the keying material for their own purposes - fn export_keying_material( + /// Exports `length` bytes of keying material from an established session, as defined in + /// RFC 5705. + /// + /// # Errors + /// + /// Returns an error before the handshake completes, when `context` is non-empty, when the + /// label is reserved by TLS, or when the negotiated cipher suite is unavailable. + pub fn export_keying_material( &self, label: &str, context: &[u8], length: usize, - ) -> shared::error::Result> { + ) -> Result { if self.local_epoch == 0 { return Err(Error::HandshakeInProgress); } else if !context.is_empty() { @@ -279,8 +324,15 @@ impl KeyingMaterialExporter for State { } if let Some(cipher_suite) = &self.cipher_suite { - match prf_p_hash(&self.master_secret, &seed, length, cipher_suite.hash_func()) { - Ok(v) => Ok(v), + let provider = self.crypto_provider.as_ref(); + match prf_p_hash( + provider.crypto(), + &self.master_secret, + &seed, + length, + cipher_suite.hash_func(), + ) { + Ok(v) => Ok(SecretVec::new(v)), Err(err) => Err(Error::Hash(err.to_string())), } } else { diff --git a/rtc-ice/Cargo.toml b/rtc-ice/Cargo.toml index f25fc52e..3ac093e5 100644 --- a/rtc-ice/Cargo.toml +++ b/rtc-ice/Cargo.toml @@ -12,37 +12,36 @@ keywords.workspace = true categories.workspace = true [features] -default = ["ring"] -ring = ["stun/ring"] -aws-lc-rs = ["stun/aws-lc-rs"] +default = ["crypto-ring"] +crypto-ring = ["crypto/crypto-ring", "stun/crypto-ring"] +crypto-aws-lc-rs = ["crypto/crypto-aws-lc-rs", "stun/crypto-aws-lc-rs"] [dependencies] shared = { workspace = true, default-features = false, features = [] } sansio.workspace = true stun.workspace = true mdns.workspace = true +crypto.workspace = true -crc = "3.0.1" +crc.workspace = true log.workspace = true -rand.workspace = true serde.workspace = true -url = "2.5.0" -uuid = { version = "1", features = ["v4"] } +url.workspace = true +uuid.workspace = true bytes.workspace = true [dev-dependencies] -regex = "1.10.3" +regex.workspace = true env_logger.workspace = true chrono.workspace = true -ipnet = "2.9.0" +ipnet.workspace = true clap.workspace = true -lazy_static = "1.4.0" -hyper = { version = "0.14.28", features = ["full"] } -sha1 = "0.10.6" -waitgroup = "0.1.2" -serde_json = "1.0.114" +lazy_static.workspace = true +hyper.workspace = true +waitgroup.workspace = true +serde_json.workspace = true tokio.workspace = true -futures = "0.3.30" +futures.workspace = true ctrlc.workspace = true [[example]] diff --git a/rtc-ice/examples/ping_pong.rs b/rtc-ice/examples/ping_pong.rs index 55a642cf..8e3dc419 100644 --- a/rtc-ice/examples/ping_pong.rs +++ b/rtc-ice/examples/ping_pong.rs @@ -7,6 +7,7 @@ use rtc_ice::agent::Agent; use rtc_ice::agent::agent_config::AgentConfig; use rtc_ice::candidate::candidate_host::CandidateHostConfig; use rtc_ice::candidate::*; +use rtc_ice::crypto; use rtc_ice::state::*; use rtc_ice::{Credentials, Event}; use sansio::Protocol; @@ -163,11 +164,14 @@ async fn main() -> Result<(), Error> { let port = if cli.controlling { 4000 } else { 4001 }; let udp_socket = UdpSocket::bind(("0.0.0.0", port)).await?; - let mut ice_agent = Agent::new(Arc::new(AgentConfig { - disconnected_timeout: Some(Duration::from_secs(5)), - failed_timeout: Some(Duration::from_secs(5)), - ..Default::default() - }))?; + let mut ice_agent = Agent::new( + Arc::new(AgentConfig { + disconnected_timeout: Some(Duration::from_secs(5)), + failed_timeout: Some(Duration::from_secs(5)), + ..Default::default() + }), + crypto::default_provider().map_err(|e| Error::Other(e.to_string()))?, + )?; let client = Arc::new(Client::new()); diff --git a/rtc-ice/src/agent/agent_config.rs b/rtc-ice/src/agent/agent_config.rs index 59808394..f6b93c99 100644 --- a/rtc-ice/src/agent/agent_config.rs +++ b/rtc-ice/src/agent/agent_config.rs @@ -51,6 +51,7 @@ pub(crate) fn default_candidate_types() -> Vec { /// future-proofness of the interface. #[derive(Default)] pub struct AgentConfig { + /// The STUN and TURN servers to gather reflexive and relay candidates from. pub urls: Vec, /// It is used to perform connectivity checks. The values MUST be unguessable, with at least @@ -104,6 +105,7 @@ pub struct AgentConfig { /// request or a nomination we set the pair as failed. pub max_binding_requests: Option, + /// Whether this agent takes the controlling role. pub is_controlling: bool, /// lite agents do not perform connectivity check and only provide host candidates. diff --git a/rtc-ice/src/agent/agent_proto.rs b/rtc-ice/src/agent/agent_proto.rs index c85ef9d4..f90c40f0 100644 --- a/rtc-ice/src/agent/agent_proto.rs +++ b/rtc-ice/src/agent/agent_proto.rs @@ -36,6 +36,7 @@ impl sansio::Protocol for Agent { error!("mDNS Query {} timed out for {}", id, c.address()); } } + _ => {} } } remote_candidates @@ -113,6 +114,7 @@ impl sansio::Protocol for Agent { error!("mDNS Query {} timed out for {}", id, c.address()); } } + _ => {} } } remote_candidates diff --git a/rtc-ice/src/agent/agent_selector.rs b/rtc-ice/src/agent/agent_selector.rs index 1610804e..06270129 100644 --- a/rtc-ice/src/agent/agent_selector.rs +++ b/rtc-ice/src/agent/agent_selector.rs @@ -113,8 +113,9 @@ impl Agent { Box::::default(), Box::new(AttrControlling(self.tie_breaker)), Box::new(PriorityAttr(pair.local_priority)), - Box::new(MessageIntegrity::new_short_term_integrity( + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( remote_credentials.pwd.clone(), + self.crypto_provider.crypto(), )), Box::new(FINGERPRINT), ]); @@ -280,8 +281,9 @@ impl ControllingSelector for Agent { Box::new(Username::new(ATTR_USERNAME, username)), Box::new(AttrControlling(self.tie_breaker)), Box::new(PriorityAttr(self.local_candidates[local_index].priority())), - Box::new(MessageIntegrity::new_short_term_integrity( + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( remote_credentials.pwd.clone(), + self.crypto_provider.crypto(), )), Box::new(FINGERPRINT), ]); @@ -441,8 +443,9 @@ impl ControlledSelector for Agent { Box::new(Username::new(ATTR_USERNAME, username)), Box::new(AttrControlled(self.tie_breaker)), Box::new(PriorityAttr(self.local_candidates[local_index].priority())), - Box::new(MessageIntegrity::new_short_term_integrity( + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( remote_credentials.pwd.clone(), + self.crypto_provider.crypto(), )), Box::new(FINGERPRINT), ]); diff --git a/rtc-ice/src/agent/agent_stats.rs b/rtc-ice/src/agent/agent_stats.rs index f68481ac..944767ca 100644 --- a/rtc-ice/src/agent/agent_stats.rs +++ b/rtc-ice/src/agent/agent_stats.rs @@ -7,6 +7,7 @@ use crate::network_type::NetworkType; /// Contains ICE candidate pair statistics. pub struct CandidatePairStats { /// The timestamp associated with this struct. + /// When this snapshot was taken. pub timestamp: Instant, /// The id of the local candidate. @@ -148,7 +149,7 @@ impl Default for CandidatePairStats { /// Contains ICE candidate statistics related to the `ICETransport` objects. #[derive(Debug, Clone)] pub struct CandidateStats { - // The timestamp associated with this struct. + /// The timestamp associated with this struct. pub timestamp: Instant, /// The candidate id. diff --git a/rtc-ice/src/agent/agent_test.rs b/rtc-ice/src/agent/agent_test.rs index 5746d5f4..5768bc50 100644 --- a/rtc-ice/src/agent/agent_test.rs +++ b/rtc-ice/src/agent/agent_test.rs @@ -12,10 +12,16 @@ use crate::candidate::candidate_relay::CandidateRelayConfig; use crate::candidate::candidate_server_reflexive::*; use crate::candidate::*; +/// Explicit provider for tests. The default-resolving STUN constructors were removed before 1.0, +/// so every `MessageIntegrity` now names its provider. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + #[test] fn test_pair_search() -> Result<()> { let config = Arc::new(AgentConfig::default()); - let mut a = Agent::new(config)?; + let mut a = Agent::new(config, test_crypto_provider())?; assert!( a.candidate_pairs.is_empty(), @@ -32,7 +38,7 @@ fn test_pair_search() -> Result<()> { #[test] fn test_pair_priority() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; let host_config = CandidateHostConfig { base_config: CandidateConfig { @@ -154,7 +160,7 @@ fn pipe( }; cfg0.urls = vec![]; - let a_agent = Agent::new(Arc::new(cfg0))?; + let a_agent = Agent::new(Arc::new(cfg0), test_crypto_provider())?; let mut cfg1 = if let Some(cfg) = default_config1 { cfg @@ -163,14 +169,14 @@ fn pipe( }; cfg1.urls = vec![]; - let b_agent = Agent::new(Arc::new(cfg1))?; + let b_agent = Agent::new(Arc::new(cfg1), test_crypto_provider())?; Ok((a_agent, b_agent)) } #[test] fn test_on_selected_candidate_pair_change() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; let host_config = CandidateHostConfig { base_config: CandidateConfig { @@ -221,7 +227,7 @@ fn test_on_selected_candidate_pair_change() -> Result<()> { #[test] fn test_handle_peer_reflexive_udp_pflx_candidate() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; let host_config = CandidateHostConfig { base_config: CandidateConfig { @@ -262,7 +268,10 @@ fn test_handle_peer_reflexive_udp_pflx_candidate() -> Result<()> { Box::new(UseCandidateAttr::new()), Box::new(AttrControlling(tie_breaker)), Box::new(PriorityAttr(local_priority)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + test_crypto_provider().crypto(), + )), Box::new(FINGERPRINT), ])?; @@ -304,7 +313,7 @@ fn test_handle_peer_reflexive_udp_pflx_candidate() -> Result<()> { #[test] fn test_handle_peer_reflexive_unknown_remote() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; let mut tid = TransactionId::default(); tid.0[..3].copy_from_slice("ABC".as_bytes()); @@ -343,7 +352,10 @@ fn test_handle_peer_reflexive_unknown_remote() -> Result<()> { msg.build(&[ Box::new(BINDING_SUCCESS), Box::new(tid), - Box::new(MessageIntegrity::new_short_term_integrity(remote_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + remote_pwd, + test_crypto_provider().crypto(), + )), Box::new(FINGERPRINT), ])?; @@ -422,7 +434,7 @@ fn test_connectivity_on_startup() -> Result<()> { ..Default::default() }; - let mut a_agent = Agent::new(cfg0)?; + let mut a_agent = Agent::new(cfg0, test_crypto_provider())?; let cfg1 = AgentConfig { keepalive_interval, @@ -430,7 +442,7 @@ fn test_connectivity_on_startup() -> Result<()> { ..Default::default() }; - let mut b_agent = Agent::new(cfg1)?; + let mut b_agent = Agent::new(cfg1, test_crypto_provider())?; // Manual signaling let (a_ufrag, a_pwd) = a_agent.get_local_user_credentials(); @@ -493,7 +505,7 @@ fn test_connectivity_lite() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); a_agent.on_connection_state_change(a_notifier); let cfg1 = AgentConfig { @@ -505,7 +517,7 @@ fn test_connectivity_lite() -> Result<()> { ..Default::default() }; - let b_agent = Arc::new(Agent::new(cfg1)?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); b_agent.on_connection_state_change(b_notifier); let _ = connect_with_vnet(&a_agent, &b_agent)?; @@ -563,7 +575,7 @@ fn build_msg(c: MessageClass, username: String, key: String) -> Result Box::new(MessageType::new(METHOD_BINDING, c)), Box::new(TransactionId::new()), Box::new(Username::new(ATTR_USERNAME, username)), - Box::new(MessageIntegrity::new_short_term_integrity(key)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider(key, test_crypto_provider().crypto())), Box::new(FINGERPRINT), ])?; Ok(msg) @@ -745,7 +757,7 @@ fn test_inbound_validity() -> Result<()> { Box::new(BINDING_REQUEST), Box::new(TransactionId::new()), Box::new(Username::new(ATTR_USERNAME, username)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider(local_pwd, test_crypto_provider())), ])?; a.internal.handle_inbound(&mut msg, &local, remote); @@ -779,7 +791,7 @@ fn test_inbound_validity() -> Result<()> { msg.build(&[ Box::new(BINDING_SUCCESS), Box::new(t_id), - Box::new(MessageIntegrity::new_short_term_integrity(remote_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider(remote_pwd, test_crypto_provider())), Box::new(FINGERPRINT), ])?; @@ -885,8 +897,8 @@ fn test_connection_state_callback() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); - let b_agent = Arc::new(Agent::new(cfg1)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); let (is_checking_tx, mut is_checking_rx) = mpsc::channel::<()>(1); let (is_connected_tx, mut is_connected_rx) = mpsc::channel::<()>(1); @@ -1478,8 +1490,8 @@ fn test_connection_state_failed_delete_all_candidates() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); - let b_agent = Arc::new(Agent::new(cfg1)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); let (is_failed_tx, mut is_failed_rx) = mpsc::channel::<()>(1); let is_failed_tx = Arc::new(Mutex::new(Some(is_failed_tx))); @@ -1532,8 +1544,8 @@ fn test_connection_state_connecting_to_failed() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); - let b_agent = Arc::new(Agent::new(cfg1)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); let is_failed = WaitGroup::new(); let is_checking = WaitGroup::new(); @@ -1792,8 +1804,8 @@ fn test_close_in_connection_state_callback() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); - let b_agent = Arc::new(Agent::new(cfg1)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); let (is_closed_tx, mut is_closed_rx) = mpsc::channel::<()>(1); let (is_connected_tx, mut is_connected_rx) = mpsc::channel::<()>(1); @@ -1849,8 +1861,8 @@ fn test_run_task_in_connection_state_callback() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); - let b_agent = Arc::new(Agent::new(cfg1)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); let (is_complete_tx, mut is_complete_rx) = mpsc::channel::<()>(1); let is_complete_tx = Arc::new(Mutex::new(Some(is_complete_tx))); @@ -1901,8 +1913,8 @@ fn test_run_task_in_selected_candidate_pair_change_callback() -> Result<()> { ..Default::default() }; - let a_agent = Arc::new(Agent::new(cfg0)?); - let b_agent = Arc::new(Agent::new(cfg1)?); + let a_agent = Arc::new(Agent::new(cfg0, test_crypto_provider())?); + let b_agent = Arc::new(Agent::new(cfg1, test_crypto_provider())?); let (is_tested_tx, mut is_tested_rx) = mpsc::channel::<()>(1); let is_tested_tx = Arc::new(Mutex::new(Some(is_tested_tx))); @@ -2016,7 +2028,7 @@ fn test_role_conflict_both_controlling_smaller_tiebreaker_switches() -> Result<( // Create agent with controlling role let mut config = AgentConfig::default(); config.is_controlling = true; - let mut agent = Agent::new(Arc::new(config))?; + let mut agent = Agent::new(Arc::new(config), test_crypto_provider())?; // Set a specific tiebreaker value agent.tie_breaker = 100; @@ -2069,7 +2081,10 @@ fn test_role_conflict_both_controlling_smaller_tiebreaker_switches() -> Result<( Box::new(Username::new(ATTR_USERNAME, username)), Box::new(AttrControlling(remote_tiebreaker)), // Remote is also controlling Box::new(PriorityAttr(1000)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + test_crypto_provider().crypto(), + )), Box::new(FINGERPRINT), ])?; @@ -2134,7 +2149,7 @@ fn test_role_conflict_both_controlling_larger_tiebreaker_stays() -> Result<()> { // Create agent with controlling role let mut config = AgentConfig::default(); config.is_controlling = true; - let mut agent = Agent::new(Arc::new(config))?; + let mut agent = Agent::new(Arc::new(config), test_crypto_provider())?; // Set a larger tiebreaker value agent.tie_breaker = 500; @@ -2187,7 +2202,10 @@ fn test_role_conflict_both_controlling_larger_tiebreaker_stays() -> Result<()> { Box::new(Username::new(ATTR_USERNAME, username)), Box::new(AttrControlling(remote_tiebreaker)), // Remote is also controlling Box::new(PriorityAttr(1000)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + test_crypto_provider().crypto(), + )), Box::new(FINGERPRINT), ])?; @@ -2227,7 +2245,7 @@ fn test_role_conflict_both_controlled_larger_tiebreaker_switches() -> Result<()> // Create agent with controlled role let mut config = AgentConfig::default(); config.is_controlling = false; // Controlled - let mut agent = Agent::new(Arc::new(config))?; + let mut agent = Agent::new(Arc::new(config), test_crypto_provider())?; // Set a larger tiebreaker value agent.tie_breaker = 500; @@ -2280,7 +2298,10 @@ fn test_role_conflict_both_controlled_larger_tiebreaker_switches() -> Result<()> Box::new(Username::new(ATTR_USERNAME, username)), Box::new(AttrControlled(remote_tiebreaker)), // Remote is also controlled Box::new(PriorityAttr(1000)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + test_crypto_provider().crypto(), + )), Box::new(FINGERPRINT), ])?; @@ -2326,7 +2347,7 @@ fn test_role_conflict_both_controlled_smaller_tiebreaker_stays() -> Result<()> { // Create agent with controlled role let mut config = AgentConfig::default(); config.is_controlling = false; // Controlled - let mut agent = Agent::new(Arc::new(config))?; + let mut agent = Agent::new(Arc::new(config), test_crypto_provider())?; // Set a smaller tiebreaker value agent.tie_breaker = 100; @@ -2379,7 +2400,10 @@ fn test_role_conflict_both_controlled_smaller_tiebreaker_stays() -> Result<()> { Box::new(Username::new(ATTR_USERNAME, username)), Box::new(AttrControlled(remote_tiebreaker)), // Remote is also controlled Box::new(PriorityAttr(1000)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + test_crypto_provider().crypto(), + )), Box::new(FINGERPRINT), ])?; @@ -2417,7 +2441,7 @@ fn test_candidate_type_filtering() -> Result<()> { candidate_types: vec![CandidateType::Relay], ..Default::default() }); - let mut agent = Agent::new(config)?; + let mut agent = Agent::new(config, test_crypto_provider())?; // Host local candidate should be rejected let host_local = CandidateHostConfig { @@ -2491,7 +2515,7 @@ fn test_candidate_type_filtering() -> Result<()> { // recently updated the candidate timestamps (RFC 7675). #[test] fn test_keepalive_sent_during_media_flow() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; // Set up a selected pair let host_local = CandidateHostConfig { @@ -2562,7 +2586,7 @@ fn test_keepalive_sent_during_media_flow() -> Result<()> { #[test] fn test_pair_network_type_mismatch() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; // UDP: IPv4 local should not pair with IPv6 remote. let local_v4 = CandidateHostConfig { @@ -2684,7 +2708,7 @@ fn test_pair_network_type_mismatch() -> Result<()> { // thread '...' panicked at 'index out of bounds: the len is 0 but the index is 0' #[test] fn test_transition_to_failed_clears_stale_candidate_pairs() -> Result<()> { - let mut a = Agent::new(Arc::new(AgentConfig::default()))?; + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; let local = CandidateHostConfig { base_config: CandidateConfig { @@ -2758,7 +2782,7 @@ fn test_handle_inbound_request_defers_failing_connectivity_check() -> Result<()> failed_timeout: Some(Duration::from_secs(0)), ..Default::default() }; - let mut a = Agent::new(Arc::new(cfg))?; + let mut a = Agent::new(Arc::new(cfg), test_crypto_provider())?; let local_candidate = CandidateHostConfig { base_config: CandidateConfig { @@ -2800,7 +2824,10 @@ fn test_handle_inbound_request_defers_failing_connectivity_check() -> Result<()> Box::new(UseCandidateAttr::new()), Box::new(AttrControlling(tie_breaker)), Box::new(PriorityAttr(local_priority)), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + test_crypto_provider().crypto(), + )), Box::new(FINGERPRINT), ])?; @@ -2848,10 +2875,13 @@ fn test_query_only_agent_queries_mdns_remote_candidate() -> Result<()> { "1114572465 1 udp 2113939711 61b445d2-6503-41ac-96ce-ee3edac00e9f.local 61163 typ host"; // QueryOnly: adding the candidate issues an mDNS query (not a drop). - let mut agent = Agent::new(Arc::new(AgentConfig { - multicast_dns_mode: crate::mdns::MulticastDnsMode::QueryOnly, - ..Default::default() - }))?; + let mut agent = Agent::new( + Arc::new(AgentConfig { + multicast_dns_mode: crate::mdns::MulticastDnsMode::QueryOnly, + ..Default::default() + }), + test_crypto_provider(), + )?; let added = agent.add_remote_candidate(unmarshal_candidate(cand_line)?)?; assert!( !added, @@ -2867,10 +2897,13 @@ fn test_query_only_agent_queries_mdns_remote_candidate() -> Result<()> { ); // Disabled: the same candidate is silently dropped -- no mDNS query. - let mut agent = Agent::new(Arc::new(AgentConfig { - multicast_dns_mode: crate::mdns::MulticastDnsMode::Disabled, - ..Default::default() - }))?; + let mut agent = Agent::new( + Arc::new(AgentConfig { + multicast_dns_mode: crate::mdns::MulticastDnsMode::Disabled, + ..Default::default() + }), + test_crypto_provider(), + )?; agent.add_remote_candidate(unmarshal_candidate(cand_line)?)?; assert!( agent.poll_write().is_none(), @@ -2879,3 +2912,60 @@ fn test_query_only_agent_queries_mdns_remote_candidate() -> Result<()> { Ok(()) } + +/// Regression test: connectivity checks for a server-reflexive local candidate +/// must be tagged with the candidate's base (the bound host socket), not the +/// NAT-mapped srflx address (RFC 8445 §6.1.2). Drivers that route outbound +/// transmits by `transport.local_addr` otherwise have no socket to send from +/// and drop the packet. +#[test] +fn test_send_stun_from_srflx_uses_base_addr() -> Result<()> { + let mut a = Agent::new(Arc::new(AgentConfig::default()), test_crypto_provider())?; + + let srflx_local = CandidateServerReflexiveConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "10.79.12.1".to_owned(), // NAT mapping; no local socket here + port: 60823, + component: 1, + ..Default::default() + }, + rel_addr: "192.168.0.2".to_owned(), // base: bound host socket + rel_port: 5000, + ..Default::default() + } + .new_candidate_server_reflexive()?; + a.add_local_candidate(srflx_local)?; + + let host_remote = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "10.79.11.1".to_owned(), + port: 37983, + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + a.add_remote_candidate(host_remote)?; + + a.write_outs.clear(); + + let msg = Message::new(); + a.send_stun(&msg, 0, 0); + + let transmit = a.write_outs.pop_front().expect("send_stun must emit"); + assert_eq!( + transmit.transport.local_addr, + "192.168.0.2:5000".parse().unwrap(), + "srflx checks must be sent from the candidate's base, not the mapped address" + ); + assert_eq!( + transmit.transport.peer_addr, + "10.79.11.1:37983".parse().unwrap() + ); + + a.close()?; + Ok(()) +} diff --git a/rtc-ice/src/agent/mod.rs b/rtc-ice/src/agent/mod.rs index 1f06a776..a1d17086 100644 --- a/rtc-ice/src/agent/mod.rs +++ b/rtc-ice/src/agent/mod.rs @@ -1,13 +1,27 @@ +//! The Sans-I/O ICE agent. +//! +//! An [`Agent`](crate::agent::Agent) is given local and remote candidates and inbound datagrams; it produces the +//! connectivity checks to send, the state transitions to report, and eventually a selected +//! candidate pair. It owns no sockets and no clock — the caller drives time with +//! `handle_timeout`. +//! +//! Two roles exist: the controlling agent nominates the pair that will carry media, the +//! controlled agent accepts that choice. Which side controls is decided by which offered, and the +//! two must not agree — see [`Credentials`](crate::agent::Credentials) and [`Agent::set_role`](crate::agent::Agent::set_role). #[cfg(test)] mod agent_test; +/// Configuration for a new [`Agent`]: servers, timeouts and role. pub mod agent_config; mod agent_proto; +/// Pair selection and nomination — which candidate pair becomes the selected one. pub mod agent_selector; +/// Snapshot statistics for an agent's candidates and pairs. pub mod agent_stats; use agent_config::*; use bytes::BytesMut; +use crypto::{RTCCrypto, RTCCryptoProvider}; use log::{debug, error, info, trace, warn}; use mdns::{Mdns, QueryId}; use sansio::Protocol; @@ -55,8 +69,11 @@ impl Default for BindingRequest { } #[derive(Default, Clone)] +/// The ICE credentials for one side of a session, exchanged in SDP. pub struct Credentials { + /// The username fragment, echoed in every STUN check so a receiver can demultiplex. pub ufrag: String, + /// The password, used as the `MESSAGE-INTEGRITY` key. pub pwd: String, } @@ -82,13 +99,22 @@ fn assert_inbound_username(m: &Message, expected_username: &str) -> Result<()> { Ok(()) } -fn assert_inbound_message_integrity(m: &mut Message, key: &[u8]) -> Result<()> { - let message_integrity_attr = MessageIntegrity(key.to_vec()); - message_integrity_attr.check(m) +fn assert_inbound_message_integrity( + m: &mut Message, + key: &[u8], + crypto: &dyn RTCCrypto, +) -> Result<()> { + MessageIntegrity::check(m, key, crypto) } +/// What the agent reports to its caller. +#[non_exhaustive] pub enum Event { + /// The agent's connection state changed. ConnectionStateChange(ConnectionState), + /// A new pair was selected, carrying the local and remote candidates. + /// + /// Media should be sent on this pair from now on. SelectedCandidatePairChange(Box, Box), /// Emitted when the ICE role switches due to a role conflict (RFC 8445 §7.3.1.1). /// The bool is `true` if the agent is now controlling, `false` if now controlled. @@ -97,6 +123,7 @@ pub enum Event { /// Represents the ICE agent. pub struct Agent { + pub(crate) crypto_provider: Arc, pub(crate) tie_breaker: u64, pub(crate) is_controlling: bool, pub(crate) lite: bool, @@ -162,53 +189,16 @@ pub struct Agent { pub(crate) event_outs: VecDeque, } -impl Default for Agent { - fn default() -> Self { - Self { - tie_breaker: 0, - is_controlling: false, - lite: false, - start_time: Instant::now(), - connection_state: Default::default(), - last_connection_state: Default::default(), - ufrag_pwd: Default::default(), - local_candidates: vec![], - remote_candidates: vec![], - candidate_pairs: vec![], - nominated_pair: None, - selected_pair: None, - pending_binding_requests: vec![], - insecure_skip_verify: false, - max_binding_requests: 0, - host_acceptance_min_wait: Default::default(), - srflx_acceptance_min_wait: Default::default(), - prflx_acceptance_min_wait: Default::default(), - relay_acceptance_min_wait: Default::default(), - disconnected_timeout: Default::default(), - failed_timeout: Default::default(), - keepalive_interval: Default::default(), - last_consent_sent: Instant::now(), - check_interval: Default::default(), - checking_duration: Instant::now(), - last_checking_time: Instant::now(), - force_candidate_contact: false, - mdns_mode: MulticastDnsMode::Disabled, - mdns_local_name: "".to_owned(), - mdns_local_ip: None, - mdns_queries: HashMap::new(), - mdns: None, - candidate_types: vec![], - network_types: vec![], - urls: vec![], - write_outs: Default::default(), - event_outs: Default::default(), - } - } -} - impl Agent { /// Creates a new Agent. - pub fn new(config: Arc) -> Result { + /// + /// The crypto provider is supplied by the caller; this crate never resolves a default. + pub fn new( + config: Arc, + crypto_provider: Arc, + ) -> Result { + let tie_breaker = generate_tie_breaker(crypto_provider.random())?; + let mut mdns_local_name = config.multicast_dns_local_name.clone(); if mdns_local_name.is_empty() { mdns_local_name = generate_multicast_dns_name(); @@ -251,7 +241,8 @@ impl Agent { } let mut agent = Self { - tie_breaker: rand::random::(), + crypto_provider, + tie_breaker, is_controlling: config.is_controlling, lite: config.lite, @@ -533,18 +524,26 @@ impl Agent { &self.ufrag_pwd.local_credentials } + /// Whether this agent is the controlling one, which decides nomination. pub fn role(&self) -> bool { self.is_controlling } + /// Sets the controlling role. + /// + /// Determined by which side offered; the two agents must not agree, or nomination stalls. pub fn set_role(&mut self, is_controlling: bool) { self.is_controlling = is_controlling; } + /// The agent's current connection state. pub fn state(&self) -> ConnectionState { self.connection_state } + /// Whether a non-STUN datagram on `transport` should be accepted as media. + /// + /// Guards against accepting media from an address that has not passed a connectivity check. pub fn is_valid_non_stun_traffic(&mut self, transport: TransportContext) -> bool { self.find_local_candidate(transport.local_addr, transport.transport_protocol) .is_some() @@ -595,6 +594,7 @@ impl Agent { } } + /// The highest-priority pair that is still usable, whether or not it has been nominated. pub fn get_best_available_candidate_pair(&self) -> Option<(&Candidate, &Candidate)> { if let Some(pair_index) = self.get_best_available_pair() { let candidate_pair = &self.candidate_pairs[pair_index]; @@ -637,10 +637,10 @@ impl Agent { keep_local_candidates: bool, ) -> Result<()> { if ufrag.is_empty() { - ufrag = generate_ufrag(); + ufrag = generate_ufrag_with_random(self.crypto_provider.random())?; } if pwd.is_empty() { - pwd = generate_pwd(); + pwd = generate_pwd_with_random(self.crypto_provider.random())?; } if ufrag.len() * 8 < 24 { @@ -999,7 +999,10 @@ impl Agent { Box::new(m.clone()), Box::new(BINDING_SUCCESS), Box::new(XorMappedAddress { ip, port }), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + self.crypto_provider.crypto(), + )), Box::new(FINGERPRINT), ]); (out, result) @@ -1040,7 +1043,10 @@ impl Agent { Box::new(m.clone()), Box::new(stun::message::BINDING_ERROR), Box::new(CODE_ROLE_CONFLICT), - Box::new(MessageIntegrity::new_short_term_integrity(local_pwd)), + Box::new(MessageIntegrity::new_short_term_integrity_with_provider( + local_pwd, + self.crypto_provider.crypto(), + )), Box::new(FINGERPRINT), ]); (out, result) @@ -1256,8 +1262,11 @@ impl Agent { let mut remote_candidate_index = self.find_remote_candidate(remote_addr); if m.typ.class == CLASS_SUCCESS_RESPONSE { - if let Err(err) = assert_inbound_message_integrity(m, remote_credentials.pwd.as_bytes()) - { + if let Err(err) = assert_inbound_message_integrity( + m, + remote_credentials.pwd.as_bytes(), + self.crypto_provider.crypto(), + ) { warn!( "[{}]: discard message from ({}), {}", self.get_name(), @@ -1293,6 +1302,7 @@ impl Agent { } else if let Err(err) = assert_inbound_message_integrity( m, self.ufrag_pwd.local_credentials.pwd.as_bytes(), + self.crypto_provider.crypto(), ) { warn!( "[{}]: discard message from ({}), {}", @@ -1381,7 +1391,10 @@ impl Agent { pub(crate) fn send_stun(&mut self, msg: &Message, local_index: usize, remote_index: usize) { let peer_addr = self.remote_candidates[remote_index].addr(); - let local_addr = self.local_candidates[local_index].addr(); + // RFC 8445 §6.1.2: checks for a (server/peer-)reflexive candidate must + // be sent from its base, the bound local socket the candidate was + // derived from; the mapped address is not a local socket. + let local_addr = self.local_candidates[local_index].base_addr(); let transport_protocol = if self.local_candidates[local_index].network_type().is_tcp() { TransportProtocol::TCP } else { diff --git a/rtc-ice/src/attributes/control/mod.rs b/rtc-ice/src/attributes/control/mod.rs index 34b57201..9438d815 100644 --- a/rtc-ice/src/attributes/control/mod.rs +++ b/rtc-ice/src/attributes/control/mod.rs @@ -112,8 +112,11 @@ impl Getter for AttrControl { #[derive(Default, PartialEq, Eq, Copy, Clone, Debug)] pub enum Role { #[default] + /// This agent is controlling. Controlling, + /// This agent is controlled. Controlled, + /// No role attribute was present. Unspecified, } diff --git a/rtc-ice/src/attributes/mod.rs b/rtc-ice/src/attributes/mod.rs index 2e9d3441..7c39ac50 100644 --- a/rtc-ice/src/attributes/mod.rs +++ b/rtc-ice/src/attributes/mod.rs @@ -1,3 +1,6 @@ +/// `ICE-CONTROLLING` and `ICE-CONTROLLED`, which resolve role conflicts. pub mod control; +/// The `PRIORITY` attribute carried in connectivity checks. pub mod priority; +/// The `USE-CANDIDATE` flag, by which the controlling agent nominates a pair. pub mod use_candidate; diff --git a/rtc-ice/src/attributes/use_candidate/mod.rs b/rtc-ice/src/attributes/use_candidate/mod.rs index 353ff573..9adeec01 100644 --- a/rtc-ice/src/attributes/use_candidate/mod.rs +++ b/rtc-ice/src/attributes/use_candidate/mod.rs @@ -19,6 +19,7 @@ impl Setter for UseCandidateAttr { impl UseCandidateAttr { #[must_use] + /// A `USE-CANDIDATE` attribute, which carries no value. pub const fn new() -> Self { Self } diff --git a/rtc-ice/src/candidate/candidate_host.rs b/rtc-ice/src/candidate/candidate_host.rs index 431ddc57..a642c9b9 100644 --- a/rtc-ice/src/candidate/candidate_host.rs +++ b/rtc-ice/src/candidate/candidate_host.rs @@ -4,8 +4,10 @@ use crate::rand::generate_cand_id; /// The config required to create a new `CandidateHost`. #[derive(Default)] pub struct CandidateHostConfig { + /// The fields shared by every candidate type. pub base_config: CandidateConfig, + /// The TCP role, for ICE-TCP host candidates. pub tcp_type: TcpType, } diff --git a/rtc-ice/src/candidate/candidate_pair.rs b/rtc-ice/src/candidate/candidate_pair.rs index 5ff8805b..f224f0f3 100644 --- a/rtc-ice/src/candidate/candidate_pair.rs +++ b/rtc-ice/src/candidate/candidate_pair.rs @@ -1,12 +1,23 @@ +//! Candidate pairs and their check state. +//! +//! ICE forms a pair from each compatible local/remote candidate combination and works through +//! them in priority order. A pair's combined priority is computed from both sides' priorities with +//! the controlling agent's dominating, so both agents derive the same ordering. +//! +//! [`CandidatePairState`](crate::candidate::candidate_pair::CandidatePairState) tracks how far a pair has got: waiting, in progress, succeeded or +//! failed. The controlling agent nominates one of the succeeded pairs, and that pair carries the +//! media. use serde::{Deserialize, Serialize}; use std::fmt; use std::time::Duration; /// Represent the ICE candidate pair state. #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum CandidatePairState { #[default] #[serde(rename = "unspecified")] + /// No state was set. Unspecified = 0, /// Means a check has not been performed for this pair. @@ -56,9 +67,13 @@ impl fmt::Display for CandidatePairState { /// Represents a combination of a local and remote candidate. #[derive(Clone, Copy)] pub struct CandidatePair { + /// Index of the local candidate in the agent's list. pub local_index: usize, + /// Index of the remote candidate in the agent's list. pub remote_index: usize, + /// The local candidate's priority. pub local_priority: u32, + /// The remote candidate's priority. pub remote_priority: u32, pub(crate) ice_role_controlling: bool, pub(crate) binding_request_count: u16, @@ -120,6 +135,9 @@ impl PartialEq for CandidatePair { impl CandidatePair { #[must_use] + /// Forms a pair from a local and a remote candidate. + /// + /// `controlling` selects which priority dominates in the pair's combined priority. pub fn new( local_index: usize, remote_index: usize, diff --git a/rtc-ice/src/candidate/candidate_peer_reflexive.rs b/rtc-ice/src/candidate/candidate_peer_reflexive.rs index 5c017fda..ff3f402b 100644 --- a/rtc-ice/src/candidate/candidate_peer_reflexive.rs +++ b/rtc-ice/src/candidate/candidate_peer_reflexive.rs @@ -6,9 +6,12 @@ use shared::error::*; /// The config required to create a new `CandidatePeerReflexive`. #[derive(Default)] pub struct CandidatePeerReflexiveConfig { + /// The fields shared by every candidate type. pub base_config: CandidateConfig, + /// The base address this candidate was derived from, reported as `raddr`. pub rel_addr: String, + /// The base port, reported as `rport`. pub rel_port: u16, } diff --git a/rtc-ice/src/candidate/candidate_relay.rs b/rtc-ice/src/candidate/candidate_relay.rs index 7b1de871..256adbab 100644 --- a/rtc-ice/src/candidate/candidate_relay.rs +++ b/rtc-ice/src/candidate/candidate_relay.rs @@ -6,11 +6,15 @@ use shared::error::*; /// The config required to create a new `CandidateRelay`. #[derive(Default)] pub struct CandidateRelayConfig { + /// The fields shared by every candidate type. pub base_config: CandidateConfig, + /// The base address this candidate was derived from, reported as `raddr`. pub rel_addr: String, + /// The base port, reported as `rport`. pub rel_port: u16, + /// The server this candidate was gathered from. pub url: Option, } diff --git a/rtc-ice/src/candidate/candidate_server_reflexive.rs b/rtc-ice/src/candidate/candidate_server_reflexive.rs index 0da585d6..1df98a60 100644 --- a/rtc-ice/src/candidate/candidate_server_reflexive.rs +++ b/rtc-ice/src/candidate/candidate_server_reflexive.rs @@ -6,11 +6,15 @@ use shared::error::*; /// The config required to create a new `CandidateServerReflexive`. #[derive(Default)] pub struct CandidateServerReflexiveConfig { + /// The fields shared by every candidate type. pub base_config: CandidateConfig, + /// The base address this candidate was derived from, reported as `raddr`. pub rel_addr: String, + /// The base port, reported as `rport`. pub rel_port: u16, + /// The server this candidate was gathered from. pub url: Option, } diff --git a/rtc-ice/src/candidate/candidate_test.rs b/rtc-ice/src/candidate/candidate_test.rs index 09c29459..51ce52b7 100644 --- a/rtc-ice/src/candidate/candidate_test.rs +++ b/rtc-ice/src/candidate/candidate_test.rs @@ -1,5 +1,9 @@ use super::*; +use crate::candidate::candidate_host::CandidateHostConfig; use crate::candidate::candidate_pair::CandidatePairState; +use crate::candidate::candidate_peer_reflexive::CandidatePeerReflexiveConfig; +use crate::candidate::candidate_relay::CandidateRelayConfig; +use crate::candidate::candidate_server_reflexive::CandidateServerReflexiveConfig; use crate::candidate::{Candidate, unmarshal_candidate}; use std::time::Instant; @@ -429,3 +433,98 @@ fn test_candidate_marshal() -> Result<()> { Ok(()) } + +/// Regression test: a candidate's base address must be the local (bound) +/// transport address. For server/peer-reflexive candidates that is the +/// related address, not the NAT-mapped candidate address (RFC 8445 §5.1.1). +#[test] +fn test_candidate_base_addr() -> Result<()> { + let host = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "192.168.0.2".to_owned(), + port: 5000, + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + assert_eq!(host.base_addr(), host.addr(), "host base is itself"); + + let srflx = CandidateServerReflexiveConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "10.79.12.1".to_owned(), + port: 60823, + component: 1, + ..Default::default() + }, + rel_addr: "192.168.0.2".to_owned(), + rel_port: 5000, + ..Default::default() + } + .new_candidate_server_reflexive()?; + assert_eq!( + srflx.base_addr(), + "192.168.0.2:5000".parse().unwrap(), + "srflx base must be the related (host) address" + ); + assert_ne!(srflx.base_addr(), srflx.addr()); + + let prflx = CandidatePeerReflexiveConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "10.79.12.1".to_owned(), + port: 60824, + component: 1, + ..Default::default() + }, + rel_addr: "192.168.0.2".to_owned(), + rel_port: 5000, + ..Default::default() + } + .new_candidate_peer_reflexive()?; + assert_eq!( + prflx.base_addr(), + "192.168.0.2:5000".parse().unwrap(), + "prflx base must be the related (host) address" + ); + + // Missing/unparseable related address falls back to the candidate address. + let prflx_no_rel = CandidatePeerReflexiveConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "10.79.12.1".to_owned(), + port: 60825, + component: 1, + ..Default::default() + }, + rel_addr: "".to_owned(), + rel_port: 0, + ..Default::default() + } + .new_candidate_peer_reflexive()?; + assert_eq!(prflx_no_rel.base_addr(), prflx_no_rel.addr()); + + let relay = CandidateRelayConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: "50.0.0.1".to_owned(), + port: 5000, + component: 1, + ..Default::default() + }, + rel_addr: "192.168.0.2".to_owned(), + rel_port: 5001, + ..Default::default() + } + .new_candidate_relay()?; + assert_eq!( + relay.base_addr(), + relay.addr(), + "relay base is the relayed address itself" + ); + + Ok(()) +} diff --git a/rtc-ice/src/candidate/mod.rs b/rtc-ice/src/candidate/mod.rs index fd55fa69..0a20be83 100644 --- a/rtc-ice/src/candidate/mod.rs +++ b/rtc-ice/src/candidate/mod.rs @@ -1,3 +1,17 @@ +//! ICE candidates: the addresses an agent can be reached at. +//! +//! A [`Candidate`](crate::candidate::Candidate) pairs a transport address with a [`CandidateType`](crate::candidate::CandidateType) — host, server-reflexive, +//! peer-reflexive or relay — and the bookkeeping ICE needs: a priority, a foundation, and +//! last-sent/last-received times that feed consent freshness. +//! +//! Type drives priority, and priority drives check order: host candidates are tried first +//! because they need no traversal, relay candidates last because they always cost an extra hop. +//! The [`foundation`](crate::candidate::Candidate::foundation) groups candidates that share a base and transport, +//! so redundant checks can be skipped. +//! +//! Each type has its own constructor module (`candidate_host`, `candidate_relay`, …), all +//! built on the shared [`CandidateConfig`](crate::candidate::CandidateConfig). + #[cfg(test)] mod candidate_pair_test; #[cfg(test)] @@ -9,10 +23,15 @@ mod candidate_test; TODO: mod candidate_server_reflexive_test; */ +/// Host candidates: an address on a local interface. pub mod candidate_host; +/// A local/remote candidate pair and its check state. pub mod candidate_pair; +/// Peer-reflexive candidates, learned from an inbound check's source address. pub mod candidate_peer_reflexive; +/// Relay candidates, allocated on a TURN server. pub mod candidate_relay; +/// Server-reflexive candidates, learned from a STUN Binding response. pub mod candidate_server_reflexive; use crate::network_type::NetworkType; @@ -40,17 +59,30 @@ pub(crate) const COMPONENT_RTCP: u16 = 0; /// Represents the type of candidate `CandidateType` enum. #[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum CandidateType { #[default] #[serde(rename = "unspecified")] + /// No candidate type was set. Unspecified, #[serde(rename = "host")] + /// An address on one of this host's own interfaces. + /// + /// Highest priority: reachable without traversal when both peers share a network. Host, #[serde(rename = "srflx")] + /// This host's address as seen by a STUN server — its public mapping through the NAT. ServerReflexive, #[serde(rename = "prflx")] + /// An address learned from a peer's inbound connectivity check. + /// + /// Discovered during checking rather than gathering, when a NAT maps a different port per + /// destination. PeerReflexive, #[serde(rename = "relay")] + /// An address allocated on a TURN server, which forwards on this host's behalf. + /// + /// Lowest priority: it always costs an extra hop. Relay, } @@ -104,7 +136,11 @@ pub(crate) fn contains_candidate_type( /// Convey transport addresses related to the candidate, useful for diagnostics and other purposes. #[derive(PartialEq, Eq, Debug, Clone)] pub struct CandidateRelatedAddress { + /// The address of the related candidate — the base this one was derived from. + /// The candidate's address. pub address: String, + /// The port of the related candidate. + /// The candidate's port. pub port: u16, } @@ -116,17 +152,29 @@ impl fmt::Display for CandidateRelatedAddress { } #[derive(Default)] +/// The fields common to every candidate type, used when constructing one. pub struct CandidateConfig { + /// A unique identifier for this candidate; generated when left empty. pub candidate_id: String, + /// The transport, `udp` or `tcp`. pub network: String, + /// The candidate's address. pub address: String, + /// The candidate's port. pub port: u16, + /// The RTP component id: `1` for RTP, `2` for RTCP when not multiplexed. pub component: u16, + /// The candidate priority; computed from the type and local preference when zero. pub priority: u32, + /// The foundation, which groups candidates that share a base and transport. + /// + /// Pairs with the same foundation are checked together, so redundant checks are avoided. pub foundation: String, } #[derive(Clone, Debug)] +/// One ICE candidate: a transport address this agent can be reached at, or can reach a peer +/// at, together with its type, priority and liveness bookkeeping. pub struct Candidate { pub(crate) id: String, pub(crate) network_type: NetworkType, @@ -205,6 +253,10 @@ impl fmt::Display for Candidate { } impl Candidate { + /// The candidate's foundation, computed from its type, base address and transport. + /// + /// A foundation groups equivalent candidates; it is deterministic bookkeeping rather than a + /// secret, random identifier. CRC-32C is therefore intentional here. pub fn foundation(&self) -> String { if !self.foundation_override.is_empty() { return self.foundation_override.clone(); @@ -287,10 +339,12 @@ impl Candidate { self.candidate_type } + /// The TCP role for ICE-TCP candidates; `Unspecified` for UDP. pub fn tcp_type(&self) -> TcpType { self.tcp_type } + /// The STUN or TURN server this candidate was gathered from, if any. pub fn url(&self) -> Option<&str> { self.url.as_deref() } @@ -323,10 +377,37 @@ impl Candidate { val } + /// The candidate's socket address. pub fn addr(&self) -> SocketAddr { self.resolved_addr } + /// Returns the candidate's base address: the local transport address the + /// candidate was derived from, i.e. the address packets for this candidate + /// must be sent from (RFC 8445 §5.1.1). + /// + /// For server-reflexive and peer-reflexive candidates this is the related + /// (host) address; for host and relay candidates the base is the candidate + /// address itself. + pub fn base_addr(&self) -> SocketAddr { + match self.candidate_type { + CandidateType::ServerReflexive | CandidateType::PeerReflexive => self + .related_address + .as_ref() + .and_then(|ra| { + ra.address + .parse::() + .ok() + .map(|ip| SocketAddr::new(ip, ra.port)) + }) + .unwrap_or(self.resolved_addr), + _ => self.resolved_addr, + } + } + + /// Records traffic on this candidate, updating its last-sent or last-received time. + /// + /// Feeds consent freshness — a pair that stops seeing traffic is eventually abandoned. pub fn seen(&mut self, outbound: bool) { let now = Instant::now(); @@ -367,6 +448,11 @@ impl Candidate { } } + /// Sets the resolved IP, deriving the network type from it. + /// + /// # Errors + /// + /// Fails if `ip`'s family does not match this candidate's network type. pub fn set_ip(&mut self, ip: &IpAddr) -> Result<()> { self.network_type = determine_network_type(&self.network, ip)?; self.resolved_addr = SocketAddr::new(*ip, self.port); //TODO: create_addr(network_type, *ip, self.port); @@ -375,10 +461,12 @@ impl Candidate { } impl Candidate { + /// Records that traffic was received on this candidate at `now`. pub fn set_last_received(&mut self, now: Instant) { self.last_received = now; } + /// Records that traffic was sent on this candidate at `now`. pub fn set_last_sent(&mut self, now: Instant) { self.last_sent = now; } diff --git a/rtc-ice/src/lib.rs b/rtc-ice/src/lib.rs index 22bef907..808fdf97 100644 --- a/rtc-ice/src/lib.rs +++ b/rtc-ice/src/lib.rs @@ -1,15 +1,81 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! ICE for the Sans-I/O WebRTC stack. +//! +//! Interactive Connectivity Establishment ([RFC 8445], superseding [RFC 5245]) with the +//! extensions WebRTC uses: ICE-TCP candidates ([RFC 6544]), consent freshness +//! ([RFC 7675]), and Trickle ICE. ICE is what finds a path between two peers behind NATs: +//! it gathers candidate addresses, pairs local with remote, and probes each pair with STUN +//! connectivity checks until one succeeds. +//! +//! # Structure +//! +//! * [`agent`] — the Sans-I/O [`Agent`]: give it candidates and inbound +//! datagrams, poll it for checks to send, state transitions, and the selected pair. It +//! owns no sockets and no clock. +//! * [`candidate`] — the candidate types (host, server-reflexive, peer-reflexive, relay), +//! their priorities, and SDP `a=candidate` parsing. +//! * [`state`] — connection and gathering states, and the checklist state machine. +//! * [`url`] — parsing `stun:`/`turn:` server URLs into something the agent can gather from. +//! * [`network_type`], [`tcp_type`] — UDP/TCP and active/passive/simultaneous-open. +//! * [`stats`] — per-candidate and per-pair counters, surfaced through `getStats`. +//! * [`mdns`] — mDNS candidate handling, for hiding private addresses. +//! +//! # Example +//! +//! ``` +//! use rtc_ice::url::{ProtoType, SchemeType, Url}; +//! +//! # fn example() -> Result<(), Box> { +//! let stun = Url::parse_url("stun:stun.l.google.com:19302")?; +//! assert_eq!(stun.scheme, SchemeType::Stun); +//! assert_eq!(stun.port, 19302); +//! +//! // TURN URLs may pin the transport used to reach the server. +//! let turn = Url::parse_url("turn:turn.example.com:3478?transport=tcp")?; +//! assert_eq!(turn.scheme, SchemeType::Turn); +//! assert_eq!(turn.proto, ProtoType::Tcp); +//! # Ok(()) +//! # } +//! ``` +//! +//! Most applications do not depend on this crate directly — the +//! [`rtc`](https://docs.rs/rtc) crate drives the agent as one layer of the peer-connection +//! pipeline. +//! +//! [RFC 8445]: https://datatracker.ietf.org/doc/html/rfc8445 +//! [RFC 5245]: https://datatracker.ietf.org/doc/html/rfc5245 +//! [RFC 6544]: https://datatracker.ietf.org/doc/html/rfc6544 +//! [RFC 7675]: https://datatracker.ietf.org/doc/html/rfc7675 + +/// The Sans-I/O ICE agent: candidate pairing, connectivity checks, and nomination. +/// The crypto provider API. +/// +/// Re-exported because this crate's public constructors take an +/// [`Arc`](crypto::RTCCryptoProvider), which a caller must be able to name +/// without adding — and version-matching — a direct `rtc-crypto` dependency. +pub use crypto; + pub mod agent; +/// The ICE-specific STUN attributes carried in connectivity checks. pub mod attributes; +/// Candidate types, priorities, and SDP `a=candidate` parsing. pub mod candidate; +/// mDNS candidate handling, which hides private addresses behind `.local` names. pub mod mdns; +/// UDP/TCP over IPv4/IPv6, as a candidate's transport. pub mod network_type; +/// Random ICE credentials and identifiers. pub mod rand; +/// Connection and gathering states. pub mod state; +/// Per-candidate and per-pair counters, surfaced through `getStats`. pub mod stats; +/// Active, passive and simultaneous-open, for ICE-TCP candidates. pub mod tcp_type; +/// Parsing `stun:`/`turn:` server URLs. pub mod url; pub use agent::{ diff --git a/rtc-ice/src/mdns/mod.rs b/rtc-ice/src/mdns/mod.rs index 591158ac..1c37ce7a 100644 --- a/rtc-ice/src/mdns/mod.rs +++ b/rtc-ice/src/mdns/mod.rs @@ -11,6 +11,7 @@ use shared::error::Result; /// Represents the different Multicast modes that ICE can run. #[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +#[non_exhaustive] pub enum MulticastDnsMode { /// Means remote mDNS candidates will be discarded, and local host candidates will use IPs. Disabled, @@ -26,6 +27,8 @@ pub enum MulticastDnsMode { pub(crate) fn generate_multicast_dns_name() -> String { // https://tools.ietf.org/id/draft-ietf-rtcweb-mdns-ice-candidates-02.html#gathering // The unique name MUST consist of a version 4 UUID as defined in [RFC4122], followed by “.local”. + // This is a short-lived privacy alias, not a credential. `Uuid::new_v4` obtains randomness + // from the UUID crate's secure random source without forcing provider ownership into mDNS. let u = Uuid::new_v4(); format!("{u}.local") } diff --git a/rtc-ice/src/network_type/mod.rs b/rtc-ice/src/network_type/mod.rs index df7f3922..d598396f 100644 --- a/rtc-ice/src/network_type/mod.rs +++ b/rtc-ice/src/network_type/mod.rs @@ -1,3 +1,8 @@ +//! Candidate transports: UDP or TCP, over IPv4 or IPv6. +//! +//! A [`NetworkType`](crate::network_type::NetworkType) pairs the transport protocol with the address family, because ICE treats +//! them together: a candidate's type constrains which remote candidates it can be paired with, +//! and pairs across families are never formed. #[cfg(test)] mod network_type_test; @@ -13,6 +18,7 @@ pub(crate) const UDP: &str = "udp"; pub(crate) const TCP: &str = "tcp"; #[must_use] +/// Every network type this crate can gather candidates for. pub fn supported_network_types() -> Vec { vec![ NetworkType::Udp4, @@ -24,9 +30,11 @@ pub fn supported_network_types() -> Vec { /// Represents the type of network. #[derive(Default, PartialEq, Debug, Copy, Clone, Eq, Hash, Serialize, Deserialize)] +#[non_exhaustive] pub enum NetworkType { #[serde(rename = "unspecified")] #[default] + /// No network type was set. Unspecified, /// Indicates UDP over IPv4. @@ -85,6 +93,7 @@ impl NetworkType { } #[must_use] + /// The transport protocol, discarding the address family. pub fn to_protocol(self) -> TransportProtocol { if self.is_tcp() { TransportProtocol::TCP diff --git a/rtc-ice/src/rand/mod.rs b/rtc-ice/src/rand/mod.rs index 6fd04661..0b03b0bf 100644 --- a/rtc-ice/src/rand/mod.rs +++ b/rtc-ice/src/rand/mod.rs @@ -1,3 +1,5 @@ +use crypto::RTCRandom; +use shared::error::{Error, Result}; use shared::util::generate_crypto_random_string; #[cfg(test)] @@ -14,6 +16,10 @@ const LEN_PWD: usize = 32; /// candidate-id = "candidate" ":" foundation /// foundation = 1*32ice-char /// ice-char = ALPHA / DIGIT / "+" / "/" +/// +/// Candidate IDs provide local uniqueness and diagnostics; they are not credentials or +/// cryptographic identities. This standalone helper therefore uses `rand`'s thread-local CSPRNG +/// rather than requiring a crypto provider. pub fn generate_cand_id() -> String { format!( "candidate:{}", @@ -21,14 +27,65 @@ pub fn generate_cand_id() -> String { ) } -/// Generates ICE pwd. -/// This internally uses `generate_crypto_random_string`. +fn generate_string_with_random( + length: usize, + alphabet: &[u8], + random: &dyn RTCRandom, +) -> Result { + debug_assert!(!alphabet.is_empty() && alphabet.len() <= u8::MAX as usize + 1); + + let acceptance_limit = 256 - (256 % alphabet.len()); + let mut output = String::with_capacity(length); + let mut random_bytes = [0_u8; 64]; + while output.len() < length { + random + .fill(&mut random_bytes) + .map_err(|error| Error::Crypto(error.to_string()))?; + for byte in random_bytes { + if byte as usize >= acceptance_limit { + continue; + } + output.push(alphabet[byte as usize % alphabet.len()] as char); + if output.len() == length { + break; + } + } + } + Ok(output) +} + +/// Generates an ICE password with `rand`'s thread-local CSPRNG. +/// +/// ICE agents use the provider-backed internal variant. This function remains as a standalone +/// compatibility helper for callers that do not own an [`crypto::RTCCryptoProvider`]. pub fn generate_pwd() -> String { generate_crypto_random_string(LEN_PWD, RUNES_ALPHA) } -/// ICE user fragment. -/// This internally uses `generate_crypto_random_string`. +/// Generates an ICE password using an explicitly supplied cryptographically secure random source. +pub(crate) fn generate_pwd_with_random(random: &dyn RTCRandom) -> Result { + generate_string_with_random(LEN_PWD, RUNES_ALPHA, random) +} + +/// Generates the 64-bit ICE role-conflict tie breaker from the agent's random provider. +pub(crate) fn generate_tie_breaker(random: &dyn RTCRandom) -> Result { + let mut bytes = [0_u8; std::mem::size_of::()]; + random + .fill(&mut bytes) + .map_err(|error| Error::Crypto(error.to_string()))?; + Ok(u64::from_be_bytes(bytes)) +} + +/// Generates an ICE username fragment with `rand`'s thread-local CSPRNG. +/// +/// ICE agents use the provider-backed internal variant. This function remains as a standalone +/// compatibility helper for callers that do not own an [`crypto::RTCCryptoProvider`]. pub fn generate_ufrag() -> String { generate_crypto_random_string(LEN_UFRAG, RUNES_ALPHA) } + +/// Generates an ICE username fragment using an explicitly supplied cryptographically secure random +/// source. +pub(crate) fn generate_ufrag_with_random(random: &dyn RTCRandom) -> Result { + generate_string_with_random(LEN_UFRAG, RUNES_ALPHA, random) +} diff --git a/rtc-ice/src/rand/rand_test.rs b/rtc-ice/src/rand/rand_test.rs index 568bfc5c..75fc5236 100644 --- a/rtc-ice/src/rand/rand_test.rs +++ b/rtc-ice/src/rand/rand_test.rs @@ -1,6 +1,62 @@ use super::*; +use crypto::{CryptoError, RTCRandom}; use shared::error::Result; +struct FixedRandom(u8); + +impl RTCRandom for FixedRandom { + fn fill(&self, output: &mut [u8]) -> std::result::Result<(), CryptoError> { + output.fill(self.0); + Ok(()) + } +} + +struct FailingRandom; + +impl RTCRandom for FailingRandom { + fn fill(&self, _output: &mut [u8]) -> std::result::Result<(), CryptoError> { + Err(CryptoError::RandomnessFailed) + } +} + +#[test] +fn provider_backed_credentials_have_required_lengths_and_alphabet() -> Result<()> { + let random = FixedRandom(0); + let ufrag = generate_ufrag_with_random(&random)?; + let password = generate_pwd_with_random(&random)?; + + assert_eq!(ufrag.len(), LEN_UFRAG); + assert_eq!(password.len(), LEN_PWD); + assert!(ufrag.bytes().all(|byte| RUNES_ALPHA.contains(&byte))); + assert!(password.bytes().all(|byte| RUNES_ALPHA.contains(&byte))); + Ok(()) +} + +#[test] +fn provider_backed_credentials_propagate_randomness_failure() { + assert!(matches!( + generate_pwd_with_random(&FailingRandom), + Err(Error::Crypto(_)) + )); +} + +#[test] +fn provider_backed_tie_breaker_uses_all_random_bytes() -> Result<()> { + assert_eq!( + generate_tie_breaker(&FixedRandom(0x2a))?, + u64::from_be_bytes([0x2a; 8]) + ); + Ok(()) +} + +#[test] +fn provider_backed_tie_breaker_propagates_randomness_failure() { + assert!(matches!( + generate_tie_breaker(&FailingRandom), + Err(Error::Crypto(_)) + )); +} + #[test] fn test_random_generator_collision() -> Result<()> { let test_cases = vec![ diff --git a/rtc-ice/src/state/mod.rs b/rtc-ice/src/state/mod.rs index d44679ca..6433bb7c 100644 --- a/rtc-ice/src/state/mod.rs +++ b/rtc-ice/src/state/mod.rs @@ -5,7 +5,9 @@ use std::fmt; /// An enum showing the state of a ICE Connection List of supported States. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum ConnectionState { + /// No state was set. #[default] Unspecified, @@ -64,7 +66,9 @@ impl From for ConnectionState { /// Describes the state of the candidate gathering process. #[derive(Default, PartialEq, Eq, Copy, Clone)] +#[non_exhaustive] pub enum GatheringState { + /// No state was set. #[default] Unspecified, diff --git a/rtc-ice/src/stats/mod.rs b/rtc-ice/src/stats/mod.rs index 440e910d..192202ea 100644 --- a/rtc-ice/src/stats/mod.rs +++ b/rtc-ice/src/stats/mod.rs @@ -3,157 +3,157 @@ use std::time::Instant; use crate::candidate::candidate_pair::CandidatePairState; use crate::candidate::*; -// CandidatePairStats contains ICE candidate pair statistics +/// CandidatePairStats contains ICE candidate pair statistics. #[derive(Debug, Clone)] pub struct CandidatePairStats { - // timestamp is the timestamp associated with this object. + /// The timestamp associated with this object. pub timestamp: Instant, - // local_candidate_id is the id of the local candidate + /// The id of the local candidate. pub local_candidate_id: String, - // remote_candidate_id is the id of the remote candidate + /// The id of the remote candidate. pub remote_candidate_id: String, - // state represents the state of the checklist for the local and remote - // candidates in a pair. + /// The state of the checklist for the local and remote + /// candidates in a pair. pub state: CandidatePairState, - // nominated is true when this valid pair that should be used for media - // if it is the highest-priority one amongst those whose nominated flag is set + /// True when this valid pair that should be used for media + /// if it is the highest-priority one amongst those whose nominated flag is set. pub nominated: bool, - // packets_sent represents the total number of packets sent on this candidate pair. + /// The total number of packets sent on this candidate pair. pub packets_sent: u32, - // packets_received represents the total number of packets received on this candidate pair. + /// The total number of packets received on this candidate pair. pub packets_received: u32, - // bytes_sent represents the total number of payload bytes sent on this candidate pair - // not including headers or padding. + /// The total number of payload bytes sent on this candidate pair + /// not including headers or padding. pub bytes_sent: u64, - // bytes_received represents the total number of payload bytes received on this candidate pair - // not including headers or padding. + /// The total number of payload bytes received on this candidate pair + /// not including headers or padding. pub bytes_received: u64, - // last_packet_sent_timestamp represents the timestamp at which the last packet was - // sent on this particular candidate pair, excluding STUN packets. + /// The timestamp at which the last packet was + /// sent on this particular candidate pair, excluding STUN packets. pub last_packet_sent_timestamp: Instant, - // last_packet_received_timestamp represents the timestamp at which the last packet - // was received on this particular candidate pair, excluding STUN packets. + /// The timestamp at which the last packet + /// was received on this particular candidate pair, excluding STUN packets. pub last_packet_received_timestamp: Instant, - // first_request_timestamp represents the timestamp at which the first STUN request - // was sent on this particular candidate pair. + /// The timestamp at which the first STUN request + /// was sent on this particular candidate pair. pub first_request_timestamp: Instant, - // last_request_timestamp represents the timestamp at which the last STUN request - // was sent on this particular candidate pair. The average interval between two - // consecutive connectivity checks sent can be calculated with - // (last_request_timestamp - first_request_timestamp) / requests_sent. + /// The timestamp at which the last STUN request + /// was sent on this particular candidate pair. The average interval between two + /// consecutive connectivity checks sent can be calculated with + /// (last_request_timestamp - first_request_timestamp) / requests_sent. pub last_request_timestamp: Instant, - // last_response_timestamp represents the timestamp at which the last STUN response - // was received on this particular candidate pair. + /// The timestamp at which the last STUN response + /// was received on this particular candidate pair. pub last_response_timestamp: Instant, - // total_round_trip_time represents the sum of all round trip time measurements - // in seconds since the beginning of the session, based on STUN connectivity - // check responses (responses_received), including those that reply to requests - // that are sent in order to verify consent. The average round trip time can - // be computed from total_round_trip_time by dividing it by responses_received. + /// The sum of all round trip time measurements + /// in seconds since the beginning of the session, based on STUN connectivity + /// check responses (responses_received), including those that reply to requests + /// that are sent in order to verify consent. The average round trip time can + /// be computed from total_round_trip_time by dividing it by responses_received. pub total_round_trip_time: f64, - // current_round_trip_time represents the latest round trip time measured in seconds, - // computed from both STUN connectivity checks, including those that are sent - // for consent verification. + /// The latest round trip time measured in seconds, + /// computed from both STUN connectivity checks, including those that are sent + /// for consent verification. pub current_round_trip_time: f64, - // available_outgoing_bitrate is calculated by the underlying congestion control - // by combining the available bitrate for all the outgoing RTP streams using - // this candidate pair. The bitrate measurement does not count the size of the - // ip or other transport layers like TCP or UDP. It is similar to the TIAS defined - // in RFC 3890, i.e., it is measured in bits per second and the bitrate is calculated - // over a 1 second window. + /// Calculated by the underlying congestion control + /// by combining the available bitrate for all the outgoing RTP streams using + /// this candidate pair. The bitrate measurement does not count the size of the + /// ip or other transport layers like TCP or UDP. It is similar to the TIAS defined + /// in RFC 3890, i.e., it is measured in bits per second and the bitrate is calculated + /// over a 1 second window. pub available_outgoing_bitrate: f64, - // available_incoming_bitrate is calculated by the underlying congestion control - // by combining the available bitrate for all the incoming RTP streams using - // this candidate pair. The bitrate measurement does not count the size of the - // ip or other transport layers like TCP or UDP. It is similar to the TIAS defined - // in RFC 3890, i.e., it is measured in bits per second and the bitrate is - // calculated over a 1 second window. + /// Calculated by the underlying congestion control + /// by combining the available bitrate for all the incoming RTP streams using + /// this candidate pair. The bitrate measurement does not count the size of the + /// ip or other transport layers like TCP or UDP. It is similar to the TIAS defined + /// in RFC 3890, i.e., it is measured in bits per second and the bitrate is + /// calculated over a 1 second window. pub available_incoming_bitrate: f64, - // circuit_breaker_trigger_count represents the number of times the circuit breaker - // is triggered for this particular 5-tuple, ceasing transmission. + /// The number of times the circuit breaker + /// is triggered for this particular 5-tuple, ceasing transmission. pub circuit_breaker_trigger_count: u32, - // requests_received represents the total number of connectivity check requests - // received (including retransmissions). It is impossible for the receiver to - // tell whether the request was sent in order to check connectivity or check - // consent, so all connectivity checks requests are counted here. + /// The total number of connectivity check requests + /// received (including retransmissions). It is impossible for the receiver to + /// tell whether the request was sent in order to check connectivity or check + /// consent, so all connectivity checks requests are counted here. pub requests_received: u64, - // requests_sent represents the total number of connectivity check requests - // sent (not including retransmissions). + /// The total number of connectivity check requests + /// sent (not including retransmissions). pub requests_sent: u64, - // responses_received represents the total number of connectivity check responses received. + /// The total number of connectivity check responses received. pub responses_received: u64, - // responses_sent epresents the total number of connectivity check responses sent. - // Since we cannot distinguish connectivity check requests and consent requests, - // all responses are counted. + /// Responses_sent epresents the total number of connectivity check responses sent. + /// Since we cannot distinguish connectivity check requests and consent requests, + /// all responses are counted. pub responses_sent: u64, - // retransmissions_received represents the total number of connectivity check - // request retransmissions received. + /// The total number of connectivity check + /// request retransmissions received. pub retransmissions_received: u64, - // retransmissions_sent represents the total number of connectivity check - // request retransmissions sent. + /// The total number of connectivity check + /// request retransmissions sent. pub retransmissions_sent: u64, - // consent_requests_sent represents the total number of consent requests sent. + /// The total number of consent requests sent. pub consent_requests_sent: u64, - // consent_expired_timestamp represents the timestamp at which the latest valid - // STUN binding response expired. + /// The timestamp at which the latest valid. + /// STUN binding response expired. pub consent_expired_timestamp: Instant, } -// CandidateStats contains ICE candidate statistics related to the ICETransport objects. +/// CandidateStats contains ICE candidate statistics related to the ICETransport objects. #[derive(Debug, Clone)] pub struct CandidateStats { - // timestamp is the timestamp associated with this object. + /// The timestamp associated with this object. pub timestamp: Instant, - // id is the candidate id + /// The candidate id. pub id: String, - // ip is the ip address of the candidate, allowing for IPv4 addresses and - // IPv6 addresses, but fully qualified domain names (FQDNs) are not allowed. + /// The ip address of the candidate, allowing for IPv4 addresses and. + /// IPv6 addresses, but fully qualified domain names (FQDNs) are not allowed. pub ip: String, - // port is the port number of the candidate. + /// The port number of the candidate. pub port: u16, - // candidate_type is the "Type" field of the ICECandidate. + /// The "Type" field of the ICECandidate. pub candidate_type: CandidateType, - // priority is the "priority" field of the ICECandidate. + /// The "priority" field of the ICECandidate. pub priority: u32, - // url is the url of the TURN or STUN server indicated in the that translated - // this ip address. It is the url address surfaced in an PeerConnectionICEEvent. + /// The url of the TURN or STUN server indicated in the that translated + /// this ip address. It is the url address surfaced in an PeerConnectionICEEvent. pub url: String, - // relay_protocol is the protocol used by the endpoint to communicate with the - // TURN server. This is only present for local candidates. Valid values for - // the TURN url protocol is one of udp, tcp, or tls. + /// The protocol used by the endpoint to communicate with the. + /// TURN server. This is only present for local candidates. Valid values for + /// the TURN url protocol is one of udp, tcp, or tls. pub relay_protocol: String, // deleted is true if the candidate has been deleted/freed. For host candidates, @@ -161,6 +161,6 @@ pub struct CandidateStats { // candidate have been released. For TURN candidates, this means the TURN allocation // is no longer active. // - // Only defined for local candidates. For remote candidates, this property is not applicable. + /// Only defined for local candidates. For remote candidates, this property is not applicable. pub deleted: bool, } diff --git a/rtc-ice/src/tcp_type/mod.rs b/rtc-ice/src/tcp_type/mod.rs index 7feee6ad..677366a2 100644 --- a/rtc-ice/src/tcp_type/mod.rs +++ b/rtc-ice/src/tcp_type/mod.rs @@ -6,6 +6,10 @@ use std::fmt; // TCPType is the type of ICE TCP candidate as described in // https://tools.ietf.org/html/rfc6544#section-4.5 #[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +/// The role of an ICE-TCP candidate, per [RFC 6544] §4.5. +/// +/// [RFC 6544]: https://datatracker.ietf.org/doc/html/rfc6544#section-4.5 +#[non_exhaustive] pub enum TcpType { /// The default value. For example UDP candidates do not need this field. #[default] diff --git a/rtc-ice/src/url/mod.rs b/rtc-ice/src/url/mod.rs index ee5aec22..5261b275 100644 --- a/rtc-ice/src/url/mod.rs +++ b/rtc-ice/src/url/mod.rs @@ -9,6 +9,7 @@ use shared::error::*; /// The type of server used in the ice.URL structure. #[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +#[non_exhaustive] pub enum SchemeType { /// The URL represents a STUN server. Stun, @@ -24,6 +25,7 @@ pub enum SchemeType { #[default] /// Default public constant to use for "enum" like struct comparisons when no value was defined. + /// A scheme or transport this crate does not recognise. Unknown, } @@ -56,6 +58,7 @@ impl fmt::Display for SchemeType { /// The transport protocol type that is used in the `ice::url::Url` structure. #[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +#[non_exhaustive] pub enum ProtoType { /// The URL uses a UDP transport. #[default] @@ -64,6 +67,7 @@ pub enum ProtoType { /// The URL uses a TCP transport. Tcp, + /// A transport this crate does not recognise. Unknown, } @@ -95,11 +99,17 @@ impl fmt::Display for ProtoType { /// Represents a STUN (rfc7064) or TURN (rfc7065) URL. #[derive(Debug, Clone, Default)] pub struct Url { + /// The URL scheme: `stun`, `stuns`, `turn` or `turns`. pub scheme: SchemeType, + /// The server host name or address. pub host: String, + /// The server port; defaults to 3478, or 5349 for the secure schemes. pub port: u16, + /// The TURN username, for `turn:`/`turns:` URLs. pub username: String, + /// The TURN credential. pub password: String, + /// The transport to reach the server over, from the URL's `?transport=` parameter. pub proto: ProtoType, } diff --git a/rtc-interceptor-derive/Cargo.toml b/rtc-interceptor-derive/Cargo.toml index 901584e8..d421d8d2 100644 --- a/rtc-interceptor-derive/Cargo.toml +++ b/rtc-interceptor-derive/Cargo.toml @@ -15,6 +15,6 @@ categories.workspace = true proc-macro = true [dependencies] -proc-macro2 = "1" -quote = "1" -syn = { version = "2", features = ["full", "parsing", "extra-traits"] } +proc-macro2.workspace = true +quote.workspace = true +syn.workspace = true diff --git a/rtc-interceptor-derive/src/lib.rs b/rtc-interceptor-derive/src/lib.rs index 876203c0..ccbcce73 100644 --- a/rtc-interceptor-derive/src/lib.rs +++ b/rtc-interceptor-derive/src/lib.rs @@ -1,3 +1,4 @@ +#![warn(missing_docs)] //! Derive macros for RTC Interceptor trait. //! //! This crate provides two macros that work together: @@ -7,10 +8,18 @@ //! //! # Design Pattern //! +//! The examples below are illustrative rather than compiled: the macros only expand to something +//! meaningful in the presence of the `Interceptor` trait from +//! [`rtc-interceptor`](https://docs.rs/rtc-interceptor), which depends on *this* crate — so a +//! doctest here cannot import it. They are exercised for real by `rtc-interceptor`'s own +//! documentation and tests. +//! //! The design follows Rust's derive pattern (similar to `#[derive(Default)]` with `#[default]`): //! //! ```ignore -//! use rtc_interceptor::{Interceptor, interceptor, TaggedPacket, Packet, StreamInfo}; +//! use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +//! use rtc_shared::error::Error; +//! use sansio::Protocol; //! use std::collections::VecDeque; //! //! #[derive(Interceptor)] @@ -35,6 +44,10 @@ //! For interceptors that just pass through without modification: //! //! ```ignore +//! # use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +//! # use rtc_shared::error::Error; +//! # use sansio::Protocol; +//! # use std::collections::VecDeque; //! #[derive(Interceptor)] //! pub struct PassthroughInterceptor { //! #[next] @@ -50,12 +63,22 @@ //! //! The macros require certain types to be in scope: //! +//! The generated code names `sansio::Protocol`, `Error`, `StreamInfo` and +//! `TaggedPacket`, so all four must be in scope at the use site — not only the macros +//! themselves: +//! +//! ```ignore +//! use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +//! use rtc_shared::error::Error; +//! use sansio::Protocol; +//! ``` +//! +//! Through the `rtc` umbrella crate the same imports are: +//! //! ```ignore -//! use rtc_interceptor::{Interceptor, interceptor, TaggedPacket, Packet, StreamInfo}; -//! // Or through rtc umbrella crate: -//! use rtc::interceptor::{Interceptor, interceptor, TaggedPacket, Packet, StreamInfo}; +//! use rtc::interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +//! use rtc::sansio::Protocol; //! use rtc::shared::error::Error; -//! use rtc::sansio; // Required for macro-generated code //! ``` use proc_macro::TokenStream; @@ -76,6 +99,10 @@ use syn::{Data, DeriveInput, Fields, Ident, ImplItem, ItemImpl, Type, parse_macr /// /// Pure delegation (no custom logic): /// ```ignore +/// # use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +/// # use rtc_shared::error::Error; +/// # use sansio::Protocol; +/// # use std::collections::VecDeque; /// #[derive(Interceptor)] /// pub struct PassthroughInterceptor { /// #[next] @@ -88,6 +115,10 @@ use syn::{Data, DeriveInput, Fields, Ident, ImplItem, ItemImpl, Type, parse_macr /// /// With custom logic: /// ```ignore +/// # use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +/// # use rtc_shared::error::Error; +/// # use sansio::Protocol; +/// # use std::collections::VecDeque; /// #[derive(Interceptor)] /// pub struct MyInterceptor { /// #[next] @@ -149,6 +180,10 @@ pub fn derive_interceptor(input: TokenStream) -> TokenStream { /// /// With custom logic: /// ```ignore +/// # use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +/// # use rtc_shared::error::Error; +/// # use sansio::Protocol; +/// # use std::collections::VecDeque; /// #[derive(Interceptor)] /// pub struct MyInterceptor { /// #[next] @@ -168,6 +203,10 @@ pub fn derive_interceptor(input: TokenStream) -> TokenStream { /// /// Pure delegation (no custom logic): /// ```ignore +/// # use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +/// # use rtc_shared::error::Error; +/// # use sansio::Protocol; +/// # use std::collections::VecDeque; /// #[derive(Interceptor)] /// pub struct PassthroughInterceptor { /// #[next] diff --git a/rtc-interceptor/src/lib.rs b/rtc-interceptor/src/lib.rs index 44e74561..514aea2b 100644 --- a/rtc-interceptor/src/lib.rs +++ b/rtc-interceptor/src/lib.rs @@ -76,7 +76,7 @@ //! //! # Quick Start //! -//! ```ignore +//! ``` //! use rtc_interceptor::{ //! Registry, SenderReportBuilder, ReceiverReportBuilder, //! NackGeneratorBuilder, NackResponderBuilder, @@ -109,12 +109,45 @@ //! .build(); //! ``` //! +//! # Type-Erasing a Chain +//! +//! A chain's type spells out its whole composition +//! (`TwccReceiverInterceptor>`), and it propagates into every type +//! that holds the peer connection built from it. That is fine when the chain is fixed at compile +//! time, and a problem when it is chosen at runtime or has to live in your own structs. +//! +//! [`Interceptor`] is object safe, so [`Registry::boxed`] can erase the chain to +//! [`BoxedInterceptor`] — one concrete type, whatever it was built from: +//! +//! ``` +//! use rtc_interceptor::{BoxedInterceptor, NackGeneratorBuilder, Registry, SenderReportBuilder}; +//! +//! # let nack_enabled = true; // e.g. from configuration, negotiated SDP, … +//! // Two different chain types, unified by `.boxed()`. +//! let chain: BoxedInterceptor = if nack_enabled { +//! Registry::new() +//! .with(SenderReportBuilder::new().build()) +//! .with(NackGeneratorBuilder::new().build()) +//! .boxed() +//! .build() +//! } else { +//! Registry::new().with(SenderReportBuilder::new().build()).boxed().build() +//! }; +//! ``` +//! +//! The cost is one virtual call per chain entry point (`handle_read`, `poll_write`, +//! `handle_timeout`, …); the layers inside still call each other through static dispatch and +//! inline as before. `Box

` and `&mut P` both implement [`Interceptor`], so a boxed or borrowed +//! chain satisfies an `I: Interceptor` bound like any other. +//! //! # Stream Binding //! //! Before interceptors can process packets for a stream, the stream must be bound: //! -//! ```ignore -//! use rtc_interceptor::{StreamInfo, RTCPFeedback, RTPHeaderExtension}; +//! ``` +//! use rtc_interceptor::{Interceptor, RTCPFeedback, RTPHeaderExtension, Registry, StreamInfo}; +//! +//! let mut chain = Registry::new().build(); //! //! // Create stream info with NACK and TWCC support //! let stream_info = StreamInfo { @@ -144,8 +177,10 @@ //! //! Use the derive macros to easily create custom interceptors: //! -//! ```ignore -//! use rtc_interceptor::{Interceptor, interceptor, TaggedPacket, StreamInfo}; +//! ``` +//! use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +//! use sansio::Protocol; +//! use shared::error::Error; // the generated `Protocol` impl names it //! use std::collections::VecDeque; //! //! #[derive(Interceptor)] @@ -172,6 +207,7 @@ //! See the [`Interceptor`] trait documentation for more details. #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] use shared::TransportMessage; @@ -211,6 +247,7 @@ pub use interceptor_derive::{Interceptor, interceptor}; /// An enum representing either an RTP or RTCP packet that can be processed /// by interceptors in the chain. #[derive(Debug, Clone, PartialEq)] +#[non_exhaustive] pub enum Packet { /// RTP (Real-time Transport Protocol) packet containing media data Rtp(rtp::Packet), @@ -243,8 +280,10 @@ pub type TaggedPacket = TransportMessage; /// /// The easiest way to create a custom interceptor is using the derive macros: /// -/// ```ignore -/// use rtc_interceptor::{Interceptor, interceptor, TaggedPacket, Packet, StreamInfo}; +/// ``` +/// use rtc_interceptor::{Interceptor, StreamInfo, TaggedPacket, interceptor}; +/// use sansio::Protocol; +/// use shared::error::Error; // the generated `Protocol` impl names it /// use std::collections::VecDeque; /// /// #[derive(Interceptor)] @@ -273,7 +312,9 @@ pub type TaggedPacket = TransportMessage; /// /// ## Manual Implementation /// -/// For more control, you can implement the traits manually: +/// For more control, you can implement the traits manually. The sketch below omits the +/// `Protocol` method bodies, so it is not compiled — see [`NoopInterceptor`] for a complete +/// hand-written implementation: /// /// ```ignore /// pub struct MyInterceptor

{ @@ -299,9 +340,14 @@ pub type TaggedPacket = TransportMessage; /// /// # Using with Registry /// -/// ```ignore -/// let chain = Registry::new() -/// .with(|inner| MyInterceptor { next: inner, buffer: VecDeque::new() }); +/// A builder is just a closure from the next layer to the wrapping one, so a custom +/// interceptor can be added the same way as a built-in: +/// +/// ``` +/// use rtc_interceptor::{Registry, SenderReportBuilder}; +/// +/// let registry = Registry::new().with(SenderReportBuilder::new().build()); +/// // ...or with a closure: `.with(|inner| MyInterceptor { next: inner, .. })` /// ``` pub trait Interceptor: sansio::Protocol< @@ -313,10 +359,8 @@ pub trait Interceptor: Eout = (), Time = Instant, Error = shared::error::Error, - > + Sized - + Send + > + Send + Sync - + 'static { /// Wrap this interceptor with another layer. /// @@ -325,16 +369,17 @@ pub trait Interceptor: /// /// # Example /// - /// ```ignore + /// ``` + /// use rtc_interceptor::{Interceptor, NoopInterceptor, SenderReportBuilder}; /// use std::time::Duration; - /// use rtc_interceptor::{NoopInterceptor, SenderReportBuilder}; /// - /// // Using the builder pattern (recommended) + /// // `Interceptor` must be in scope for `with` to resolve. /// let chain = NoopInterceptor::new() /// .with(SenderReportBuilder::new().with_interval(Duration::from_secs(1)).build()); /// ``` fn with(self, f: F) -> O where + Self: Sized, F: FnOnce(Self) -> O, O: Interceptor, { @@ -356,6 +401,58 @@ pub trait Interceptor: fn unbind_remote_stream(&mut self, info: &StreamInfo); } +/// A type-erased interceptor chain. +/// +/// `Interceptor` is object safe, so a chain built at runtime can be erased into this one +/// concrete type. That lets an application store a `RTCPeerConnection` +/// (see [`Registry::boxed`]) instead of being generic over the chain's type. +pub type BoxedInterceptor = Box; + +impl Interceptor for Box

{ + fn bind_local_stream(&mut self, info: &StreamInfo) { + (**self).bind_local_stream(info) + } + + fn unbind_local_stream(&mut self, info: &StreamInfo) { + (**self).unbind_local_stream(info) + } + + fn bind_remote_stream(&mut self, info: &StreamInfo) { + (**self).bind_remote_stream(info) + } + + fn unbind_remote_stream(&mut self, info: &StreamInfo) { + (**self).unbind_remote_stream(info) + } +} + +/// Blanket implementation for mutable references. +/// +/// This lets a borrowed chain satisfy an `Interceptor` bound, so a function taking +/// `I: Interceptor` by value can be called with `&mut chain` and leave ownership with the +/// caller. It mirrors [`sansio::Protocol`]'s own `&mut P` implementation, and the same idiom +/// in `std` (`impl Read for &mut R`, `impl Iterator for &mut I`). +/// +/// This is only expressible because [`Interceptor`] does not require `'static`: `&'a mut P` +/// outlives only `'a`. See [`Registry::boxed`], which carries that bound locally instead. +impl Interceptor for &mut P { + fn bind_local_stream(&mut self, info: &StreamInfo) { + (**self).bind_local_stream(info) + } + + fn unbind_local_stream(&mut self, info: &StreamInfo) { + (**self).unbind_local_stream(info) + } + + fn bind_remote_stream(&mut self, info: &StreamInfo) { + (**self).bind_remote_stream(info) + } + + fn unbind_remote_stream(&mut self, info: &StreamInfo) { + (**self).unbind_remote_stream(info) + } +} + #[cfg(test)] mod derive_tests { use super::*; @@ -423,4 +520,46 @@ mod derive_tests { chain.bind_remote_stream(&info); chain.unbind_remote_stream(&info); } + + /// Consumes an interceptor by value, as the `Registry`/`with` builders do. + fn takes_by_value(mut interceptor: I, info: &StreamInfo) { + interceptor.bind_local_stream(info); + interceptor.unbind_local_stream(info); + } + + #[test] + fn test_borrowed_chain_satisfies_interceptor_bound() { + let mut chain = SimplePassthrough::new(NoopInterceptor::new()); + let info = StreamInfo { + ssrc: 12345, + ..Default::default() + }; + + // `&mut chain` satisfies a by-value `I: Interceptor` bound thanks to the blanket impl. + takes_by_value(&mut chain, &info); + + // Ownership stayed with us, so the chain is still usable afterwards. + takes_by_value(&mut chain, &info); + chain.bind_remote_stream(&info); + + // The borrow also still drives the Protocol side. + let pkt = TaggedPacket { + now: std::time::Instant::now(), + transport: Default::default(), + message: Packet::Rtp(rtp::Packet::default()), + }; + sansio::Protocol::handle_write(&mut chain, pkt).unwrap(); + assert!(sansio::Protocol::poll_write(&mut chain).is_some()); + } + + #[test] + fn test_boxed_chain_still_satisfies_interceptor_bound() { + // The `Box

` impl coexists with the new `&mut P` impl. + let chain: BoxedInterceptor = Box::new(SimplePassthrough::new(NoopInterceptor::new())); + let info = StreamInfo { + ssrc: 999, + ..Default::default() + }; + takes_by_value(chain, &info); + } } diff --git a/rtc-interceptor/src/nack/generator.rs b/rtc-interceptor/src/nack/generator.rs index ec4b2180..7d858e9e 100644 --- a/rtc-interceptor/src/nack/generator.rs +++ b/rtc-interceptor/src/nack/generator.rs @@ -14,7 +14,7 @@ use std::time::{Duration, Instant}; /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, NackGeneratorBuilder}; /// use std::time::Duration; /// diff --git a/rtc-interceptor/src/nack/mod.rs b/rtc-interceptor/src/nack/mod.rs index 7f8a73f7..03b818f9 100644 --- a/rtc-interceptor/src/nack/mod.rs +++ b/rtc-interceptor/src/nack/mod.rs @@ -1,59 +1,12 @@ -//! NACK (Negative Acknowledgement) Interceptors. +//! NACK interceptors (internal module). //! -//! This module provides interceptors for handling RTCP NACK-based packet loss recovery -//! as specified in RFC 4585 (Extended RTP Profile for RTCP-Based Feedback). -//! -//! # Interceptors -//! -//! - [`NackGeneratorInterceptor`]: Monitors incoming RTP packets and generates -//! NACK requests for missing packets. -//! - [`NackResponderInterceptor`]: Buffers outgoing RTP packets and retransmits -//! them when NACK requests are received. -//! -//! # How NACK Works -//! -//! 1. **Detection**: The receiver detects missing packets by tracking sequence numbers -//! 2. **Request**: The receiver sends an RTCP NACK packet listing missing sequence numbers -//! 3. **Retransmission**: The sender retransmits the requested packets -//! -//! # RTX Support (RFC 4588) -//! -//! The responder supports RFC 4588 RTX (Retransmission) format, which uses a separate -//! SSRC and payload type for retransmissions. This allows the receiver to distinguish -//! between original and retransmitted packets. RTX is enabled by setting `ssrc_rtx` -//! and `payload_type_rtx` in [`StreamInfo`](crate::StreamInfo). -//! -//! # NACK Support Detection -//! -//! Both interceptors check if a stream supports NACK by looking for an [`RTCPFeedback`](crate::RTCPFeedback) -//! entry with `type: "nack"` and empty `parameter`. Streams without NACK support -//! are passed through without modification. +//! `NackGeneratorInterceptor` and `NackResponderInterceptor` are re-exported from the crate +//! root, where the user-facing documentation and examples live so that rustdoc renders them. //! //! # References //! -//! - [RFC 4585](https://datatracker.ietf.org/doc/html/rfc4585) - Extended RTP Profile for RTCP-Based Feedback (RTP/AVPF) -//! - [RFC 4588](https://datatracker.ietf.org/doc/html/rfc4588) - RTP Retransmission Payload Format -//! -//! # Example -//! -//! ```ignore -//! use rtc_interceptor::{Registry, NackGeneratorBuilder, NackResponderBuilder}; -//! use std::time::Duration; -//! -//! let chain = Registry::new() -//! // Generator for incoming streams (detects loss, sends NACKs) -//! .with(NackGeneratorBuilder::new() -//! .with_size(512) // Buffer size for tracking -//! .with_interval(Duration::from_millis(100)) // NACK generation interval -//! .with_skip_last_n(2) // Skip recent packets (may just be delayed) -//! .build()) -//! // Responder for outgoing streams (buffers packets, handles NACKs) -//! .with(NackResponderBuilder::new() -//! .with_size(1024) // Buffer size for retransmission -//! .build()) -//! .build(); -//! ``` - +//! - [RFC 4585](https://datatracker.ietf.org/doc/html/rfc4585) - RTP/AVPF (NACK feedback) +//! - [RFC 4588](https://datatracker.ietf.org/doc/html/rfc4588) - RTP Retransmission (RTX) pub(crate) mod generator; pub(crate) mod receive_log; pub(crate) mod responder; diff --git a/rtc-interceptor/src/nack/responder.rs b/rtc-interceptor/src/nack/responder.rs index 392ccf20..9bc0663f 100644 --- a/rtc-interceptor/src/nack/responder.rs +++ b/rtc-interceptor/src/nack/responder.rs @@ -14,7 +14,7 @@ use std::time::Instant; /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, NackResponderBuilder}; /// /// let chain = Registry::new() diff --git a/rtc-interceptor/src/noop.rs b/rtc-interceptor/src/noop.rs index a6321532..09a9a205 100644 --- a/rtc-interceptor/src/noop.rs +++ b/rtc-interceptor/src/noop.rs @@ -14,12 +14,18 @@ use std::time::Instant; /// /// # Example /// -/// ```ignore -/// use rtc_interceptor::NoopInterceptor; +/// ``` +/// use rtc_interceptor::{NoopInterceptor, Packet, TaggedPacket}; /// use sansio::Protocol; +/// use std::time::Instant; /// /// let mut noop = NoopInterceptor::new(); -/// noop.handle_read(TaggedPacket::Rtp(...)).unwrap(); +/// noop.handle_read(TaggedPacket { +/// now: Instant::now(), +/// transport: Default::default(), +/// message: Packet::Rtp(rtp::Packet::default()), +/// }) +/// .unwrap(); /// assert!(noop.poll_read().is_some()); /// ``` pub struct NoopInterceptor { diff --git a/rtc-interceptor/src/registry.rs b/rtc-interceptor/src/registry.rs index 5a3bb51e..d3afa272 100644 --- a/rtc-interceptor/src/registry.rs +++ b/rtc-interceptor/src/registry.rs @@ -19,8 +19,8 @@ //! Results in: C wraps B wraps A wraps NoopInterceptor //! ``` -use crate::Interceptor; use crate::noop::NoopInterceptor; +use crate::{BoxedInterceptor, Interceptor}; /// Registry for constructing interceptor chains. /// @@ -29,14 +29,11 @@ use crate::noop::NoopInterceptor; /// /// # Example /// -/// ```ignore -/// use rtc_interceptor::Registry; -/// -/// // Create a new registry -/// let mut registry = Registry::new(); +/// ``` +/// use rtc_interceptor::{ReceiverReportBuilder, Registry, SenderReportBuilder}; /// -/// // Add interceptors (can be done in helper functions) -/// registry = registry +/// // Each `with` changes the registry's type, so rebind rather than reassign. +/// let registry = Registry::new() /// .with(SenderReportBuilder::new().build()) /// .with(ReceiverReportBuilder::new().build()); /// @@ -46,10 +43,12 @@ use crate::noop::NoopInterceptor; /// /// # Helper Function Pattern /// -/// ```ignore +/// ``` +/// use rtc_interceptor::{Interceptor, ReceiverReportBuilder, Registry, SenderReportBuilder}; +/// /// fn register_default_interceptors( /// registry: Registry

, -/// ) -> Registry { +/// ) -> Registry> { /// registry /// .with(SenderReportBuilder::new().build()) /// .with(ReceiverReportBuilder::new().build()) @@ -71,7 +70,7 @@ impl Registry { /// /// # Example /// - /// ```ignore + /// ``` /// use rtc_interceptor::Registry; /// /// let registry = Registry::new(); @@ -94,8 +93,10 @@ impl Registry

{ /// /// # Example /// - /// ```ignore - /// let custom = MyCustomInterceptor::new(); + /// ``` + /// use rtc_interceptor::{NoopInterceptor, Registry}; + /// + /// let custom = NoopInterceptor::new(); /// let registry = Registry::from(custom); /// ``` pub fn from(inner: P) -> Self { @@ -108,7 +109,9 @@ impl Registry

{ /// /// # Example /// - /// ```ignore + /// ``` + /// use rtc_interceptor::{ReceiverReportBuilder, Registry, SenderReportBuilder}; + /// /// let registry = Registry::new() /// .with(SenderReportBuilder::new().build()) /// .with(ReceiverReportBuilder::new().build()); @@ -129,13 +132,56 @@ impl Registry

{ /// /// # Example /// - /// ```ignore - /// let registry = Registry::new().with(MyInterceptor::new); + /// ``` + /// use rtc_interceptor::{Registry, SenderReportBuilder}; + /// + /// let registry = Registry::new().with(SenderReportBuilder::new().build()); /// let chain = registry.build(); /// ``` pub fn build(self) -> P { self.inner } + + /// Erase the chain's type, turning this into a `Registry`. + /// + /// The chain an application assembles at runtime is a deep nest of generic types + /// (`TwccSender>>`), which otherwise leaks into every + /// type that holds the peer connection. Boxing it collapses that to one concrete type, so + /// a struct can store an `RTCPeerConnection` field directly. + /// + /// # Example + /// + /// ``` + /// use rtc_interceptor::{BoxedInterceptor, Registry, SenderReportBuilder}; + /// + /// // Whatever the chain was composed of, the result has one concrete type. + /// let chain: BoxedInterceptor = Registry::new() + /// .with(SenderReportBuilder::new().build()) + /// .boxed() + /// .build(); + /// ``` + /// + /// The `rtc` crate accepts the erased registry directly, so a peer connection can be stored + /// as `RTCPeerConnection`: + /// + /// ```ignore + /// let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; + /// let pc = RTCPeerConnectionBuilder::new() + /// .with_interceptor_registry(registry.boxed()) + /// .build()?; + /// ``` + /// + /// `P: 'static` is required because [`BoxedInterceptor`] is `Box`, + /// so the chain must not borrow anything shorter-lived. This is the only operation that needs + /// the bound, which is why [`Interceptor`] itself does not require `'static`. + pub fn boxed(self) -> Registry + where + P: 'static, + { + Registry { + inner: Box::new(self.inner), + } + } } #[cfg(test)] diff --git a/rtc-interceptor/src/report/mod.rs b/rtc-interceptor/src/report/mod.rs index 52c850e9..0c0cf27f 100644 --- a/rtc-interceptor/src/report/mod.rs +++ b/rtc-interceptor/src/report/mod.rs @@ -1,55 +1,12 @@ -//! RTCP Report Interceptors. +//! RTCP report interceptors (internal module). //! -//! This module provides interceptors for generating and handling RTCP reports -//! as specified in RFC 3550 (RTP: A Transport Protocol for Real-Time Applications). -//! -//! # Interceptors -//! -//! - [`SenderReportInterceptor`]: Generates RTCP Sender Reports (SR) for local streams -//! and filters hop-by-hop RTCP feedback that shouldn't be forwarded end-to-end. -//! - [`ReceiverReportInterceptor`]: Generates RTCP Receiver Reports (RR) based on -//! incoming RTP packet statistics (loss, jitter, etc.). -//! -//! # Sender Reports (SR) -//! -//! Sender Reports contain: -//! - NTP timestamp (wall-clock time) -//! - RTP timestamp (media time) -//! - Sender's packet count and octet count -//! - Optional report blocks for streams the sender is also receiving -//! -//! # Receiver Reports (RR) -//! -//! Receiver Reports contain report blocks with: -//! - Fraction of packets lost since last report -//! - Cumulative packets lost -//! - Extended highest sequence number received -//! - Interarrival jitter estimate -//! - Last SR timestamp (LSR) and delay since last SR (DLSR) +//! `SenderReportInterceptor` and `ReceiverReportInterceptor` are re-exported from the crate +//! root, where the user-facing documentation and examples live so that rustdoc renders them. //! //! # References //! -//! - [RFC 3550](https://datatracker.ietf.org/doc/html/rfc3550) - RTP: A Transport Protocol for Real-Time Applications -//! - [RFC 3611](https://datatracker.ietf.org/doc/html/rfc3611) - RTP Control Protocol Extended Reports (RTCP XR) -//! -//! # Example -//! -//! ```ignore -//! use rtc_interceptor::{Registry, SenderReportBuilder, ReceiverReportBuilder}; -//! use std::time::Duration; -//! -//! let chain = Registry::new() -//! // Sender Report for outgoing streams -//! .with(SenderReportBuilder::new() -//! .with_interval(Duration::from_secs(1)) -//! .build()) -//! // Receiver Report for incoming streams -//! .with(ReceiverReportBuilder::new() -//! .with_interval(Duration::from_secs(1)) -//! .build()) -//! .build(); -//! ``` - +//! - [RFC 3550](https://datatracker.ietf.org/doc/html/rfc3550) - RTP (Sender/Receiver Reports) +//! - [RFC 3611](https://datatracker.ietf.org/doc/html/rfc3611) - RTCP Extended Reports (XR) pub(crate) mod receiver; pub(crate) mod receiver_stream; pub(crate) mod sender; diff --git a/rtc-interceptor/src/report/receiver.rs b/rtc-interceptor/src/report/receiver.rs index 6387fbe2..2d6308e7 100644 --- a/rtc-interceptor/src/report/receiver.rs +++ b/rtc-interceptor/src/report/receiver.rs @@ -13,7 +13,7 @@ use std::time::{Duration, Instant}; /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, ReceiverReportBuilder}; /// use std::time::Duration; /// @@ -54,12 +54,16 @@ impl

ReceiverReportBuilder

{ /// /// # Example /// - /// ```ignore + /// ``` + /// use rtc_interceptor::{ReceiverReportBuilder, Registry}; /// use std::time::Duration; - /// use rtc_interceptor::ReceiverReportBuilder; /// - /// let builder = ReceiverReportBuilder::new() - /// .with_interval(Duration::from_millis(500)); + /// // The builder is generic over the next layer, so its type is pinned by `with`. + /// let registry = Registry::new().with( + /// ReceiverReportBuilder::new() + /// .with_interval(Duration::from_millis(500)) + /// .build(), + /// ); /// ``` pub fn with_interval(mut self, interval: Duration) -> Self { self.interval = interval; @@ -72,7 +76,7 @@ impl

ReceiverReportBuilder

{ /// /// # Example /// - /// ```ignore + /// ``` /// use rtc_interceptor::{Registry, ReceiverReportBuilder}; /// /// let registry = Registry::new() @@ -94,7 +98,7 @@ impl

ReceiverReportBuilder

{ /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, ReceiverReportBuilder}; /// /// let chain = Registry::new() diff --git a/rtc-interceptor/src/report/sender.rs b/rtc-interceptor/src/report/sender.rs index b9c33df0..60875409 100644 --- a/rtc-interceptor/src/report/sender.rs +++ b/rtc-interceptor/src/report/sender.rs @@ -14,7 +14,7 @@ use std::time::{Duration, Instant}; /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, SenderReportBuilder}; /// use std::time::Duration; /// @@ -63,12 +63,16 @@ impl

SenderReportBuilder

{ /// /// # Example /// - /// ```ignore + /// ``` + /// use rtc_interceptor::{Registry, SenderReportBuilder}; /// use std::time::Duration; - /// use rtc_interceptor::SenderReportBuilder; /// - /// let builder = SenderReportBuilder::new() - /// .with_interval(Duration::from_millis(500)); + /// // The builder is generic over the next layer, so its type is pinned by `with`. + /// let registry = Registry::new().with( + /// SenderReportBuilder::new() + /// .with_interval(Duration::from_millis(500)) + /// .build(), + /// ); /// ``` pub fn with_interval(mut self, interval: Duration) -> Self { self.interval = interval; @@ -89,11 +93,11 @@ impl

SenderReportBuilder

{ /// /// # Example /// - /// ```ignore - /// use rtc_interceptor::SenderReportBuilder; + /// ``` + /// use rtc_interceptor::{Registry, SenderReportBuilder}; /// - /// let builder = SenderReportBuilder::new() - /// .with_use_latest_packet(); + /// let registry = + /// Registry::new().with(SenderReportBuilder::new().with_use_latest_packet().build()); /// ``` pub fn with_use_latest_packet(mut self) -> Self { self.use_latest_packet = true; @@ -106,7 +110,7 @@ impl

SenderReportBuilder

{ /// /// # Example /// - /// ```ignore + /// ``` /// use rtc_interceptor::{Registry, SenderReportBuilder}; /// /// let registry = Registry::new() @@ -129,7 +133,7 @@ impl

SenderReportBuilder

{ /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, SenderReportBuilder}; /// /// let chain = Registry::new() diff --git a/rtc-interceptor/src/stream_info.rs b/rtc-interceptor/src/stream_info.rs index 5d385a9c..5e279321 100644 --- a/rtc-interceptor/src/stream_info.rs +++ b/rtc-interceptor/src/stream_info.rs @@ -86,7 +86,7 @@ pub struct RTCPFeedback { /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{StreamInfo, RTCPFeedback, RTPHeaderExtension}; /// /// let info = StreamInfo { diff --git a/rtc-interceptor/src/twcc/mod.rs b/rtc-interceptor/src/twcc/mod.rs index 2a994622..278942fa 100644 --- a/rtc-interceptor/src/twcc/mod.rs +++ b/rtc-interceptor/src/twcc/mod.rs @@ -1,69 +1,12 @@ -//! TWCC (Transport Wide Congestion Control) Interceptors. +//! TWCC interceptors (internal module). //! -//! This module provides interceptors for Transport Wide Congestion Control, -//! a bandwidth estimation mechanism that provides detailed per-packet feedback. -//! -//! # Interceptors -//! -//! - [`TwccSenderInterceptor`]: Adds transport-wide sequence numbers to outgoing RTP packets. -//! - [`TwccReceiverInterceptor`]: Tracks incoming RTP packets and generates TransportLayerCC feedback. -//! -//! # How TWCC Works -//! -//! 1. **Sender**: Adds a transport-wide sequence number to each RTP packet via header extension -//! 2. **Receiver**: Records arrival time of each packet by sequence number -//! 3. **Feedback**: Receiver periodically sends TransportLayerCC RTCP packets with arrival info -//! 4. **Estimation**: Sender uses feedback to estimate available bandwidth -//! -//! # Sequence Number Sharing -//! -//! Unlike per-stream RTP sequence numbers, TWCC sequence numbers are shared across -//! all streams in a session. This allows the sender to correlate feedback across -//! multiple media tracks for more accurate bandwidth estimation. -//! -//! # TWCC Support Detection -//! -//! Interceptors detect TWCC support by checking [`StreamInfo::rtp_header_extensions`](crate::StreamInfo::rtp_header_extensions) -//! for the TWCC header extension URI. Streams without the extension are passed through -//! without modification. +//! `TwccSenderInterceptor` and `TwccReceiverInterceptor` are re-exported from the crate root, +//! where the user-facing documentation and examples live so that rustdoc renders them. //! //! # References //! -//! - [draft-holmer-rmcat-transport-wide-cc-extensions-01](https://datatracker.ietf.org/doc/html/draft-holmer-rmcat-transport-wide-cc-extensions-01) - RTP Extensions for Transport-wide Congestion Control -//! -//! # Example -//! -//! ```ignore -//! use rtc_interceptor::{Registry, TwccSenderBuilder, TwccReceiverBuilder}; -//! use std::time::Duration; -//! -//! let chain = Registry::new() -//! // Sender: adds TWCC sequence numbers to outgoing packets -//! .with(TwccSenderBuilder::new().build()) -//! // Receiver: generates TWCC feedback for incoming packets -//! .with(TwccReceiverBuilder::new() -//! .with_interval(Duration::from_millis(100)) // Feedback interval -//! .build()) -//! .build(); -//! ``` -//! -//! # Stream Configuration -//! -//! To enable TWCC for a stream, include the header extension in [`StreamInfo`](crate::StreamInfo): -//! -//! ```ignore -//! use rtc_interceptor::{StreamInfo, RTPHeaderExtension}; -//! -//! let stream_info = StreamInfo { -//! ssrc: 0x12345678, -//! rtp_header_extensions: vec![RTPHeaderExtension { -//! uri: "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01".to_string(), -//! id: 5, // Extension ID negotiated via SDP -//! }], -//! ..Default::default() -//! }; -//! ``` - +//! - [draft-holmer-rmcat-transport-wide-cc-extensions-01](https://datatracker.ietf.org/doc/html/draft-holmer-rmcat-transport-wide-cc-extensions-01) +//! - RTP Extensions for Transport-wide Congestion Control pub(crate) mod arrival_time_map; pub(crate) mod receiver; pub(crate) mod recorder; diff --git a/rtc-interceptor/src/twcc/receiver.rs b/rtc-interceptor/src/twcc/receiver.rs index cbb6509b..fd24325e 100644 --- a/rtc-interceptor/src/twcc/receiver.rs +++ b/rtc-interceptor/src/twcc/receiver.rs @@ -18,7 +18,7 @@ const DEFAULT_INTERVAL: Duration = Duration::from_millis(100); /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, TwccReceiverBuilder}; /// use std::time::Duration; /// diff --git a/rtc-interceptor/src/twcc/sender.rs b/rtc-interceptor/src/twcc/sender.rs index 5349d9e7..45da3339 100644 --- a/rtc-interceptor/src/twcc/sender.rs +++ b/rtc-interceptor/src/twcc/sender.rs @@ -12,7 +12,7 @@ use std::marker::PhantomData; /// /// # Example /// -/// ```ignore +/// ``` /// use rtc_interceptor::{Registry, TwccSenderBuilder}; /// /// let chain = Registry::new() diff --git a/rtc-mdns/Cargo.toml b/rtc-mdns/Cargo.toml index b9999160..ba5def4e 100644 --- a/rtc-mdns/Cargo.toml +++ b/rtc-mdns/Cargo.toml @@ -17,7 +17,7 @@ sansio.workspace = true bytes.workspace = true log.workspace = true -socket2 = { version = "0.6", features = ["all"] } +socket2.workspace = true [dev-dependencies] env_logger.workspace = true diff --git a/rtc-mdns/examples/mdns_query.rs b/rtc-mdns/examples/mdns_query.rs index 993d5d97..9fe67a87 100644 --- a/rtc-mdns/examples/mdns_query.rs +++ b/rtc-mdns/examples/mdns_query.rs @@ -157,6 +157,7 @@ async fn main() -> Result<(), Box> { conn.close()?; return Err(format!("Query timed out after {} seconds", args.timeout).into()); } + _ => {} } } } diff --git a/rtc-mdns/examples/mdns_server_query.rs b/rtc-mdns/examples/mdns_server_query.rs index de00f876..e78e1ac8 100644 --- a/rtc-mdns/examples/mdns_server_query.rs +++ b/rtc-mdns/examples/mdns_server_query.rs @@ -255,6 +255,7 @@ async fn main() -> Result<(), Box> { format!("Query {} timed out after {} seconds", id, args.timeout).into(), ); } + _ => {} } } } diff --git a/rtc-mdns/src/config.rs b/rtc-mdns/src/config.rs index 9cd3a98f..1e5cd69c 100644 --- a/rtc-mdns/src/config.rs +++ b/rtc-mdns/src/config.rs @@ -1,55 +1,7 @@ -//! MdnsConfiguration for mDNS connections. +//! `MdnsConfig`, the configuration for an mDNS connection. //! -//! This module provides the [`MdnsConfig`] struct for configuring mDNS client and server behavior. -//! -//! # Examples -//! -//! ## Client MdnsConfiguration -//! -//! For a client that only sends queries: -//! -//! ```rust -//! use rtc_mdns::MdnsConfig; -//! use std::time::Duration; -//! -//! let config = MdnsConfig::default() -//! .with_query_interval(Duration::from_millis(500)); // Retry every 500ms -//! ``` -//! -//! ## Server MdnsConfiguration -//! -//! For a server that responds to queries: -//! -//! ```rust -//! use rtc_mdns::MdnsConfig; -//! use std::net::{IpAddr, Ipv4Addr}; -//! -//! let config = MdnsConfig::default() -//! .with_local_names(vec![ -//! "mydevice.local".to_string(), -//! "mydevice._http._tcp.local".to_string(), -//! ]) -//! .with_local_ip( -//! IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), -//! ); -//! ``` -//! -//! ## Combined Client/Server -//! -//! For a connection that both queries and responds: -//! -//! ```rust -//! use rtc_mdns::MdnsConfig; -//! use std::net::{IpAddr, Ipv4Addr}; -//! use std::time::Duration; -//! -//! let config = MdnsConfig::default() -//! .with_query_interval(Duration::from_secs(1)) -//! .with_local_names(vec!["myhost.local".to_string()]) -//! .with_local_ip( -//! IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)), -//! ); -//! ``` +//! This module is private; the type is re-exported as [`crate::MdnsConfig`], where its +//! documentation and examples live so that rustdoc renders them and their doctests run. use std::net::IpAddr; use std::time::Duration; @@ -90,6 +42,55 @@ pub(crate) const RESPONSE_TTL: u32 = 120; /// - `query_timeout`: Maximum time to wait for a query answer (default: None - no timeout) /// - `local_names`: Names this connection will respond to (empty by default) /// - `local_addr`: IP address to advertise in responses (required for server mode) +/// +/// # Configuration scenarios +/// +/// ## Client MdnsConfiguration +/// +/// For a client that only sends queries: +/// +/// ```rust +/// use rtc_mdns::MdnsConfig; +/// use std::time::Duration; +/// +/// let config = MdnsConfig::default() +/// .with_query_interval(Duration::from_millis(500)); // Retry every 500ms +/// ``` +/// +/// ## Server MdnsConfiguration +/// +/// For a server that responds to queries: +/// +/// ```rust +/// use rtc_mdns::MdnsConfig; +/// use std::net::{IpAddr, Ipv4Addr}; +/// +/// let config = MdnsConfig::default() +/// .with_local_names(vec![ +/// "mydevice.local".to_string(), +/// "mydevice._http._tcp.local".to_string(), +/// ]) +/// .with_local_ip( +/// IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), +/// ); +/// ``` +/// +/// ## Combined Client/Server +/// +/// For a connection that both queries and responds: +/// +/// ```rust +/// use rtc_mdns::MdnsConfig; +/// use std::net::{IpAddr, Ipv4Addr}; +/// use std::time::Duration; +/// +/// let config = MdnsConfig::default() +/// .with_query_interval(Duration::from_secs(1)) +/// .with_local_names(vec!["myhost.local".to_string()]) +/// .with_local_ip( +/// IpAddr::V4(Ipv4Addr::new(192, 168, 1, 50)), +/// ); +/// ``` #[derive(Clone, Debug)] pub struct MdnsConfig { /// How often to retry unanswered queries. diff --git a/rtc-mdns/src/lib.rs b/rtc-mdns/src/lib.rs index 093f4383..f2352dd0 100644 --- a/rtc-mdns/src/lib.rs +++ b/rtc-mdns/src/lib.rs @@ -186,6 +186,7 @@ //! - **Compression**: DNS name compression is supported for efficiency #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] pub(crate) mod config; diff --git a/rtc-mdns/src/proto/mod.rs b/rtc-mdns/src/proto/mod.rs index 6180de14..9a2be429 100644 --- a/rtc-mdns/src/proto/mod.rs +++ b/rtc-mdns/src/proto/mod.rs @@ -1,70 +1,7 @@ -//! Sans-I/O mDNS Connection implementation. +//! The Sans-I/O mDNS connection. //! -//! This module provides [`Mdns`], a sans-I/O implementation of an mDNS client/server -//! that implements the [`sansio::Protocol`] trait for integration with any I/O framework. -//! -//! # Overview -//! -//! The [`Mdns`] struct handles the mDNS protocol logic without performing any I/O. -//! The caller is responsible for: -//! -//! 1. **Network I/O**: Reading/writing UDP packets to/from 224.0.0.251:5353 -//! 2. **Timing**: Calling `handle_timeout()` when `poll_timeout()` expires -//! 3. **Event Processing**: Handling events from `poll_event()` -//! -//! # Client Usage -//! -//! To query for a hostname: -//! -//! ```rust -//! use rtc_mdns::{MdnsConfig, Mdns, MdnsEvent}; -//! use sansio::Protocol; -//! use std::time::Instant; -//! -//! let mut mdns_client = Mdns::new(MdnsConfig::default()); -//! -//! // Start a query - this queues a packet to send -//! let query_id = mdns_client.query("printer.local"); -//! -//! // Get the packet to send over the network -//! if let Some(packet) = mdns_client.poll_write() { -//! // Send packet.message to packet.transport.peer_addr via UDP -//! println!("Send {} bytes to {}", packet.message.len(), packet.transport.peer_addr); -//! } -//! -//! // When a response packet arrives, call handle_read() -//! // Then check poll_event() for QueryAnswered events -//! ``` -//! -//! # Server Usage -//! -//! To respond to queries: -//! -//! ```rust -//! use rtc_mdns::{MdnsConfig, Mdns}; -//! use std::net::{IpAddr, Ipv4Addr}; -//! -//! let config = MdnsConfig::default() -//! .with_local_names(vec!["myserver.local".to_string()]) -//! .with_local_ip( -//! IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10)), -//! ); -//! -//! let mut mdns_client = Mdns::new(config); -//! -//! // When a query packet arrives, call handle_read() -//! // The connection automatically queues responses for configured local_names -//! // Retrieve them with poll_write() -//! ``` -//! -//! # Query Lifecycle -//! -//! 1. Call [`Mdns::query()`] with the hostname to resolve -//! 2. Retrieve the query packet from [`poll_write()`](sansio::Protocol::poll_write) -//! 3. Send the packet to the mDNS multicast address -//! 4. When responses arrive, pass them to [`handle_read()`](sansio::Protocol::handle_read) -//! 5. Check [`poll_event()`](sansio::Protocol::poll_event) for [`MdnsEvent::QueryAnswered`] -//! 6. If no answer, call [`handle_timeout()`](sansio::Protocol::handle_timeout) to trigger retries +//! This module is private; [`Mdns`] is re-exported as [`crate::Mdns`], where its +//! documentation and examples live so that rustdoc renders them and their doctests run. use std::collections::{HashMap, VecDeque}; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; @@ -148,6 +85,7 @@ pub struct Query { /// } /// ``` #[derive(Debug)] +#[non_exhaustive] pub enum MdnsEvent { /// A query was successfully answered. /// @@ -226,6 +164,8 @@ pub enum MdnsEvent { /// MdnsEvent::QueryTimeout(id) => { /// println!("Query {} timed out", id); /// } +/// // MdnsEvent is #[non_exhaustive]: a wildcard arm is required. +/// _ => {} /// } /// } /// ``` @@ -253,6 +193,24 @@ pub enum MdnsEvent { /// assert_eq!(mdns.pending_query_count(), 2); /// assert!(!mdns.is_query_pending(id2)); /// ``` +/// +/// # Overview +/// +/// The [`Mdns`] struct handles the mDNS protocol logic without performing any I/O. +/// The caller is responsible for: +/// +/// 1. **Network I/O**: Reading/writing UDP packets to/from 224.0.0.251:5353 +/// 2. **Timing**: Calling `handle_timeout()` when `poll_timeout()` expires +/// 3. **Event Processing**: Handling events from `poll_event()` +/// +/// # Query Lifecycle +/// +/// 1. Call [`Mdns::query()`] with the hostname to resolve +/// 2. Retrieve the query packet from [`poll_write()`](sansio::Protocol::poll_write) +/// 3. Send the packet to the mDNS multicast address +/// 4. When responses arrive, pass them to [`handle_read()`](sansio::Protocol::handle_read) +/// 5. Check [`poll_event()`](sansio::Protocol::poll_event) for [`MdnsEvent::QueryAnswered`] +/// 6. If no answer, call [`handle_timeout()`](sansio::Protocol::handle_timeout) to trigger retries pub struct Mdns { /// MdnsConfiguration config: MdnsConfig, diff --git a/rtc-mdns/src/socket.rs b/rtc-mdns/src/socket.rs index 270cda2f..7de17874 100644 --- a/rtc-mdns/src/socket.rs +++ b/rtc-mdns/src/socket.rs @@ -95,11 +95,18 @@ impl MulticastSocket { } } + /// Sets the local IPv4 address to bind the multicast socket to. + /// + /// Defaults to unspecified (`0.0.0.0`), letting the OS choose. pub fn with_multicast_local_ipv4(mut self, multicast_local_ipv4: Ipv4Addr) -> Self { self.multicast_local_ipv4 = Some(multicast_local_ipv4); self } + /// Sets the local port to bind the multicast socket to. + /// + /// Defaults to the mDNS port (5353), which is required to receive multicast queries + /// from other hosts; a different port is only useful for testing. pub fn with_multicast_local_port(mut self, multicast_local_port: u16) -> Self { self.multicast_local_port = Some(multicast_local_port); self diff --git a/rtc-mdns/tests/integration_test.rs b/rtc-mdns/tests/integration_test.rs index 5d21645a..3891e914 100644 --- a/rtc-mdns/tests/integration_test.rs +++ b/rtc-mdns/tests/integration_test.rs @@ -130,6 +130,7 @@ fn test_server_responds_to_query() { assert_eq!(addr, server_addr.ip()); } MdnsEvent::QueryTimeout(_) => panic!("Unexpected QueryTimeout"), + other => panic!("Unexpected event: {other:?}"), } // Query should no longer be pending diff --git a/rtc-media/Cargo.toml b/rtc-media/Cargo.toml index e5ba0b10..ea8cef99 100644 --- a/rtc-media/Cargo.toml +++ b/rtc-media/Cargo.toml @@ -22,7 +22,7 @@ thiserror.workspace = true [dev-dependencies] criterion.workspace = true -nearly_eq = "0.2" +nearly_eq.workspace = true [[bench]] name = "bench" diff --git a/rtc-media/src/audio/buffer.rs b/rtc-media/src/audio/buffer.rs index 93e4092a..d7a490cd 100644 --- a/rtc-media/src/audio/buffer.rs +++ b/rtc-media/src/audio/buffer.rs @@ -1,4 +1,16 @@ +//! Multi-channel audio buffers. +//! +//! A buffer is a flat slice of samples plus a [`BufferInfo`](crate::audio::buffer::BufferInfo) recording how many channels and +//! frames it holds. The layout is a type parameter: [`Interleaved`](crate::audio::buffer::layout::Interleaved) stores +//! one sample per channel per frame (what most audio APIs use), while +//! [`Deinterleaved`](crate::audio::buffer::layout::Deinterleaved) stores each channel contiguously. +//! +//! Encoding a layout in the type rather than a runtime flag means indexing is resolved at compile +//! time and the two cannot be mixed up. [`FromBytes`](crate::audio::buffer::FromBytes) and [`ToByteBufferRef`](crate::audio::buffer::ToByteBufferRef) convert to and from +//! raw bytes in a caller-chosen endianness. +/// Channel and frame counts for a buffer. pub mod info; +/// The interleaved and deinterleaved buffer layouts. pub mod layout; use std::mem::{ManuallyDrop, MaybeUninit}; @@ -10,16 +22,33 @@ pub use layout::BufferLayout; use layout::{Deinterleaved, Interleaved}; use thiserror::Error; +/// Decodes a buffer from raw little- or big-endian bytes. +/// +/// `L` is the [`BufferLayout`] the decoded samples are arranged in. pub trait FromBytes: Sized { + /// The error type produced by a failed conversion. type Error; + /// Decodes `channels` channels of samples from `bytes`, reading in byte order `B`. + /// + /// # Errors + /// + /// Fails if `bytes` is too short for a whole number of frames. fn from_bytes(bytes: &[u8], channels: usize) -> Result; } +/// Encodes a buffer into raw bytes in a caller-chosen endianness. pub trait ToByteBufferRef: Sized { + /// The error type produced by a failed conversion. type Error; + /// The number of bytes [`Self::to_bytes`] will write. fn bytes_len(&self); + /// Encodes the buffer into `bytes` in byte order `B`, returning the bytes written. + /// + /// # Errors + /// + /// Fails if `bytes` is too short. fn to_bytes( &self, bytes: &mut [u8], @@ -28,18 +57,30 @@ pub trait ToByteBufferRef: Sized { } #[derive(Debug, Error, PartialEq, Eq)] +/// Errors from converting between buffers and raw bytes. +#[non_exhaustive] pub enum Error { #[error("Unexpected end of buffer: (expected: {expected}, actual: {actual})")] - UnexpectedEndOfBuffer { expected: usize, actual: usize }, + /// The byte slice was too short to hold the expected number of samples. + UnexpectedEndOfBuffer { + /// Bytes required. + expected: usize, + /// Bytes available. + actual: usize, + }, } #[derive(Eq, PartialEq, Clone, Debug)] +/// A borrowed view of multi-channel audio: samples of type `T` in layout `L`. pub struct BufferRef<'a, T, L> { samples: &'a [T], info: BufferInfo, } impl<'a, T, L> BufferRef<'a, T, L> { + /// Wraps `samples` as `channels` interleaved or deinterleaved channels. + /// + /// The frame count is derived from the slice length, which must divide evenly by `channels`. pub fn new(samples: &'a [T], channels: usize) -> Self { debug_assert_eq!(samples.len() % channels, 0); let info = { @@ -58,6 +99,9 @@ pub struct Buffer { } impl Buffer { + /// Takes ownership of `samples` as `channels` channels. + /// + /// The frame count is derived from the length, which must divide evenly by `channels`. pub fn new(samples: Vec, channels: usize) -> Self { debug_assert_eq!(samples.len() % channels, 0); let info = { @@ -67,6 +111,7 @@ impl Buffer { Self { samples, info } } + /// Borrows the whole buffer as a [`BufferRef`]. pub fn as_ref(&'_ self) -> BufferRef<'_, T, L> { BufferRef { samples: &self.samples[..], @@ -74,6 +119,7 @@ impl Buffer { } } + /// Borrows a sample range of the buffer as a [`BufferRef`]. pub fn sub_range(&'_ self, range: Range) -> BufferRef<'_, T, L> { let samples_len = range.len(); let samples = &self.samples[range]; diff --git a/rtc-media/src/audio/buffer/info.rs b/rtc-media/src/audio/buffer/info.rs index bd70e12d..dbd1025c 100644 --- a/rtc-media/src/audio/buffer/info.rs +++ b/rtc-media/src/audio/buffer/info.rs @@ -3,6 +3,7 @@ use std::marker::PhantomData; use crate::audio::buffer::layout::{Deinterleaved, Interleaved}; #[derive(Eq, PartialEq, Debug)] +/// The channel and frame counts of a buffer, tagged with its layout `L`. pub struct BufferInfo { channels: usize, frames: usize, @@ -10,6 +11,7 @@ pub struct BufferInfo { } impl BufferInfo { + /// Describes a buffer of `channels` channels and `frames` frames per channel. pub fn new(channels: usize, frames: usize) -> Self { Self { channels, @@ -38,6 +40,7 @@ impl BufferInfo { self.frames = frames; } + /// The total sample count: channels times frames. pub fn samples(&self) -> usize { self.channels * self.frames } diff --git a/rtc-media/src/audio/buffer/layout.rs b/rtc-media/src/audio/buffer/layout.rs index d26f1fe7..625634ee 100644 --- a/rtc-media/src/audio/buffer/layout.rs +++ b/rtc-media/src/audio/buffer/layout.rs @@ -1,11 +1,18 @@ use crate::audio::buffer::BufferInfo; use crate::audio::sealed::Sealed; +/// How multi-channel samples are arranged in a flat buffer. +/// +/// Sealed: the only layouts are [`Interleaved`] and [`Deinterleaved`]. pub trait BufferLayout: Sized + Sealed { + /// The flat index of `frame` on `channel`, for a buffer described by `info`. fn index_of(info: &BufferInfo, channel: usize, frame: usize) -> usize; } #[derive(Eq, PartialEq, Copy, Clone, Debug)] +/// Channels stored one after another: all of channel 0, then all of channel 1. +/// +/// A marker type — it has no values. pub enum Deinterleaved {} impl Sealed for Deinterleaved {} @@ -18,6 +25,9 @@ impl BufferLayout for Deinterleaved { } #[derive(Eq, PartialEq, Copy, Clone, Debug)] +/// Frames stored one after another, each holding one sample per channel. +/// +/// The layout most audio APIs use, and a marker type with no values. pub enum Interleaved {} impl Sealed for Interleaved {} diff --git a/rtc-media/src/audio/mod.rs b/rtc-media/src/audio/mod.rs index e259ae9c..f6d53c68 100644 --- a/rtc-media/src/audio/mod.rs +++ b/rtc-media/src/audio/mod.rs @@ -1,3 +1,4 @@ +/// Multi-channel audio buffers in interleaved or deinterleaved layout. pub mod buffer; mod sample; diff --git a/rtc-media/src/audio/sample.rs b/rtc-media/src/audio/sample.rs index a98910a1..6b65c30d 100644 --- a/rtc-media/src/audio/sample.rs +++ b/rtc-media/src/audio/sample.rs @@ -6,6 +6,9 @@ use nearly_eq::NearlyEq; #[derive(Eq, PartialEq, Copy, Clone, Default, Debug)] #[repr(transparent)] +/// One audio sample of raw type `Raw` (`i16`, `f32`, …). +/// +/// A transparent newtype, so a `[Sample]` can be reinterpreted as `[T]` without copying. pub struct Sample(Raw); impl From for Sample { diff --git a/rtc-media/src/io/h26x_reader/mod.rs b/rtc-media/src/io/h26x_reader/mod.rs index 3f40df99..6cea4743 100644 --- a/rtc-media/src/io/h26x_reader/mod.rs +++ b/rtc-media/src/io/h26x_reader/mod.rs @@ -1,5 +1,14 @@ +//! Reading H.264/H.265 Annex B byte streams. +//! +//! Annex B delimits NAL units with start codes (`00 00 01` or `00 00 00 01`). This module walks +//! those boundaries and parses each unit's header, which differs between the codecs: H.264 uses +//! one byte ([`H264NAL`](crate::io::h26x_reader::H264NAL)), H.265 two ([`H265NAL`](crate::io::h26x_reader::H265NAL)). +//! +//! Use [`sample_reader`](crate::io::h26x_reader::sample_reader) instead when you want whole access units — the NAL units making up one +//! frame — rather than individual units. #[cfg(test)] mod h26x_reader_test; +/// Reads Annex B streams as whole samples rather than individual NAL units. pub mod sample_reader; pub use sample_reader::{H26xSample, H26xSampleReader}; @@ -53,6 +62,7 @@ impl ReadBuffer { /// H264NalUnitType is the type of a NAL /// Enums for H264NalUnitType #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum H264NalUnitType { /// Unspecified #[default] @@ -141,11 +151,14 @@ impl From for H264NalUnitType { /// NAL H.264 Network Abstraction Layer pub struct H264NAL { + /// The picture order count parsed from the slice header, which orders frames for display. pub picture_order_count: u32, /// NAL header pub forbidden_zero_bit: bool, + /// `nal_ref_idc`: how important this unit is as a reference; `0` means it is not referenced. pub ref_idc: u8, + /// The NAL unit type, which says whether this is a slice, SPS, PPS, and so on. pub unit_type: H264NalUnitType, /// header byte + rbsp @@ -153,6 +166,7 @@ pub struct H264NAL { } impl H264NAL { + /// Wraps `data` as a NAL unit, without parsing its header yet. pub fn new(data: BytesMut) -> Self { H264NAL { picture_order_count: 0, @@ -163,6 +177,7 @@ impl H264NAL { } } + /// Parses the NAL header out of the unit's bytes, filling in the fields above. pub fn parse_header(&mut self) { let first_byte = self.data[0]; self.forbidden_zero_bit = ((first_byte & 0x80) >> 7) == 1; // 0x80 = 0b10000000 @@ -174,6 +189,7 @@ impl H264NAL { /// H265NalUnitType is the type of a NAL unit in H.265/HEVC /// Based on ITU-T H.265 (04/2013) Table 7-1 #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum H265NalUnitType { /// Coded slice of a non-TSA, non-STSA trailing picture #[default] @@ -302,8 +318,11 @@ impl From for H265NalUnitType { pub struct H265NAL { /// NAL header (2 bytes for H.265) pub forbidden_zero_bit: bool, + /// The NAL unit type. pub unit_type: H265NalUnitType, + /// `nuh_layer_id`: the scalability layer this unit belongs to; `0` for the base layer. pub nuh_layer_id: u8, + /// `nuh_temporal_id_plus1`: the temporal sub-layer, offset by one. pub nuh_temporal_id_plus1: u8, /// NAL unit header (2 bytes) + rbsp @@ -311,6 +330,7 @@ pub struct H265NAL { } impl H265NAL { + /// Wraps `data` as a NAL unit, without parsing its header yet. pub fn new(data: BytesMut) -> Self { H265NAL { forbidden_zero_bit: false, @@ -321,6 +341,7 @@ impl H265NAL { } } + /// Parses the NAL header out of the unit's bytes, filling in the fields above. pub fn parse_header(&mut self) { if self.data.len() < 2 { return; @@ -346,12 +367,16 @@ impl H265NAL { } /// H26xNAL represents either an H264 or H265 NAL unit +#[non_exhaustive] pub enum H26xNAL { + /// An H.264 NAL unit. H264(H264NAL), + /// An H.265 (HEVC) NAL unit. H265(H265NAL), } impl H26xNAL { + /// The unit's bytes, whichever codec it is. pub fn data(&self) -> &BytesMut { match self { H26xNAL::H264(nal) => &nal.data, diff --git a/rtc-media/src/io/h26x_reader/sample_reader.rs b/rtc-media/src/io/h26x_reader/sample_reader.rs index f16d60a5..0e5947a8 100644 --- a/rtc-media/src/io/h26x_reader/sample_reader.rs +++ b/rtc-media/src/io/h26x_reader/sample_reader.rs @@ -7,11 +7,15 @@ use super::{H26xNAL, H26xReader, H264NalUnitType, H265NalUnitType}; const ANNEXB_START_CODE: [u8; 4] = [0x00, 0x00, 0x00, 0x01]; #[derive(Debug, Clone, PartialEq, Eq)] +/// One access unit read from an Annex B stream — the NAL units making up a single frame. pub struct H26xSample { + /// The sample's bytes, start codes included. pub data: Bytes, + /// Whether this sample advances presentation time, i.e. completes a frame. pub timed: bool, } +/// Reads whole samples from an H.264 or H.265 Annex B stream. pub struct H26xSampleReader { reader: H26xReader, is_hevc: bool, @@ -19,6 +23,9 @@ pub struct H26xSampleReader { } impl H26xSampleReader { + /// Wraps `reader`, buffering up to `capacity` bytes. + /// + /// Set `is_hevc` for H.265; the two codecs differ in how NAL headers are parsed. pub fn new(reader: R, capacity: usize, is_hevc: bool) -> Self { Self { reader: H26xReader::new(reader, capacity, is_hevc), @@ -27,6 +34,11 @@ impl H26xSampleReader { } } + /// Reads the next sample. + /// + /// # Errors + /// + /// Fails on an I/O error, or at end of stream. pub fn next_sample(&mut self) -> Result { loop { let nal = match self.reader.next_nal() { diff --git a/rtc-media/src/io/h26x_writer/mod.rs b/rtc-media/src/io/h26x_writer/mod.rs index d3332e8f..b27385a9 100644 --- a/rtc-media/src/io/h26x_writer/mod.rs +++ b/rtc-media/src/io/h26x_writer/mod.rs @@ -181,6 +181,8 @@ impl H26xWriter { self.buffer.clear(); } } + // Unknown payload kind: skip it rather than corrupt the Annex-B stream. + _ => {} } } diff --git a/rtc-media/src/io/ivf_reader/mod.rs b/rtc-media/src/io/ivf_reader/mod.rs index 18074abc..2b0b51eb 100644 --- a/rtc-media/src/io/ivf_reader/mod.rs +++ b/rtc-media/src/io/ivf_reader/mod.rs @@ -1,3 +1,8 @@ +//! Reading IVF files. +//! +//! IVF is the minimal container the VPx and AV1 tools use: a 32-byte file header naming the codec +//! and frame size, then a 12-byte header before each frame giving its length and timestamp. +//! [`IVFReader`](crate::io::ivf_reader::IVFReader) yields one frame at a time, which is the unit an RTP payloader wants. #[cfg(test)] mod ivf_reader_test; @@ -9,32 +14,47 @@ use bytes::BytesMut; use crate::io::ResetFn; use shared::error::{Error, Result}; +/// The four-byte signature every IVF file starts with. pub const IVF_FILE_HEADER_SIGNATURE: &[u8] = b"DKIF"; +/// The size of the IVF file header in bytes. pub const IVF_FILE_HEADER_SIZE: usize = 32; +/// The size of each IVF frame header in bytes. pub const IVF_FRAME_HEADER_SIZE: usize = 12; /// IVFFileHeader 32-byte header for IVF files /// #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub struct IVFFileHeader { - pub signature: [u8; 4], // 0-3 - pub version: u16, // 4-5 - pub header_size: u16, // 6-7 - pub four_cc: [u8; 4], // 8-11 - pub width: u16, // 12-13 - pub height: u16, // 14-15 + /// Bytes 0–3: always `DKIF`. + pub signature: [u8; 4], // 0-3 + /// Bytes 4–5: the format version, currently 0. + pub version: u16, // 4-5 + /// Bytes 6–7: the header length, normally 32. + pub header_size: u16, // 6-7 + /// Bytes 8–11: the codec FourCC, such as `VP80`, `VP90` or `AV01`. + pub four_cc: [u8; 4], // 8-11 + /// Bytes 12–13: frame width in pixels. + pub width: u16, // 12-13 + /// Bytes 14–15: frame height in pixels. + pub height: u16, // 14-15 + /// Bytes 16–19: the timebase denominator — the frame rate's numerator, confusingly. pub timebase_denominator: u32, // 16-19 - pub timebase_numerator: u32, // 20-23 - pub num_frames: u32, // 24-27 - pub unused: u32, // 28-31 + /// Bytes 20–23: the timebase numerator. + pub timebase_numerator: u32, // 20-23 + /// Bytes 24–27: the frame count, if the writer knew it. + pub num_frames: u32, // 24-27 + /// Bytes 28–31: reserved. + pub unused: u32, // 28-31 } /// IVFFrameHeader 12-byte header for IVF frames /// #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub struct IVFFrameHeader { + /// Bytes 0–3: the frame's payload length in bytes. pub frame_size: u32, // 0-3 - pub timestamp: u64, // 4-11 + /// Bytes 4–11: the frame's presentation timestamp, in timebase units. + pub timestamp: u64, // 4-11 } /// IVFReader is used to read IVF files and return frame payloads diff --git a/rtc-media/src/io/ivf_writer/mod.rs b/rtc-media/src/io/ivf_writer/mod.rs index bef35130..eee3061c 100644 --- a/rtc-media/src/io/ivf_writer/mod.rs +++ b/rtc-media/src/io/ivf_writer/mod.rs @@ -14,10 +14,14 @@ use shared::error::Result; /// Codec type for IVF writer #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[non_exhaustive] pub enum IvfCodec { #[default] + /// VP8, written with the `VP80` FourCC. Vp8, + /// VP9, written with the `VP90` FourCC. Vp9, + /// AV1, written with the `AV01` FourCC. Av1, } diff --git a/rtc-media/src/io/mod.rs b/rtc-media/src/io/mod.rs index a9c7d2af..eb8100a1 100644 --- a/rtc-media/src/io/mod.rs +++ b/rtc-media/src/io/mod.rs @@ -1,22 +1,41 @@ +/// Reads H.264/H.265 Annex B byte streams into NAL units. pub mod h26x_reader; +/// Writes H.264/H.265 Annex B byte streams. pub mod h26x_writer; +/// Reads IVF files, the simple container used for VP8, VP9 and AV1. pub mod ivf_reader; +/// Writes IVF files. pub mod ivf_writer; +/// Reads Ogg files carrying Opus audio. pub mod ogg_reader; +/// Writes Ogg files carrying Opus audio. pub mod ogg_writer; +/// Reassembles inbound RTP packets into complete media samples. pub mod sample_builder; use shared::error::Result; +/// A callback that produces a fresh reader or writer, used to restart a stream. pub type ResetFn = Box R>; // Writer defines an interface to handle // the creation of media files +/// A sink for RTP packets, such as a file in a container format. pub trait Writer { // Add the content of an RTP packet to the media + /// Writes one RTP packet's media to the sink. + /// + /// # Errors + /// + /// Fails on an I/O error, or if the packet cannot be depacketized for this format. fn write_rtp(&mut self, pkt: &rtp::Packet) -> Result<()>; // close the media // Note: close implementation must be idempotent + /// Finalizes the sink, writing any trailing header or index the format needs. + /// + /// # Errors + /// + /// Fails on an I/O error while flushing. fn close(&mut self) -> Result<()>; } diff --git a/rtc-media/src/io/ogg_reader/mod.rs b/rtc-media/src/io/ogg_reader/mod.rs index 5e714bd6..51545302 100644 --- a/rtc-media/src/io/ogg_reader/mod.rs +++ b/rtc-media/src/io/ogg_reader/mod.rs @@ -1,3 +1,12 @@ +//! Reading Opus audio from an Ogg container. +//! +//! An Ogg stream is a sequence of pages; the first two carry the Opus headers (`OpusHead`, then +//! `OpusTags`), and the rest carry audio packets. [`OggReader`](crate::io::ogg_reader::OggReader) walks the pages and hands back +//! one Opus packet at a time, which is exactly the unit RTP carries. +//! +//! The `OpusHead` fields worth noting are [`pre_skip`](crate::io::ogg_reader::OggHeader::pre_skip) — decoder warm-up +//! samples to discard — and [`sample_rate`](crate::io::ogg_reader::OggHeader::sample_rate), which records the *original* +//! input rate; Opus itself always decodes at 48 kHz. #[cfg(test)] mod ogg_reader_test; @@ -9,18 +18,29 @@ use bytes::BytesMut; use crate::io::ResetFn; use shared::error::{Error, Result}; +/// Page header flag: this page continues a packet from the previous page. pub const PAGE_HEADER_TYPE_CONTINUATION_OF_STREAM: u8 = 0x00; +/// Page header flag: the first page of a logical stream. pub const PAGE_HEADER_TYPE_BEGINNING_OF_STREAM: u8 = 0x02; +/// Page header flag: the last page of a logical stream. pub const PAGE_HEADER_TYPE_END_OF_STREAM: u8 = 0x04; +/// The recommended Opus pre-skip: 3840 samples (80 ms at 48 kHz) of decoder warm-up to +/// discard. pub const DEFAULT_PRE_SKIP: u16 = 3840; // 3840 recommended in the RFC +/// The four-byte signature that begins every Ogg page. pub const PAGE_HEADER_SIGNATURE: &[u8] = b"OggS"; +/// The signature of the Opus identification header. pub const ID_PAGE_SIGNATURE: &[u8] = b"OpusHead"; +/// The signature of the Opus comment header. pub const COMMENT_PAGE_SIGNATURE: &[u8] = b"OpusTags"; +/// The fixed part of an Ogg page header, in bytes, before the segment table. pub const PAGE_HEADER_SIZE: usize = 27; +/// The size of the `OpusHead` payload in bytes. pub const ID_PAGE_PAYLOAD_SIZE: usize = 19; /// Header type classification for Opus pages #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum OggHeaderType { /// OpusHead - Opus ID page OpusHead, @@ -41,14 +61,23 @@ pub struct OggReader { /// #[derive(Debug, Clone)] pub struct OggHeader { + /// The channel mapping family, which says how channels map to speakers. pub channel_map: u8, + /// The channel count. pub channels: u8, + /// A gain in Q7.8 dB to apply when decoding. pub output_gain: u16, + /// Samples to discard from the start of the stream — decoder warm-up. pub pre_skip: u16, + /// The original input sample rate. Opus always decodes at 48 kHz regardless. pub sample_rate: u32, + /// The `OpusHead` version, currently 1. pub version: u8, + /// The number of Opus streams, for mapping families above 0. pub stream_count: u8, + /// How many of those streams are coupled stereo pairs. pub coupled_count: u8, + /// Which stream channel feeds each output channel. pub channel_mapping: Vec, } @@ -56,14 +85,18 @@ pub struct OggHeader { /// #[derive(Debug, Clone, Default)] pub struct OpusTags { + /// The encoder that produced the file. pub vendor: String, + /// Metadata tags from the `OpusTags` header. pub user_comments: Vec, } /// A key-value pair from Vorbis comments #[derive(Debug, Clone)] pub struct UserComment { + /// The tag name, such as `TITLE` or `ARTIST`. pub comment: String, + /// The tag value. pub value: String, } @@ -72,6 +105,7 @@ pub struct UserComment { /// #[derive(Debug, Clone)] pub struct OggPageHeader { + /// The page's granule position: total decoded samples at 48 kHz through this page. pub granule_position: u64, /// Serial number of the logical bitstream (track) pub serial: u32, @@ -306,6 +340,11 @@ impl OggReader { // parse_next_page reads from stream and returns Ogg page payload, header, // and an error if there is incomplete page data. + /// Reads the next Ogg page, returning its payload and header. + /// + /// # Errors + /// + /// Fails on an I/O error, at end of stream, or if the page signature or checksum is wrong. pub fn parse_next_page(&mut self) -> Result<(BytesMut, OggPageHeader)> { let mut h = [0u8; PAGE_HEADER_SIZE]; self.reader.read_exact(&mut h)?; diff --git a/rtc-media/src/io/sample_builder/mod.rs b/rtc-media/src/io/sample_builder/mod.rs index 6f7893db..dbbce80f 100644 --- a/rtc-media/src/io/sample_builder/mod.rs +++ b/rtc-media/src/io/sample_builder/mod.rs @@ -3,6 +3,7 @@ mod sample_builder_test; #[cfg(test)] mod sample_sequence_location_test; +/// Tracks where a sample sits within the RTP sequence-number space. pub mod sample_sequence_location; use self::sample_sequence_location::{Comparison, SampleSequenceLocation}; @@ -69,6 +70,10 @@ impl SampleBuilder { } } + /// Sets how long to wait for a missing packet before giving up on the sample it belongs to. + /// + /// Bounds head-of-line blocking: without it a single lost packet would stall reassembly + /// indefinitely. pub fn with_max_time_delay(mut self, max_late_duration: Duration) -> Self { self.max_late_timestamp = (self.sample_rate as u128 * max_late_duration.as_millis() / 1000) as u32; diff --git a/rtc-media/src/lib.rs b/rtc-media/src/lib.rs index be7a610d..23e8efd7 100644 --- a/rtc-media/src/lib.rs +++ b/rtc-media/src/lib.rs @@ -1,8 +1,51 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! Media samples and container I/O. +//! +//! The bridge between encoded media and RTP: a codec-agnostic [`Sample`] type, and readers +//! and writers for the container formats the examples and tests use. +//! +//! # Structure +//! +//! * [`Sample`] — one encoded unit of media (a video frame, an audio frame) with its +//! duration, timestamp and packet metadata. Hand these to a sample-based local track and +//! the RTP packetizer does the rest. +//! * [`io`] — the [`Writer`](io::Writer) trait plus concrete readers and writers for IVF +//! (VP8/VP9), Ogg (Opus) and H.264/H.265 Annex B — [`IVFReader`](io::ivf_reader::IVFReader), +//! [`OggReader`](io::ogg_reader::OggReader) and friends — enough to play media from disk or +//! record it to disk. [`SampleBuilder`](io::sample_builder::SampleBuilder) goes the other +//! way, reassembling inbound RTP into [`Sample`]s. +//! * [`audio`], [`video`] — per-codec helpers, including audio buffering and frame +//! inspection. +//! +//! # Example +//! +//! ``` +//! use bytes::Bytes; +//! use rtc_media::Sample; +//! use shared::time::SystemInstant; +//! use std::time::Duration; +//! +//! // One encoded frame, ready to hand to a sample-based local track. +//! let sample = Sample { +//! data: Bytes::from_static(&[0u8; 128]), +//! timestamp: SystemInstant::now(), +//! duration: Duration::from_millis(33), // ~30 fps +//! ..Default::default() +//! }; +//! assert_eq!(sample.data.len(), 128); +//! ``` +//! +//! Most applications do not depend on this crate directly — the +//! [`rtc`](https://docs.rs/rtc) crate re-exports it as `rtc::media`. + +/// Audio sample types and multi-channel buffers. pub mod audio; +/// Container readers and writers, plus RTP sample reassembly. pub mod io; +/// Video frame helpers. pub mod video; use bytes::Bytes; diff --git a/rtc-rtcp/src/extended_report/dlrr.rs b/rtc-rtcp/src/extended_report/dlrr.rs index c8701f6c..449bf6ed 100644 --- a/rtc-rtcp/src/extended_report/dlrr.rs +++ b/rtc-rtcp/src/extended_report/dlrr.rs @@ -5,8 +5,11 @@ const DLRR_REPORT_LENGTH: u16 = 12; /// DLRRReport encodes a single report inside a DLRRReportBlock. #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct DLRRReport { + /// The SSRC this sub-block reports on. pub ssrc: u32, + /// The middle 32 bits of the NTP timestamp from that receiver's last Receiver Reference Time. pub last_rr: u32, + /// Delay since that report was received, in units of 1/65536 seconds. pub dlrr: u32, } @@ -36,6 +39,7 @@ impl fmt::Display for DLRRReport { /// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct DLRRReportBlock { + /// One sub-block per SSRC reported on. pub reports: Vec, } @@ -46,6 +50,7 @@ impl fmt::Display for DLRRReportBlock { } impl DLRRReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { XRHeader { block_type: BlockType::DLRR, diff --git a/rtc-rtcp/src/extended_report/mod.rs b/rtc-rtcp/src/extended_report/mod.rs index f515c4dd..4fe463b2 100644 --- a/rtc-rtcp/src/extended_report/mod.rs +++ b/rtc-rtcp/src/extended_report/mod.rs @@ -1,12 +1,19 @@ #[cfg(test)] mod extended_report_test; +/// Delay Since Last Receiver Report blocks, for round-trip time between receivers. pub mod dlrr; +/// Packet Receipt Times blocks. pub mod prt; +/// Loss and Duplicate RLE blocks, which run-length encode per-packet receipt. pub mod rle; +/// Receiver Reference Time blocks, which anchor DLRR round-trip calculations. pub mod rrt; +/// Statistics Summary blocks: loss, duplicate, jitter and TTL ranges. pub mod ssr; +/// An unparsed block, for types this crate does not model. pub mod unknown; +/// VoIP Metrics blocks, which carry call-quality estimates. pub mod vm; pub use dlrr::{DLRRReport, DLRRReportBlock}; @@ -33,16 +40,25 @@ const XR_HEADER_LENGTH: usize = 4; /// BlockType specifies the type of report in a report block /// Extended Report block types from RFC 3611. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum BlockType { #[default] + /// A block type this crate does not model. Unknown = 0, - LossRLE = 1, // RFC 3611, section 4.1 - DuplicateRLE = 2, // RFC 3611, section 4.2 - PacketReceiptTimes = 3, // RFC 3611, section 4.3 + /// Loss RLE report block ([RFC 3611] §4.1). + LossRLE = 1, // RFC 3611, section 4.1 + /// Duplicate RLE report block ([RFC 3611] §4.2). + DuplicateRLE = 2, // RFC 3611, section 4.2 + /// Packet Receipt Times report block ([RFC 3611] §4.3). + PacketReceiptTimes = 3, // RFC 3611, section 4.3 + /// Receiver Reference Time report block ([RFC 3611] §4.4). ReceiverReferenceTime = 4, // RFC 3611, section 4.4 - DLRR = 5, // RFC 3611, section 4.5 - StatisticsSummary = 6, // RFC 3611, section 4.6 - VoIPMetrics = 7, // RFC 3611, section 4.7 + /// Delay Since Last Receiver Report block ([RFC 3611] §4.5). + DLRR = 5, // RFC 3611, section 4.5 + /// Statistics Summary report block ([RFC 3611] §4.6). + StatisticsSummary = 6, // RFC 3611, section 4.6 + /// VoIP Metrics report block ([RFC 3611] §4.7). + VoIPMetrics = 7, // RFC 3611, section 4.7 } impl From for BlockType { @@ -90,8 +106,11 @@ pub type TypeSpecificField = u8; /// packet is marshaled. #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct XRHeader { + /// Which kind of report block follows. pub block_type: BlockType, + /// Bits whose meaning depends on the block type. pub type_specific: TypeSpecificField, + /// The block's length in 32-bit words, excluding this header. pub block_length: u16, } @@ -155,7 +174,9 @@ impl Unmarshal for XRHeader { /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #[derive(Debug, PartialEq, Default, Clone)] pub struct ExtendedReport { + /// The SSRC of the sender of this extended report. pub sender_ssrc: u32, + /// The report blocks this packet carries. pub reports: Vec>, } diff --git a/rtc-rtcp/src/extended_report/prt.rs b/rtc-rtcp/src/extended_report/prt.rs index ebeafa30..ce9ea791 100644 --- a/rtc-rtcp/src/extended_report/prt.rs +++ b/rtc-rtcp/src/extended_report/prt.rs @@ -25,12 +25,17 @@ const PRT_REPORT_BLOCK_MIN_LENGTH: u16 = 8; #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct PacketReceiptTimesReportBlock { //not included in marshal/unmarshal + /// The block's `T` field, which scales the receipt-time values. pub t: u8, //marshal/unmarshal + /// The SSRC whose packets are reported on. pub ssrc: u32, + /// The first sequence number covered by this block. pub begin_seq: u16, + /// One past the last sequence number covered. pub end_seq: u16, + /// Receipt time for each packet in the range, in the block's timestamp units. pub receipt_time: Vec, } @@ -41,6 +46,7 @@ impl fmt::Display for PacketReceiptTimesReportBlock { } impl PacketReceiptTimesReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { XRHeader { block_type: BlockType::PacketReceiptTimes, diff --git a/rtc-rtcp/src/extended_report/rle.rs b/rtc-rtcp/src/extended_report/rle.rs index a4c71a2a..7cebbc44 100644 --- a/rtc-rtcp/src/extended_report/rle.rs +++ b/rtc-rtcp/src/extended_report/rle.rs @@ -1,3 +1,10 @@ +//! Loss and Duplicate RLE report blocks. +//! +//! Both blocks report per-packet status over a sequence-number range, run-length encoded: a +//! [`Chunk`](crate::extended_report::rle::Chunk) is either a run of identical values or an explicit 15-bit vector. That keeps a report +//! covering a long range compact while still describing individual packets. +//! +//! The same structure serves both blocks; [`RLEReportBlock::is_loss_rle`](crate::extended_report::rle::RLEReportBlock::is_loss_rle) selects which. use super::*; const RLE_REPORT_BLOCK_MIN_LENGTH: u16 = 8; @@ -5,8 +12,11 @@ const RLE_REPORT_BLOCK_MIN_LENGTH: u16 = 8; /// ChunkType enumerates the three kinds of chunks described in RFC 3611 section 4.1. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ChunkType { + /// A run-length chunk: a bit value repeated a stated number of times. RunLength = 0, + /// A bit-vector chunk: 15 explicit per-packet bits. BitVector = 1, + /// The terminating null chunk, which pads the block to a word boundary. TerminatingNull = 2, } @@ -105,13 +115,19 @@ impl Chunk { #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct RLEReportBlock { //not included in marshal/unmarshal + /// `true` for a Loss RLE block, `false` for a Duplicate RLE block. pub is_loss_rle: bool, + /// The block's `T` field. pub t: u8, //marshal/unmarshal + /// The SSRC whose packets are reported on. pub ssrc: u32, + /// The first sequence number covered by this block. pub begin_seq: u16, + /// One past the last sequence number covered. pub end_seq: u16, + /// The run-length and bit-vector chunks encoding per-packet status. pub chunks: Vec, } @@ -126,6 +142,7 @@ pub type LossRLEReportBlock = RLEReportBlock; pub type DuplicateRLEReportBlock = RLEReportBlock; impl RLEReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { XRHeader { block_type: if self.is_loss_rle { diff --git a/rtc-rtcp/src/extended_report/rrt.rs b/rtc-rtcp/src/extended_report/rrt.rs index 46d4562c..d95ab0c4 100644 --- a/rtc-rtcp/src/extended_report/rrt.rs +++ b/rtc-rtcp/src/extended_report/rrt.rs @@ -16,6 +16,7 @@ const RRT_REPORT_BLOCK_LENGTH: u16 = 8; /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct ReceiverReferenceTimeReportBlock { + /// The receiver's NTP timestamp, which DLRR blocks refer back to. pub ntp_timestamp: u64, } @@ -26,6 +27,7 @@ impl fmt::Display for ReceiverReferenceTimeReportBlock { } impl ReceiverReferenceTimeReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { XRHeader { block_type: BlockType::ReceiverReferenceTime, diff --git a/rtc-rtcp/src/extended_report/ssr.rs b/rtc-rtcp/src/extended_report/ssr.rs index 03916cd4..ae3aceaf 100644 --- a/rtc-rtcp/src/extended_report/ssr.rs +++ b/rtc-rtcp/src/extended_report/ssr.rs @@ -31,24 +31,41 @@ const SSR_REPORT_BLOCK_LENGTH: u16 = 4 + 2 * 2 + 4 * 6 + 4; #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct StatisticsSummaryReportBlock { //not included in marshal/unmarshal + /// Whether the loss fields are present. pub loss_reports: bool, + /// Whether the duplicate fields are present. pub duplicate_reports: bool, + /// Whether the jitter fields are present. pub jitter_reports: bool, + /// Whether the TTL fields hold an IPv4 TTL, an IPv6 hop limit, or nothing. pub ttl_or_hop_limit: TTLorHopLimitType, //marshal/unmarshal + /// The SSRC being summarized. pub ssrc: u32, + /// The first sequence number covered. pub begin_seq: u16, + /// One past the last sequence number covered. pub end_seq: u16, + /// Packets lost in the interval. pub lost_packets: u32, + /// Duplicate packets in the interval. pub dup_packets: u32, + /// Minimum observed jitter, in RTP timestamp units. pub min_jitter: u32, + /// Maximum observed jitter. pub max_jitter: u32, + /// Mean observed jitter. pub mean_jitter: u32, + /// Standard deviation of observed jitter. pub dev_jitter: u32, + /// Minimum TTL or hop limit seen. pub min_ttl_or_hl: u8, + /// Maximum TTL or hop limit seen. pub max_ttl_or_hl: u8, + /// Mean TTL or hop limit. pub mean_ttl_or_hl: u8, + /// Standard deviation of TTL or hop limit. pub dev_ttl_or_hl: u8, } @@ -63,8 +80,11 @@ impl fmt::Display for StatisticsSummaryReportBlock { #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum TTLorHopLimitType { #[default] + /// No TTL or hop-limit data is present. Missing = 0, + /// The fields hold an IPv4 TTL. IPv4 = 1, + /// The fields hold an IPv6 hop limit. IPv6 = 2, } @@ -90,6 +110,7 @@ impl fmt::Display for TTLorHopLimitType { } impl StatisticsSummaryReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { let mut type_specific = 0x00; if self.loss_reports { diff --git a/rtc-rtcp/src/extended_report/unknown.rs b/rtc-rtcp/src/extended_report/unknown.rs index d6a4de4b..768d6e06 100644 --- a/rtc-rtcp/src/extended_report/unknown.rs +++ b/rtc-rtcp/src/extended_report/unknown.rs @@ -4,6 +4,7 @@ use super::*; /// that has an unknown Report Block Type. #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct UnknownReportBlock { + /// The block's bytes, left unparsed. pub bytes: Bytes, } @@ -14,6 +15,7 @@ impl fmt::Display for UnknownReportBlock { } impl UnknownReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { XRHeader { block_type: BlockType::Unknown, diff --git a/rtc-rtcp/src/extended_report/vm.rs b/rtc-rtcp/src/extended_report/vm.rs index 0bf0d8c8..f7ba16e5 100644 --- a/rtc-rtcp/src/extended_report/vm.rs +++ b/rtc-rtcp/src/extended_report/vm.rs @@ -28,27 +28,49 @@ const VM_REPORT_BLOCK_LENGTH: u16 = 4 + 4 + 2 * 4 + 10 + 2 * 3; /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct VoIPMetricsReportBlock { + /// The SSRC these metrics describe. pub ssrc: u32, + /// Fraction of packets lost, as 256ths. pub loss_rate: u8, + /// Fraction of packets discarded by the jitter buffer, as 256ths. pub discard_rate: u8, + /// Fraction of packets lost or discarded during bursts, as 256ths. pub burst_density: u8, + /// Fraction lost or discarded during gaps between bursts, as 256ths. pub gap_density: u8, + /// Mean burst length in milliseconds. pub burst_duration: u16, + /// Mean gap length in milliseconds. pub gap_duration: u16, + /// Round-trip delay in milliseconds. pub round_trip_delay: u16, + /// End-system delay in milliseconds, covering encode, buffer and decode. pub end_system_delay: u16, + /// Signal level in dBm0. pub signal_level: u8, + /// Noise level in dBm0. pub noise_level: u8, + /// Residual echo return loss, in dB. pub rerl: u8, + /// The gap threshold: consecutive received packets needed to end a burst. pub gmin: u8, + /// The R factor, a 0–100 voice-quality estimate. pub rfactor: u8, + /// An external-network R factor, for the segment beyond this one. pub ext_rfactor: u8, + /// Listening-quality mean opinion score, in tenths. pub mos_lq: u8, + /// Conversational-quality mean opinion score, in tenths. pub mos_cq: u8, + /// Receiver configuration: packet-loss concealment and jitter-buffer flags. pub rx_config: u8, + /// Reserved; sent as zero. pub reserved: u8, + /// Nominal jitter-buffer delay in milliseconds. pub jb_nominal: u16, + /// Maximum jitter-buffer delay in milliseconds. pub jb_maximum: u16, + /// The absolute maximum the jitter buffer can grow to, in milliseconds. pub jb_abs_max: u16, } @@ -59,6 +81,7 @@ impl fmt::Display for VoIPMetricsReportBlock { } impl VoIPMetricsReportBlock { + /// The XR block header describing this block's type and length. pub fn xr_header(&self) -> XRHeader { XRHeader { block_type: BlockType::VoIPMetrics, diff --git a/rtc-rtcp/src/header.rs b/rtc-rtcp/src/header.rs index 4731f985..2b0b3946 100644 --- a/rtc-rtcp/src/header.rs +++ b/rtc-rtcp/src/header.rs @@ -1,3 +1,11 @@ +//! The RTCP header. +//! +//! Four bytes on every packet: version and padding flags, a 5-bit count whose meaning depends on +//! the packet type, the [`PacketType`](crate::header::PacketType) itself, and a length in 32-bit words. The `*_SHIFT` and +//! `*_MASK` constants describe how the flags pack into the first octet. +//! +//! The length field is why RTCP is compound: several packets can be concatenated in one +//! datagram and walked by stepping over each header's length. use shared::{ error::{Error, Result}, marshal::{Marshal, MarshalSize, Unmarshal}, @@ -9,17 +17,27 @@ use bytes::{Buf, BufMut}; /// RTCP packet types registered with IANA. See: #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] #[repr(u8)] +#[non_exhaustive] pub enum PacketType { #[default] + /// A packet type this crate does not model. Unsupported = 0, - SenderReport = 200, // RFC 3550, 6.4.1 - ReceiverReport = 201, // RFC 3550, 6.4.2 - SourceDescription = 202, // RFC 3550, 6.5 - Goodbye = 203, // RFC 3550, 6.6 - ApplicationDefined = 204, // RFC 3550, 6.7 (unimplemented) + /// Sender Report ([RFC 3550] §6.4.1): a sender's timing and packet counts. + SenderReport = 200, // RFC 3550, 6.4.1 + /// Receiver Report ([RFC 3550] §6.4.2): reception quality from a receiver. + ReceiverReport = 201, // RFC 3550, 6.4.2 + /// Source Description ([RFC 3550] §6.5): CNAME and other source metadata. + SourceDescription = 202, // RFC 3550, 6.5 + /// BYE ([RFC 3550] §6.6): the source is leaving the session. + Goodbye = 203, // RFC 3550, 6.6 + /// APP ([RFC 3550] §6.7): application-defined data. Not modelled by this crate. + ApplicationDefined = 204, // RFC 3550, 6.7 (unimplemented) + /// Transport-layer feedback ([RFC 4585]): NACK and transport-wide CC. TransportSpecificFeedback = 205, // RFC 4585, 6051 - PayloadSpecificFeedback = 206, // RFC 4585, 6.3 - ExtendedReport = 207, // RFC 3611 + /// Payload-specific feedback ([RFC 4585] §6.3): PLI, FIR, SLI, REMB. + PayloadSpecificFeedback = 206, // RFC 4585, 6.3 + /// Extended Report ([RFC 3611]). + ExtendedReport = 207, // RFC 3611 } /// Transport and Payload specific feedback messages overload the count field to act as a message type. those are listed here @@ -73,17 +91,28 @@ impl From for PacketType { } } +/// The RTP/RTCP version this crate speaks. pub const RTP_VERSION: u8 = 2; +/// Bit offset of the version field in the first header octet. pub const VERSION_SHIFT: u8 = 6; +/// Bit mask of the version field once shifted. pub const VERSION_MASK: u8 = 0x3; +/// Bit offset of the padding flag. pub const PADDING_SHIFT: u8 = 5; +/// Bit mask of the padding flag once shifted. pub const PADDING_MASK: u8 = 0x1; +/// Bit offset of the report/source count field. pub const COUNT_SHIFT: u8 = 0; +/// Bit mask of the report/source count field. pub const COUNT_MASK: u8 = 0x1f; +/// Length of the RTCP header in bytes. pub const HEADER_LENGTH: usize = 4; +/// The largest report count the 5-bit field can hold. pub const COUNT_MAX: usize = (1 << 5) - 1; +/// Length of an SSRC in bytes. pub const SSRC_LENGTH: usize = 4; +/// The longest SDES item value, bounded by its one-byte length field. pub const SDES_MAX_OCTET_COUNT: usize = (1 << 8) - 1; // https://datatracker.ietf.org/doc/html/rfc5104#section-4.3.1 @@ -94,6 +123,7 @@ pub const SDES_MAX_OCTET_COUNT: usize = (1 << 8) - 1; // // The length of the FIR feedback message MUST be set to // 2+2*N, where N is the number of FCI entries. +/// The smallest valid FIR packet, in bytes. pub const FIR_MIN_OCTET_COUNT: usize = 20; /// A Header is the common header shared by all RTCP packets diff --git a/rtc-rtcp/src/lib.rs b/rtc-rtcp/src/lib.rs index b05ea3cb..6fddf8c7 100644 --- a/rtc-rtcp/src/lib.rs +++ b/rtc-rtcp/src/lib.rs @@ -1,4 +1,5 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] //! Package rtcp implements encoding and decoding of RTCP packets according to RFCs 3550 and 5506. @@ -14,44 +15,75 @@ //! in a streaming multimedia session. An application may use this information to control quality of //! service parameters, perhaps by limiting flow, or using a different codec. //! -//! Decoding RTCP packets: -//!```nobuild -//! let pkt = rtcp::unmarshal(&rtcp_data).unwrap(); -//! -//! if let Some(e) = pkt -//! .as_any() -//! .downcast_ref::() -//! { -//! -//! } -//! else if let Some(e) = packet -//! .as_any() -//! .downcast_ref::(){} -//! .... -//!``` -//! -//! Encoding RTCP packets: -//!```nobuild -//! let pkt = PictureLossIndication{ -//! sender_ssrc: sender_ssrc, -//! media_ssrc: media_ssrc -//! }; -//! -//! let pli_data = pkt.marshal().unwrap(); -//! // ... -//!``` +//! # Decoding RTCP packets +//! +//! One datagram may hold several packets — RTCP is compound — so [`packet::unmarshal`] +//! consumes the buffer and returns them all. Recover each concrete type by downcasting: +//! +//! ``` +//! use bytes::Bytes; +//! use rtc_rtcp::goodbye::Goodbye; +//! use rtc_rtcp::packet::unmarshal; +//! use rtc_rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; +//! +//! # fn example(rtcp_data: Bytes) -> Result<(), Box> { +//! let mut buf = rtcp_data; +//! for packet in unmarshal(&mut buf)? { +//! if let Some(pli) = packet.as_any().downcast_ref::() { +//! println!("keyframe requested for ssrc {}", pli.media_ssrc); +//! } else if let Some(bye) = packet.as_any().downcast_ref::() { +//! println!("{:?} left the session", bye.sources); +//! } +//! } +//! # Ok(()) +//! # } +//! ``` +//! +//! # Encoding RTCP packets +//! +//! Every packet type implements [`Marshal`](shared::marshal::Marshal), so that trait must be +//! in scope: +//! +//! ``` +//! use rtc_rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; +//! use shared::marshal::Marshal; +//! +//! # fn example(sender_ssrc: u32, media_ssrc: u32) -> Result<(), Box> { +//! let pli = PictureLossIndication { +//! sender_ssrc, +//! media_ssrc, +//! }; +//! let pli_data = pli.marshal()?; +//! # Ok(()) +//! # } +//! ``` +/// Compound RTCP packets — the several reports that share one datagram. pub mod compound_packet; +/// Extended reports (XR, [RFC 3611]): loss/discard run lengths, receipt times and per-block +/// statistics. +/// +/// [RFC 3611]: https://datatracker.ietf.org/doc/html/rfc3611 pub mod extended_report; +/// The BYE packet, by which a source announces it is leaving. pub mod goodbye; +/// The four-byte header common to every RTCP packet. pub mod header; +/// The [`Packet`] trait every RTCP packet type implements. pub mod packet; +/// Payload-specific feedback (PT 206): PLI, FIR, SLI and REMB. pub mod payload_feedbacks; +/// An unparsed packet, used for types this crate does not model. pub mod raw_packet; +/// The Receiver Report (RR), which reports reception quality back to a sender. pub mod receiver_report; +/// The reception report block carried inside SR and RR packets. pub mod reception_report; +/// The Sender Report (SR), which carries a sender's timing and packet counts. pub mod sender_report; +/// The SDES packet, which carries CNAME and other source metadata. pub mod source_description; +/// Transport-specific feedback (PT 205): NACK and transport-wide congestion control. pub mod transport_feedbacks; mod util; diff --git a/rtc-rtcp/src/packet.rs b/rtc-rtcp/src/packet.rs index 7a650398..95ba43d8 100644 --- a/rtc-rtcp/src/packet.rs +++ b/rtc-rtcp/src/packet.rs @@ -20,11 +20,20 @@ use std::fmt; /// Packet represents an RTCP packet, a protocol used for out-of-band statistics and /// control information for an RTP session pub trait Packet: Send + Sync + Marshal + Unmarshal + fmt::Display + fmt::Debug { + /// This packet's RTCP header. fn header(&self) -> Header; + /// The SSRCs this packet is about. + /// + /// Used to route feedback: a report's destination SSRCs identify the streams it concerns, + /// which is how an SFU decides where to forward it. fn destination_ssrc(&self) -> Vec; + /// The encoded size in bytes, header and padding included. fn raw_size(&self) -> usize; + /// Downcasting hook, so a caller holding `Box` can recover the concrete type. fn as_any(&self) -> &dyn Any; + /// Compares against another packet, since `PartialEq` is not object safe. fn equal(&self, other: &dyn Packet) -> bool; + /// Clones this packet behind a trait object, since `Clone` is not object safe. fn cloned(&self) -> Box; } diff --git a/rtc-rtcp/src/payload_feedbacks/full_intra_request/mod.rs b/rtc-rtcp/src/payload_feedbacks/full_intra_request/mod.rs index 547180d4..1213aebc 100644 --- a/rtc-rtcp/src/payload_feedbacks/full_intra_request/mod.rs +++ b/rtc-rtcp/src/payload_feedbacks/full_intra_request/mod.rs @@ -14,7 +14,9 @@ use std::fmt; /// A FIREntry is a (ssrc, seqno) pair, as carried by FullIntraRequest. #[derive(Debug, PartialEq, Eq, Default, Clone)] pub struct FirEntry { + /// The SSRC being asked for an intra frame. pub ssrc: u32, + /// A counter incremented per request, so a sender can ignore retransmitted duplicates. pub sequence_number: u8, } @@ -23,8 +25,11 @@ pub struct FirEntry { /// recovery, which should use PictureLossIndication (PLI) instead. #[derive(Debug, PartialEq, Eq, Default, Clone)] pub struct FullIntraRequest { + /// The SSRC of the requesting receiver. pub sender_ssrc: u32, + /// The media source being addressed. pub media_ssrc: u32, + /// One entry per SSRC an intra frame is requested from. pub fir: Vec, } diff --git a/rtc-rtcp/src/payload_feedbacks/mod.rs b/rtc-rtcp/src/payload_feedbacks/mod.rs index e7d02f17..7e29db55 100644 --- a/rtc-rtcp/src/payload_feedbacks/mod.rs +++ b/rtc-rtcp/src/payload_feedbacks/mod.rs @@ -1,4 +1,8 @@ +/// FIR: asks a sender for a full intra frame, used when a receiver joins or resyncs. pub mod full_intra_request; +/// PLI: tells a sender the receiver lost picture and needs a keyframe. pub mod picture_loss_indication; +/// REMB: the receiver's estimate of the bitrate the path can carry. pub mod receiver_estimated_maximum_bitrate; +/// SLI: reports individual lost slices, for finer-grained repair than PLI. pub mod slice_loss_indication; diff --git a/rtc-rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/mod.rs b/rtc-rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/mod.rs index 4d15aca2..291f2f15 100644 --- a/rtc-rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/mod.rs +++ b/rtc-rtcp/src/payload_feedbacks/receiver_estimated_maximum_bitrate/mod.rs @@ -35,7 +35,7 @@ const SSRC_ENTRY_OFFSET: usize = 20; /// Keep a table of powers to units for fast conversion. const BIT_UNITS: [&str; 7] = ["b", "Kb", "Mb", "Gb", "Tb", "Pb", "Eb"]; -const UNIQUE_IDENTIFIER: [u8; 4] = [b'R', b'E', b'M', b'B']; +const UNIQUE_IDENTIFIER: [u8; 4] = *b"REMB"; /// String prints the REMB packet in a human-readable format. impl fmt::Display for ReceiverEstimatedMaximumBitrate { diff --git a/rtc-rtcp/src/payload_feedbacks/slice_loss_indication/mod.rs b/rtc-rtcp/src/payload_feedbacks/slice_loss_indication/mod.rs index a82bcb90..c0ae0fef 100644 --- a/rtc-rtcp/src/payload_feedbacks/slice_loss_indication/mod.rs +++ b/rtc-rtcp/src/payload_feedbacks/slice_loss_indication/mod.rs @@ -34,6 +34,7 @@ pub struct SliceLossIndication { /// SSRC of the media source pub media_ssrc: u32, + /// The lost slices being reported. pub sli_entries: Vec, } diff --git a/rtc-rtcp/src/source_description/mod.rs b/rtc-rtcp/src/source_description/mod.rs index e9d163c3..1ccc696a 100644 --- a/rtc-rtcp/src/source_description/mod.rs +++ b/rtc-rtcp/src/source_description/mod.rs @@ -23,17 +23,30 @@ const SDES_TEXT_OFFSET: usize = 2; /// RTP SDES item types registered with IANA. See: #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] #[repr(u8)] +#[non_exhaustive] pub enum SdesType { #[default] + /// End of the SDES item list ([RFC 3550] §6.5). SdesEnd = 0, // end of SDES list RFC 3550, 6.5 - SdesCname = 1, // canonical name RFC 3550, 6.5.1 - SdesName = 2, // user name RFC 3550, 6.5.2 - SdesEmail = 3, // user's electronic mail address RFC 3550, 6.5.3 - SdesPhone = 4, // user's phone number RFC 3550, 6.5.4 + /// CNAME: the canonical end-point identifier, which ties an SSRC to a participant. + /// + /// The one item WebRTC always sends — it is how a receiver associates streams that belong + /// together. + SdesCname = 1, // canonical name RFC 3550, 6.5.1 + /// NAME: the participant's display name. + SdesName = 2, // user name RFC 3550, 6.5.2 + /// EMAIL: the participant's email address. + SdesEmail = 3, // user's electronic mail address RFC 3550, 6.5.3 + /// PHONE: the participant's phone number. + SdesPhone = 4, // user's phone number RFC 3550, 6.5.4 + /// LOC: the participant's geographic location. SdesLocation = 5, // geographic user location RFC 3550, 6.5.5 - SdesTool = 6, // name of application or tool RFC 3550, 6.5.6 - SdesNote = 7, // notice about the source RFC 3550, 6.5.7 - SdesPrivate = 8, // private extensions RFC 3550, 6.5.8 (not implemented) + /// TOOL: the name and version of the sending application. + SdesTool = 6, // name of application or tool RFC 3550, 6.5.6 + /// NOTE: a transient note about the source, such as "on hold". + SdesNote = 7, // notice about the source RFC 3550, 6.5.7 + /// PRIV: a private extension. + SdesPrivate = 8, // private extensions RFC 3550, 6.5.8 (not implemented) } impl fmt::Display for SdesType { @@ -74,6 +87,7 @@ impl From for SdesType { pub struct SourceDescriptionChunk { /// The source (ssrc) or contributing source (csrc) identifier this packet describes pub source: u32, + /// The items describing this source. pub items: Vec, } @@ -272,6 +286,7 @@ impl Unmarshal for SourceDescriptionItem { /// A SourceDescription (SDES) packet describes the sources in an RTP stream. #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct SourceDescription { + /// One chunk per source described by this packet. pub chunks: Vec, } diff --git a/rtc-rtcp/src/transport_feedbacks/mod.rs b/rtc-rtcp/src/transport_feedbacks/mod.rs index f59db607..6176fe39 100644 --- a/rtc-rtcp/src/transport_feedbacks/mod.rs +++ b/rtc-rtcp/src/transport_feedbacks/mod.rs @@ -1,3 +1,6 @@ +/// RRR: asks a sender to resynchronize as quickly as it can. pub mod rapid_resynchronization_request; +/// Transport-wide congestion control feedback: per-packet arrival status and deltas. pub mod transport_layer_cc; +/// Generic NACK, which lists sequence numbers the receiver did not get. pub mod transport_layer_nack; diff --git a/rtc-rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs b/rtc-rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs index a352a290..bc057388 100644 --- a/rtc-rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs +++ b/rtc-rtcp/src/transport_feedbacks/transport_layer_cc/mod.rs @@ -1,3 +1,11 @@ +//! Transport-wide congestion control feedback. +//! +//! Reports the arrival status and time of packets by their *transport-wide* sequence number — the +//! one the `TransportCcExtension` RTP header extension stamps on every packet +//! regardless of stream, which is why one report can cover audio and video together. +//! +//! Status is run-length or bit-vector encoded ([`StatusChunkTypeTcc`](crate::transport_feedbacks::transport_layer_cc::StatusChunkTypeTcc)) so a report covering +//! hundreds of packets stays small. #[cfg(test)] mod transport_layer_cc_test; @@ -49,7 +57,9 @@ use std::fmt; #[repr(u16)] pub enum StatusChunkTypeTcc { #[default] + /// A run-length chunk: one status repeated for a stated number of packets. RunLengthChunk = 0, + /// A status-vector chunk: explicit per-packet status for a small group. StatusVectorChunk = 1, } @@ -76,6 +86,7 @@ pub enum SymbolSizeTypeTcc { /// #[default] OneBit = 0, + /// Two-bit symbols, which can also express "received, large delta". TwoBit = 1, } @@ -112,7 +123,9 @@ impl From for SymbolTypeTcc { /// RunLengthChunk and StatusVectorChunk #[derive(Debug, Clone, PartialEq, Eq)] pub enum PacketStatusChunk { + /// A run-length encoded chunk. RunLengthChunk(RunLengthChunk), + /// A status-vector chunk. StatusVectorChunk(StatusVectorChunk), } @@ -313,6 +326,7 @@ impl Unmarshal for StatusVectorChunk { /// #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct RecvDelta { + /// Which symbol size this chunk uses. pub type_tcc_packet: SymbolTypeTcc, /// us pub delta: i64, diff --git a/rtc-rtcp/src/transport_feedbacks/transport_layer_nack/mod.rs b/rtc-rtcp/src/transport_feedbacks/transport_layer_nack/mod.rs index 2639334a..1dfdcf9b 100644 --- a/rtc-rtcp/src/transport_feedbacks/transport_layer_nack/mod.rs +++ b/rtc-rtcp/src/transport_feedbacks/transport_layer_nack/mod.rs @@ -25,6 +25,7 @@ pub struct NackPair { pub lost_packets: PacketBitmap, } +/// Iterates the individual sequence numbers a [`NackPair`] encodes. pub struct NackIterator { packet_id: u16, bitfield: PacketBitmap, @@ -58,6 +59,7 @@ impl Iterator for NackIterator { } impl NackPair { + /// A NACK pair naming a single lost sequence number, with no additional bitmask bits set. pub fn new(seq: u16) -> Self { Self { packet_id: seq, @@ -70,6 +72,9 @@ impl NackPair { self.into_iter().collect() } + /// Calls `f` with every sequence number this pair reports lost. + /// + /// Stops early if `f` returns `false`. pub fn range(&self, f: F) where F: Fn(u16) -> bool, @@ -119,6 +124,7 @@ pub struct TransportLayerNack { /// SSRC of the media source pub media_ssrc: u32, + /// The lost-packet ranges being reported. pub nacks: Vec, } @@ -252,6 +258,9 @@ impl Unmarshal for TransportLayerNack { } } +/// Packs a list of lost sequence numbers into the smallest set of [`NackPair`]s. +/// +/// Each pair covers a base sequence number plus the next 16, so nearby losses share one pair. pub fn nack_pairs_from_sequence_numbers(seq_nos: &[u16]) -> Vec { if seq_nos.is_empty() { return vec![]; diff --git a/rtc-rtp/Cargo.toml b/rtc-rtp/Cargo.toml index de3473f2..5e01ccb8 100644 --- a/rtc-rtp/Cargo.toml +++ b/rtc-rtp/Cargo.toml @@ -17,7 +17,7 @@ shared = { workspace = true, default-features = false, features = ["marshal"] } bytes.workspace = true rand.workspace = true serde.workspace = true -memchr = "2.1.1" +memchr.workspace = true [dev-dependencies] chrono.workspace = true diff --git a/rtc-rtp/src/codec/av1/depacketizer.rs b/rtc-rtp/src/codec/av1/depacketizer.rs index b226e1a1..dc77909a 100644 --- a/rtc-rtp/src/codec/av1/depacketizer.rs +++ b/rtc-rtp/src/codec/av1/depacketizer.rs @@ -34,6 +34,7 @@ pub struct Av1Depacketizer { } impl Av1Depacketizer { + /// An AV1 depacketizer with no buffered fragments. pub fn new() -> Self { Self::default() } diff --git a/rtc-rtp/src/codec/av1/mod.rs b/rtc-rtp/src/codec/av1/mod.rs index 688e23f9..911cddb7 100644 --- a/rtc-rtp/src/codec/av1/mod.rs +++ b/rtc-rtp/src/codec/av1/mod.rs @@ -18,6 +18,7 @@ mod packetizer; pub use depacketizer::Av1Depacketizer; #[derive(Default, Clone, Debug)] +/// Packetizes AV1 temporal units into RTP payloads. pub struct Av1Payloader {} impl Payloader for Av1Payloader { diff --git a/rtc-rtp/src/codec/g7xx/mod.rs b/rtc-rtp/src/codec/g7xx/mod.rs index 5af73ac0..a695e673 100644 --- a/rtc-rtp/src/codec/g7xx/mod.rs +++ b/rtc-rtp/src/codec/g7xx/mod.rs @@ -12,6 +12,7 @@ pub type G711Payloader = G7xxPayloader; pub type G722Payloader = G7xxPayloader; #[derive(Default, Debug, Copy, Clone)] +/// Packetizes G.711/G.722 audio, which needs no fragmentation — one payload per frame. pub struct G7xxPayloader; impl Payloader for G7xxPayloader { diff --git a/rtc-rtp/src/codec/h264/mod.rs b/rtc-rtp/src/codec/h264/mod.rs index a18249ac..8f551eb7 100644 --- a/rtc-rtp/src/codec/h264/mod.rs +++ b/rtc-rtp/src/codec/h264/mod.rs @@ -1,3 +1,11 @@ +//! H.264 RTP payload format ([RFC 6184]). +//! +//! A NAL unit that fits the MTU is sent as-is. Larger ones are split into FU-A fragments, and +//! several small ones (typically SPS and PPS) may be combined into one STAP-A aggregate. The +//! `*_NALU_TYPE` constants name the types this payloader recognises, and the `*_BITMASK` +//! constants describe how the NAL header and FU header pack their fields. +//! +//! [RFC 6184]: https://datatracker.ietf.org/doc/html/rfc6184 #[cfg(test)] mod h264_test; @@ -13,25 +21,41 @@ pub struct H264Payloader { pps_nalu: Option, } +/// NAL type 24: STAP-A, which aggregates several small NAL units into one payload. pub const STAPA_NALU_TYPE: u8 = 24; +/// NAL type 28: FU-A, which fragments one NAL unit across several payloads. pub const FUA_NALU_TYPE: u8 = 28; +/// NAL type 29: FU-B, FU-A with a decoding-order number. Not used by WebRTC. pub const FUB_NALU_TYPE: u8 = 29; +/// NAL type 7: sequence parameter set. pub const SPS_NALU_TYPE: u8 = 7; +/// NAL type 8: picture parameter set. pub const PPS_NALU_TYPE: u8 = 8; +/// NAL type 9: access unit delimiter. pub const AUD_NALU_TYPE: u8 = 9; +/// NAL type 12: filler data, which is dropped rather than sent. pub const FILLER_NALU_TYPE: u8 = 12; +/// Bytes of FU-A header prefixed to each fragment. pub const FUA_HEADER_SIZE: usize = 2; +/// Bytes of STAP-A header prefixed to an aggregate. pub const STAPA_HEADER_SIZE: usize = 1; +/// Bytes of length prefix before each NAL unit inside a STAP-A. pub const STAPA_NALU_LENGTH_SIZE: usize = 2; +/// Mask selecting the NAL type from a NAL header byte. pub const NALU_TYPE_BITMASK: u8 = 0x1F; +/// Mask selecting `nal_ref_idc` from a NAL header byte. pub const NALU_REF_IDC_BITMASK: u8 = 0x60; +/// FU header start bit: this fragment begins a NAL unit. pub const FU_START_BITMASK: u8 = 0x80; +/// FU header end bit: this fragment completes a NAL unit. pub const FU_END_BITMASK: u8 = 0x40; +/// The STAP-A header byte this payloader emits. pub const OUTPUT_STAP_AHEADER: u8 = 0x78; +/// The Annex B start code (`00 00 00 01`) that delimits NAL units in a byte stream. pub static ANNEXB_NALUSTART_CODE: Bytes = Bytes::from_static(&[0x00, 0x00, 0x00, 0x01]); impl H264Payloader { @@ -199,6 +223,7 @@ impl Payloader for H264Payloader { /// H264Packet represents the H264 header that is stored in the payload of an RTP Packet #[derive(PartialEq, Eq, Debug, Default, Clone)] pub struct H264Packet { + /// Whether to emit AVCC length-prefixed output instead of Annex B start codes. pub is_avc: bool, fua_buffer: Option, } diff --git a/rtc-rtp/src/codec/h265/mod.rs b/rtc-rtp/src/codec/h265/mod.rs index 1fd4317a..08b74aa0 100644 --- a/rtc-rtp/src/codec/h265/mod.rs +++ b/rtc-rtp/src/codec/h265/mod.rs @@ -1,3 +1,15 @@ +//! H.265/HEVC RTP payload format ([RFC 7798]). +//! +//! HEVC differs from H.264 in ways that matter to packetization: NAL headers are two bytes +//! rather than one, and there is an extra aggregation form (PACI) that can carry payload +//! content information ahead of the NAL unit. +//! +//! This module provides the three packet shapes the RFC defines — single NAL unit, +//! aggregation, and fragmentation unit — plus [`HevcPayloader`](crate::codec::h265::HevcPayloader), which picks between them +//! based on the MTU, and the depacketizer that reverses the choice. +//! +//! [RFC 7798]: https://datatracker.ietf.org/doc/html/rfc7798 + use bytes::{BufMut, Bytes, BytesMut}; use super::h264::ANNEXB_NALUSTART_CODE; @@ -7,36 +19,69 @@ use shared::error::{Error, Result}; #[cfg(test)] mod h265_test; +/// The three-byte Annex B start code (`00 00 01`). pub static ANNEXB_3_NALUSTART_CODE: Bytes = Bytes::from_static(&[0x00, 0x00, 0x01]); +/// Payload header for a single NAL unit packet. pub static SING_PAYLOAD_HDR: Bytes = Bytes::from_static(&[0x1C, 0x01]); +/// Payload header for an aggregation packet. pub static AGGR_PAYLOAD_HDR: Bytes = Bytes::from_static(&[0x60, 0x01]); +/// Payload header for a fragmentation unit. pub static FRAG_PAYLOAD_HDR: Bytes = Bytes::from_static(&[0x62, 0x01]); +/// FU header for the first fragment of an IDR frame. pub static FU_HDR_IDR_S: u8 = 0x93; +/// FU header for a middle fragment of an IDR frame. pub static FU_HDR_IDR_M: u8 = 0x13; +/// FU header for the last fragment of an IDR frame. pub static FU_HDR_IDR_E: u8 = 0x53; +/// FU header for the first fragment of a P frame. pub static FU_HDR_P_S: u8 = 0x81; +/// FU header for a middle fragment of a P frame. pub static FU_HDR_P_M: u8 = 0x01; +/// FU header for the last fragment of a P frame. pub static FU_HDR_P_E: u8 = 0x41; +/// FU header for the first fragment of a B frame. pub static FU_HDR_B_S: u8 = 0x80; +/// FU header for a middle fragment of a B frame. pub static FU_HDR_B_M: u8 = 0x00; +/// FU header for the last fragment of a B frame. pub static FU_HDR_B_E: u8 = 0x40; +/// The payload MTU this payloader targets, chosen to survive typical paths without IP +/// fragmentation. pub const RTP_OUTBOUND_MTU: usize = 1200; +/// Bytes of FU header following the payload header in a fragmentation unit. pub const H265FRAGMENTATION_UNIT_HEADER_SIZE: usize = 1; +/// Bytes in an H.265 NAL header — two, unlike H.264's one. pub const NAL_HEADER_SIZE: usize = 2; #[derive(PartialEq, Hash, Debug, Copy, Clone)] +/// The H.265 NAL unit types this payloader distinguishes. +#[non_exhaustive] pub enum UnitType { + /// Video parameter set. VPS = 32, + /// Sequence parameter set. SPS = 33, + /// Picture parameter set. PPS = 34, + /// Clean random access picture — a keyframe that allows mid-stream tune-in. CRA = 21, + /// Supplemental enhancement information. SEI = 39, + /// Instantaneous decoder refresh picture — a keyframe. IDR = 19, + /// A predicted (P) frame. PFR = 1, + /// A bidirectionally predicted (B) frame. BFR = 0, + /// A unit type this payloader skips. IGNORE = -1, } impl UnitType { + /// Maps a raw NAL type id to a [`UnitType`]. + /// + /// # Errors + /// + /// Fails if the id is not one this payloader handles. pub fn for_id(id: u8) -> Result { if id > 64 { Err(Error::ErrUnhandledNaluType) @@ -58,6 +103,7 @@ impl UnitType { } #[derive(Default, Debug, Clone)] +/// Packetizes H.265/HEVC NAL units into RTP payloads, fragmenting when needed. pub struct HevcPayloader; impl HevcPayloader { @@ -104,6 +150,9 @@ impl HevcPayloader { header } + /// Locates the NAL unit boundaries in an Annex B buffer. + /// + /// Returns the offset of each start code and the length of the code that was matched. pub fn parse(nalu: &Bytes) -> (Vec, usize) { let finder = memchr::memmem::Finder::new(&ANNEXB_NALUSTART_CODE); let nals = finder.find_iter(nalu).collect::>(); @@ -280,6 +329,7 @@ const H265NALU_PACI_PACKET_TYPE: u8 = 50; pub struct H265NALUHeader(pub u16); impl H265NALUHeader { + /// Parses an H.265 NAL header from its two bytes. pub fn new(high_byte: u8, low_byte: u8) -> Self { H265NALUHeader(((high_byte as u16) << 8) | low_byte as u16) } @@ -974,10 +1024,15 @@ impl H265TSCI { /// H265 Payload Enum /// #[derive(Debug, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum H265Payload { + /// One NAL unit carried whole in a single packet. H265SingleNALUnitPacket(H265SingleNALUnitPacket), + /// One fragment of a NAL unit too large for the MTU. H265FragmentationUnitPacket(H265FragmentationUnitPacket), + /// Several small NAL units aggregated into one packet. H265AggregationPacket(H265AggregationPacket), + /// A PACI packet, which carries payload content information ahead of the NAL unit. H265PACIPacket(H265PACIPacket), } diff --git a/rtc-rtp/src/codec/mod.rs b/rtc-rtp/src/codec/mod.rs index 0296e20c..8488a070 100644 --- a/rtc-rtp/src/codec/mod.rs +++ b/rtc-rtp/src/codec/mod.rs @@ -1,7 +1,14 @@ +/// AV1 payload format ([RFC 9628]). pub mod av1; +/// G.711 and G.722 payload formats, which need no fragmentation. pub mod g7xx; +/// H.264 payload format ([RFC 6184]): STAP-A aggregation and FU-A fragmentation. pub mod h264; +/// H.265/HEVC payload format ([RFC 7798]). pub mod h265; +/// Opus payload format ([RFC 7587]), one packet per frame. pub mod opus; +/// VP8 payload format ([RFC 7741]). pub mod vp8; +/// VP9 payload format (draft-ietf-payload-vp9). pub mod vp9; diff --git a/rtc-rtp/src/codec/opus/mod.rs b/rtc-rtp/src/codec/opus/mod.rs index 7d7768ee..d12e78a0 100644 --- a/rtc-rtp/src/codec/opus/mod.rs +++ b/rtc-rtp/src/codec/opus/mod.rs @@ -7,6 +7,7 @@ use shared::error::{Error, Result}; use bytes::Bytes; #[derive(Default, Debug, Copy, Clone)] +/// Packetizes Opus audio: one RTP payload per Opus frame, never fragmented. pub struct OpusPayloader; impl Payloader for OpusPayloader { diff --git a/rtc-rtp/src/codec/vp8/mod.rs b/rtc-rtp/src/codec/vp8/mod.rs index 8a59604e..1cfc2b4b 100644 --- a/rtc-rtp/src/codec/vp8/mod.rs +++ b/rtc-rtp/src/codec/vp8/mod.rs @@ -6,11 +6,15 @@ use shared::error::{Error, Result}; use bytes::{Buf, BufMut, Bytes, BytesMut}; +/// Bytes of mandatory VP8 payload descriptor prefixed to each payload. pub const VP8_HEADER_SIZE: usize = 1; /// Vp8Payloader payloads VP8 packets #[derive(Default, Debug, Copy, Clone)] pub struct Vp8Payloader { + /// Whether to include the optional picture id in the payload descriptor. + /// + /// Lets a receiver detect loss within a frame, at one or two bytes per packet. pub enable_picture_id: bool, picture_id: u16, } diff --git a/rtc-rtp/src/codec/vp9/mod.rs b/rtc-rtp/src/codec/vp9/mod.rs index 0278c0d7..1cac160c 100644 --- a/rtc-rtp/src/codec/vp9/mod.rs +++ b/rtc-rtp/src/codec/vp9/mod.rs @@ -22,6 +22,7 @@ pub struct Vp9Payloader { picture_id: u16, initialized: bool, + /// Supplies the starting picture id; randomized when absent. pub initial_picture_id_fn: Option, } @@ -188,7 +189,9 @@ pub struct Vp9Packet { pub g: bool, /// N_G indicates the number of pictures in a Picture Group (PG) pub ng: u8, + /// Frame width for each spatial layer. pub width: Vec, + /// Frame height for each spatial layer. pub height: Vec, /// Temporal layer ID of pictures in a Picture Group pub pgtid: Vec, diff --git a/rtc-rtp/src/extension/abs_send_time_extension/mod.rs b/rtc-rtp/src/extension/abs_send_time_extension/mod.rs index 543af52a..dad9c86a 100644 --- a/rtc-rtp/src/extension/abs_send_time_extension/mod.rs +++ b/rtc-rtp/src/extension/abs_send_time_extension/mod.rs @@ -8,12 +8,14 @@ use shared::{ use bytes::{Buf, BufMut}; +/// The extension's encoded size: 3 bytes of 6.18 fixed-point seconds. pub const ABS_SEND_TIME_EXTENSION_SIZE: usize = 3; /// AbsSendTimeExtension is a extension payload format in /// #[derive(PartialEq, Eq, Debug, Default, Copy, Clone)] pub struct AbsSendTimeExtension { + /// The send time in 6.18 fixed-point format — 6 bits of seconds, 18 of fraction. pub timestamp: u64, } diff --git a/rtc-rtp/src/extension/audio_level_extension/mod.rs b/rtc-rtp/src/extension/audio_level_extension/mod.rs index 7ce0f957..142cc7d4 100644 --- a/rtc-rtp/src/extension/audio_level_extension/mod.rs +++ b/rtc-rtp/src/extension/audio_level_extension/mod.rs @@ -10,6 +10,7 @@ use shared::{ use bytes::{Buf, BufMut}; // AUDIO_LEVEL_EXTENSION_SIZE One byte header size +/// The extension's encoded size in bytes. pub const AUDIO_LEVEL_EXTENSION_SIZE: usize = 1; /// AudioLevelExtension is a extension payload format described in @@ -39,7 +40,9 @@ pub const AUDIO_LEVEL_EXTENSION_SIZE: usize = 1; /// [RFC 6464]: https://tools.ietf.org/html/rfc6464 #[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Serialize, Deserialize)] pub struct AudioLevelExtension { + /// Loudness in −dBov, from 0 (loudest) to 127 (silence). pub level: u8, + /// Whether the sender detected voice activity in this packet. pub voice: bool, } diff --git a/rtc-rtp/src/extension/mod.rs b/rtc-rtp/src/extension/mod.rs index 276dc0cf..a56bac08 100644 --- a/rtc-rtp/src/extension/mod.rs +++ b/rtc-rtp/src/extension/mod.rs @@ -6,28 +6,42 @@ use shared::{ marshal::{Marshal, MarshalSize}, }; +/// Absolute send time, for one-way-delay based bandwidth estimation. pub mod abs_send_time_extension; +/// Per-packet audio loudness and voice activity ([RFC 6464]). pub mod audio_level_extension; +/// A requested playout-delay range, for latency/smoothness trade-offs. pub mod playout_delay_extension; +/// The transport-wide sequence number that TWCC feedback refers to. pub mod transport_cc_extension; +/// Camera direction and rotation (CVO), so a receiver can display video upright. pub mod video_orientation_extension; /// A generic RTP header extension. +#[non_exhaustive] pub enum HeaderExtension { + /// The absolute-send-time extension. AbsSendTime(abs_send_time_extension::AbsSendTimeExtension), + /// The audio-level extension. AudioLevel(audio_level_extension::AudioLevelExtension), + /// The playout-delay extension. PlayoutDelay(playout_delay_extension::PlayoutDelayExtension), + /// The transport-wide CC extension. TransportCc(transport_cc_extension::TransportCcExtension), + /// The video-orientation extension. VideoOrientation(video_orientation_extension::VideoOrientationExtension), /// A custom extension Custom { + /// The extension's canonical URI, which is what SDP negotiates ids against. uri: Cow<'static, str>, + /// The extension value, erased so extensions of different types can be held together. extension: Box, }, } impl HeaderExtension { + /// The extension's URI. pub fn uri(&self) -> Cow<'static, str> { use HeaderExtension::*; @@ -43,6 +57,7 @@ impl HeaderExtension { } } + /// Whether both refer to the same extension, comparing URIs rather than values. pub fn is_same(&self, other: &Self) -> bool { use HeaderExtension::*; match (self, other) { diff --git a/rtc-rtp/src/extension/playout_delay_extension/mod.rs b/rtc-rtp/src/extension/playout_delay_extension/mod.rs index 8562b86e..d2097d38 100644 --- a/rtc-rtp/src/extension/playout_delay_extension/mod.rs +++ b/rtc-rtp/src/extension/playout_delay_extension/mod.rs @@ -5,7 +5,9 @@ use bytes::BufMut; use shared::error::{Error, Result}; use shared::marshal::{Marshal, MarshalSize, Unmarshal}; +/// The extension's encoded size: two 12-bit values packed into 3 bytes. pub const PLAYOUT_DELAY_EXTENSION_SIZE: usize = 3; +/// The largest representable delay, in 10 ms units — about 40 seconds. pub const PLAYOUT_DELAY_MAX_VALUE: u16 = (1 << 12) - 1; /// PlayoutDelayExtension is an extension payload format described in @@ -17,7 +19,9 @@ pub const PLAYOUT_DELAY_MAX_VALUE: u16 = (1 << 12) - 1; /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #[derive(PartialEq, Eq, Debug, Default, Copy, Clone)] pub struct PlayoutDelayExtension { + /// The minimum playout delay in 10 ms units. pub min_delay: u16, + /// The maximum playout delay in 10 ms units. pub max_delay: u16, } @@ -72,6 +76,8 @@ impl Marshal for PlayoutDelayExtension { } impl PlayoutDelayExtension { + /// A playout-delay extension requesting a delay between `min_delay` and `max_delay`, both in + /// 10 ms units. pub fn new(min_delay: u16, max_delay: u16) -> Self { PlayoutDelayExtension { min_delay, diff --git a/rtc-rtp/src/extension/transport_cc_extension/mod.rs b/rtc-rtp/src/extension/transport_cc_extension/mod.rs index 581e18c0..00cb7031 100644 --- a/rtc-rtp/src/extension/transport_cc_extension/mod.rs +++ b/rtc-rtp/src/extension/transport_cc_extension/mod.rs @@ -10,6 +10,7 @@ use shared::{ use bytes::{Buf, BufMut}; // transport-wide sequence +/// The extension's encoded size in bytes. pub const TRANSPORT_CC_EXTENSION_SIZE: usize = 2; /// TransportCCExtension is a extension payload format in @@ -23,6 +24,9 @@ pub const TRANSPORT_CC_EXTENSION_SIZE: usize = 2; /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Serialize, Deserialize)] pub struct TransportCcExtension { + /// A sequence number counting every packet sent on the transport, across all streams. + /// + /// TWCC feedback reports arrival times against these, which is why it spans SSRCs. pub transport_sequence: u16, } diff --git a/rtc-rtp/src/extension/video_orientation_extension/mod.rs b/rtc-rtp/src/extension/video_orientation_extension/mod.rs index e8868ba8..c1965e0e 100644 --- a/rtc-rtp/src/extension/video_orientation_extension/mod.rs +++ b/rtc-rtp/src/extension/video_orientation_extension/mod.rs @@ -11,6 +11,7 @@ use shared::{ }; // One byte header size +/// The extension's encoded size in bytes. pub const VIDEO_ORIENTATION_EXTENSION_SIZE: usize = 1; /// Coordination of Video Orientation in RTP streams. @@ -38,24 +39,35 @@ pub const VIDEO_ORIENTATION_EXTENSION_SIZE: usize = 1; /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ #[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Serialize, Deserialize)] pub struct VideoOrientationExtension { + /// Which camera produced the frame. pub direction: CameraDirection, + /// Whether the image is horizontally mirrored, as front cameras usually are. pub flip: bool, + /// How far the receiver must rotate the image to display it upright. pub rotation: VideoRotation, } #[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] +/// Which camera a frame came from. pub enum CameraDirection { #[default] + /// The front-facing (user-facing) camera. Front = 0, + /// The rear-facing camera. Back = 1, } #[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] +/// Clockwise rotation to apply when displaying the frame. pub enum VideoRotation { #[default] + /// No rotation. Degree0 = 0, + /// Rotate 90° clockwise. Degree90 = 1, + /// Rotate 180°. Degree180 = 2, + /// Rotate 270° clockwise. Degree270 = 3, } diff --git a/rtc-rtp/src/header.rs b/rtc-rtp/src/header.rs index 936fcd11..fbd873c4 100644 --- a/rtc-rtp/src/header.rs +++ b/rtc-rtp/src/header.rs @@ -1,3 +1,19 @@ +//! The RTP header and its extensions. +//! +//! [`Header`](crate::header::Header) is the fixed 12-byte header plus the optional CSRC list and header-extension +//! block. The `*_SHIFT`/`*_MASK` constants describe how the flag fields pack into the first two +//! octets, and the `*_OFFSET`/`*_LENGTH` constants give the byte positions a caller can patch in +//! place without re-encoding. +//! +//! Header extensions ([RFC 8285]) come in two forms, selected by +//! [`Header::extension_profile`](crate::header::Header::extension_profile): one-byte ids ([`EXTENSION_PROFILE_ONE_BYTE`](crate::header::EXTENSION_PROFILE_ONE_BYTE)) or two-byte ids +//! ([`EXTENSION_PROFILE_TWO_BYTE`](crate::header::EXTENSION_PROFILE_TWO_BYTE)) when an id above 14 is needed. Use +//! [`Header::set_extension`](crate::header::Header::set_extension) and [`Header::get_extension`](crate::header::Header::get_extension) rather than touching +//! [`Header::extensions`](crate::header::Header::extensions) directly — they keep [`Header::extensions_padding`](crate::header::Header::extensions_padding) and the extension +//! flag consistent, which marshalling depends on. +//! +//! [RFC 8285]: https://datatracker.ietf.org/doc/html/rfc8285 + use shared::{ error::{Error, Result}, marshal::{Marshal, MarshalSize, Unmarshal}, @@ -5,32 +21,58 @@ use shared::{ use bytes::{Buf, BufMut, Bytes}; +/// The length of the extension-profile and length fields that precede header extensions. pub const HEADER_LENGTH: usize = 4; +/// Bit offset of the version field in the first header octet. pub const VERSION_SHIFT: u8 = 6; +/// Bit mask of the version field once shifted. pub const VERSION_MASK: u8 = 0x3; +/// Bit offset of the padding flag. pub const PADDING_SHIFT: u8 = 5; +/// Bit mask of the padding flag once shifted. pub const PADDING_MASK: u8 = 0x1; +/// Bit offset of the extension flag. pub const EXTENSION_SHIFT: u8 = 4; +/// Bit mask of the extension flag once shifted. pub const EXTENSION_MASK: u8 = 0x1; +/// Extension profile `0xBEDE`, which selects one-byte header extension ids ([RFC 8285]). pub const EXTENSION_PROFILE_ONE_BYTE: u16 = 0xBEDE; +/// Extension profile `0x1000`, which selects two-byte header extension ids, allowing ids +/// above 14. pub const EXTENSION_PROFILE_TWO_BYTE: u16 = 0x1000; +/// Extension id 15, reserved by the RFC and never assigned. pub const EXTENSION_ID_RESERVED: u8 = 0xF; +/// Bit mask of the CSRC count field. pub const CC_MASK: u8 = 0xF; +/// Bit offset of the marker bit. pub const MARKER_SHIFT: u8 = 7; +/// Bit mask of the marker bit once shifted. pub const MARKER_MASK: u8 = 0x1; +/// Bit mask of the payload-type field. pub const PT_MASK: u8 = 0x7F; +/// Byte offset of the sequence number within the header. pub const SEQ_NUM_OFFSET: usize = 2; +/// Length of the sequence number in bytes. pub const SEQ_NUM_LENGTH: usize = 2; +/// Byte offset of the timestamp within the header. pub const TIMESTAMP_OFFSET: usize = 4; +/// Length of the timestamp in bytes. pub const TIMESTAMP_LENGTH: usize = 4; +/// Byte offset of the SSRC within the header. pub const SSRC_OFFSET: usize = 8; +/// Length of the SSRC in bytes. pub const SSRC_LENGTH: usize = 4; +/// Byte offset of the first CSRC within the header. pub const CSRC_OFFSET: usize = 12; +/// Length of each CSRC in bytes. pub const CSRC_LENGTH: usize = 4; #[derive(Debug, Eq, PartialEq, Default, Clone)] +/// One RTP header extension: an id and its payload bytes. pub struct Extension { + /// The extension id, as negotiated by `a=extmap`. pub id: u8, + /// The extension's value. pub payload: Bytes, } @@ -38,17 +80,30 @@ pub struct Extension { /// NOTE: PayloadOffset is populated by Marshal/Unmarshal and should not be modified #[derive(Debug, Eq, PartialEq, Default, Clone)] pub struct Header { + /// The RTP version, always 2. pub version: u8, + /// Whether the payload is followed by padding octets, the last giving the padding length. pub padding: bool, + /// Whether a header-extension block follows the fixed header. pub extension: bool, + /// The marker bit: the last packet of a video frame, or the start of a talk spurt for audio. pub marker: bool, + /// The payload type, which identifies the codec as negotiated in SDP. pub payload_type: u8, + /// Increments by one per packet sent; used to detect loss and restore order. pub sequence_number: u16, + /// The sampling instant of the first octet, in the codec's clock rate. pub timestamp: u32, + /// The synchronization source — the identifier of the stream this packet belongs to. pub ssrc: u32, + /// The contributing sources, listed when a mixer combined several streams. pub csrc: Vec, + /// Which header-extension form is in use: [`EXTENSION_PROFILE_ONE_BYTE`] or + /// [`EXTENSION_PROFILE_TWO_BYTE`]. pub extension_profile: u16, + /// The header extensions present on this packet. pub extensions: Vec, + /// Padding bytes appended to the extension block so it ends on a 32-bit boundary. pub extensions_padding: usize, } @@ -357,6 +412,7 @@ impl Marshal for Header { } impl Header { + /// The total encoded length of the extension payloads, padding excluded. pub fn get_extension_payload_len(&self) -> usize { let payload_len: usize = self .extensions diff --git a/rtc-rtp/src/lib.rs b/rtc-rtp/src/lib.rs index ba7214db..c7938114 100644 --- a/rtc-rtp/src/lib.rs +++ b/rtc-rtp/src/lib.rs @@ -1,11 +1,74 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! RTP packets, header extensions and packetization. +//! +//! The Real-time Transport Protocol ([RFC 3550]) wire format, plus the pieces WebRTC needs +//! around it: one-byte and two-byte header extensions ([RFC 8285]) and per-codec +//! packetizers that turn encoded frames into RTP payloads. +//! +//! # Structure +//! +//! * [`Packet`] / [`Header`] — the packet and its header: `unmarshal` one off the wire, +//! `marshal` one back, get and set header extensions by id. +//! * [`packetizer`] — [`Packetizer`](packetizer::Packetizer), which fragments a frame into +//! MTU-sized payloads, and the per-codec [`Payloader`](packetizer::Payloader) +//! implementations in [`codec`] (VP8, VP9, H.264, H.265, AV1, Opus, G.711). +//! * [`sequence`] — [`Sequencer`](sequence::Sequencer), for sequence numbers that start at a +//! random offset as the RFC requires. +//! * [`extension`] — the typed header extensions: audio level ([RFC 6464]), video +//! orientation, transport-wide CC, and the SDES stream ids used for simulcast. +//! +//! # Example +//! +//! ``` +//! use bytes::Bytes; +//! use rtc_rtp::Packet; +//! use shared::marshal::{Marshal, Unmarshal}; +//! +//! # fn example() -> Result<(), Box> { +//! // A minimal RTP packet: version 2, payload type 96, one byte of payload. +//! let raw = Bytes::from_static(&[ +//! 0x80, 0x60, 0x00, 0x01, // V=2, PT=96, seq=1 +//! 0x00, 0x00, 0x00, 0x20, // timestamp +//! 0xDE, 0xAD, 0xBE, 0xEF, // ssrc +//! 0xAA, // payload +//! ]); +//! +//! let mut buf = raw.clone(); +//! let packet = Packet::unmarshal(&mut buf)?; +//! assert_eq!(packet.header.payload_type, 96); +//! assert_eq!(packet.header.sequence_number, 1); +//! assert_eq!(packet.header.ssrc, 0xDEAD_BEEF); +//! +//! // Re-encoding reproduces the original bytes. +//! assert_eq!(packet.marshal()?, raw); +//! # Ok(()) +//! # } +//! ``` +//! +//! Most applications do not depend on this crate directly — the +//! [`rtc`](https://docs.rs/rtc) crate re-exports it as `rtc::rtp`, and an application +//! usually meets these types when reading or writing media on a track. +//! +//! [RFC 3550]: https://datatracker.ietf.org/doc/html/rfc3550 +//! [RFC 8285]: https://datatracker.ietf.org/doc/html/rfc8285 +//! [RFC 6464]: https://datatracker.ietf.org/doc/html/rfc6464 + +/// Per-codec payloaders and depacketizers (VP8, VP9, AV1, H.264, H.265, Opus, G.711). pub mod codec; +/// The typed RTP header extensions ([RFC 8285]). +/// +/// [RFC 8285]: https://datatracker.ietf.org/doc/html/rfc8285 pub mod extension; +/// The RTP header, its extensions, and the bit masks that encode it. pub mod header; +/// A whole RTP packet: header plus payload. pub mod packet; +/// Turning encoded frames into RTP packets, and back again. pub mod packetizer; +/// Sequence-number generation, starting from a random offset as the RFC requires. pub mod sequence; pub use header::Header; diff --git a/rtc-rtp/src/packet/mod.rs b/rtc-rtp/src/packet/mod.rs index 5913e732..14f32aa4 100644 --- a/rtc-rtp/src/packet/mod.rs +++ b/rtc-rtp/src/packet/mod.rs @@ -14,7 +14,9 @@ use std::fmt; /// NOTE: Raw is populated by Marshal/Unmarshal and should not be modified #[derive(Debug, Eq, PartialEq, Default, Clone)] pub struct Packet { + /// The packet header. pub header: Header, + /// The media payload, in whatever format the payload type implies. pub payload: Bytes, } diff --git a/rtc-rtp/src/packetizer/mod.rs b/rtc-rtp/src/packetizer/mod.rs index 737baffb..8676ce94 100644 --- a/rtc-rtp/src/packetizer/mod.rs +++ b/rtc-rtp/src/packetizer/mod.rs @@ -15,7 +15,13 @@ use std::time::Instant; /// Payloader payloads a byte array for use as rtp.Packet payloads pub trait Payloader: Send + Sync + fmt::Debug { + /// Splits one encoded frame into payloads no larger than `mtu`. + /// + /// # Errors + /// + /// Fails if the frame is malformed for this codec, or `mtu` is too small to make progress. fn payload(&mut self, mtu: usize, b: &Bytes) -> Result>; + /// Clones this payloader behind a trait object. fn clone_to(&self) -> Box; } @@ -27,9 +33,20 @@ impl Clone for Box { /// Packetizer packetizes a payload pub trait Packetizer: Send + Sync + fmt::Debug { + /// Attaches the absolute-send-time header extension under id `value`. fn enable_abs_send_time(&mut self, value: u8); + /// Packetizes one frame, advancing the timestamp by `samples`. + /// + /// Assigns sequence numbers, sets the marker bit on the final packet, and applies any + /// enabled header extensions. + /// + /// # Errors + /// + /// Propagates payloader failures. fn packetize(&mut self, payload: &Bytes, samples: u32) -> Result>; + /// Advances the timestamp without sending anything, for dropped or silent frames. fn skip_samples(&mut self, skipped_samples: u32); + /// Clones this packetizer behind a trait object. fn clone_to(&self) -> Box; } @@ -41,6 +58,11 @@ impl Clone for Box { /// Depacketizer depacketizes a RTP payload, removing any RTP specific data from the payload pub trait Depacketizer { + /// Reassembles a frame from one RTP payload, buffering fragments as needed. + /// + /// # Errors + /// + /// Fails if the payload is malformed for this codec. fn depacketize(&mut self, b: &Bytes) -> Result; /// Checks if the packet is at the beginning of a partition. This @@ -83,6 +105,10 @@ impl fmt::Debug for PacketizerImpl { } } +/// Builds a packetizer for one outbound stream. +/// +/// Ties together the codec's payloader, a sequencer, and the SSRC, payload type, MTU and clock +/// rate the stream was negotiated with. pub fn new_packetizer( mtu: usize, payload_type: u8, diff --git a/rtc-rtp/src/sequence.rs b/rtc-rtp/src/sequence.rs index 995d0d8b..a17a8192 100644 --- a/rtc-rtp/src/sequence.rs +++ b/rtc-rtp/src/sequence.rs @@ -4,8 +4,13 @@ use std::sync::atomic::{AtomicU16, AtomicU64, Ordering}; /// Sequencer generates sequential sequence numbers for building RTP packets pub trait Sequencer: Send + Sync + fmt::Debug { + /// Returns the next sequence number, wrapping at `u16::MAX` and counting the rollover. fn next_sequence_number(&self) -> u16; + /// How many times the sequence number has wrapped. + /// + /// SRTP needs this: the rollover counter is part of the packet index it encrypts with. fn roll_over_count(&self) -> u64; + /// Clones this sequencer behind a trait object. fn clone_to(&self) -> Box; } diff --git a/rtc-sctp/Cargo.toml b/rtc-sctp/Cargo.toml index 819227ad..d022be2d 100644 --- a/rtc-sctp/Cargo.toml +++ b/rtc-sctp/Cargo.toml @@ -23,14 +23,14 @@ shared = { workspace = true, default-features = false, features = [] } bytes.workspace = true rand.workspace = true thiserror.workspace = true -slab = "0.4.9" +slab.workspace = true log.workspace = true -crc32c = "0.6" -rustc-hash = "2" +crc32c.workspace = true +rustc-hash.workspace = true [dev-dependencies] -assert_matches = "1.5.0" -lazy_static = "1.4.0" +assert_matches.workspace = true +lazy_static.workspace = true [[example]] name = "sctp_micro" diff --git a/rtc-sctp/examples/sctp_e2e.rs b/rtc-sctp/examples/sctp_e2e.rs index 9f55a6ad..8e5c74c5 100644 --- a/rtc-sctp/examples/sctp_e2e.rs +++ b/rtc-sctp/examples/sctp_e2e.rs @@ -69,6 +69,7 @@ impl Node { assoc.handle_event(event); } } + _ => {} } } } diff --git a/rtc-sctp/src/association/mod.rs b/rtc-sctp/src/association/mod.rs index 6c3dc507..46052f85 100644 --- a/rtc-sctp/src/association/mod.rs +++ b/rtc-sctp/src/association/mod.rs @@ -26,7 +26,7 @@ use crate::param::{ }; use crate::queue::{payload_queue::PayloadQueue, pending_queue::PendingQueue}; use crate::shared::{AssociationEventInner, AssociationId, EndpointEvent, EndpointEventInner}; -use crate::util::{sna16lt, sna32gt, sna32gte, sna32lt, sna32lte}; +use crate::util::{constant_time_eq, sna16lt, sna32gt, sna32gte, sna32lt, sna32lte}; use crate::{AssociationEvent, Payload, Side}; use shared::error::{Error, Result}; use shared::{TransportContext, TransportMessage, TransportProtocol}; @@ -55,6 +55,7 @@ mod association_test; /// Reasons why an association might be lost #[derive(Debug, Error, Clone, PartialEq)] +#[non_exhaustive] pub enum AssociationError { /// Handshake failed #[error("handshake failed due to {0}")] @@ -100,6 +101,7 @@ pub enum Event { AssociationLost { /// Reason that the association was closed reason: AssociationError, + /// The stream the loss was reported against. id: StreamId, }, /// Stream events @@ -349,6 +351,8 @@ impl Association { // The initial cwnd before DATA transmission or after a sufficiently // long idle period MUST be set to min(4*MTU, max (2*MTU, 4380bytes)). let cwnd = (2 * mtu).clamp(4380, 4 * mtu); + // RFC 4960 requires an unpredictable initial TSN. SCTP remains usable without an RTC + // crypto provider, so this deliberately uses `rand`'s thread-local CSPRNG. let mut tsn = random::(); if tsn == 0 { tsn += 1; @@ -721,6 +725,7 @@ impl Association { } } + /// The identifiers of every stream currently open on this association. pub fn stream_ids(&self) -> Vec { self.streams.keys().cloned().collect() } @@ -1138,14 +1143,14 @@ impl Association { if let Some(my_cookie) = &self.my_cookie { match state { AssociationState::Established => { - if my_cookie.cookie != c.cookie { + if !constant_time_eq(&my_cookie.cookie, &c.cookie) { return Ok(vec![]); } } AssociationState::Closed | AssociationState::CookieWait | AssociationState::CookieEchoed => { - if my_cookie.cookie != c.cookie { + if !constant_time_eq(&my_cookie.cookie, &c.cookie) { return Ok(vec![]); } @@ -1244,7 +1249,10 @@ impl Association { let immediate_sack = d.immediate_sack; - if stream_handle_data && let Some(s) = self.streams.get_mut(&d.stream_identifier) { + if stream_handle_data + && let Some(s) = self.streams.get_mut(&d.stream_identifier) + && !s.reset_pending + { self.events.push_back(Event::DatagramReceived); if s.handle_data(d) && s.reassembly_queue.is_readable() { self.events.push_back(Event::Stream(StreamEvent::Readable { @@ -1947,11 +1955,31 @@ impl Association { self.side, p.sender_last_tsn, self.peer_last_tsn ); for id in &p.stream_identifiers { - if self.streams.contains_key(id) { + if let Some(s) = self.streams.get_mut(id) { if respond { sis_to_reset.push(*id); } - self.unregister_stream(*id, AssociationError::Reset); + if s.reassembly_queue.is_readable() { + // The reassembly queue still holds messages that were + // received before the reset but not yet delivered to + // the consumer (RFC 6525: data received prior to the + // reset must still be delivered). The reset is + // performed at the SCTP level (we respond "Success - + // Performed" as usual), but the local teardown is + // deferred until the consumer drains the queue via + // read_sctp. No new inbound DATA is accepted for the + // stream in the meantime. + debug!( + "[{}] resetStream(): deferring teardown of stream {} until reassembly queue is drained", + self.side, id + ); + s.reset_pending = true; + s.state = RecvSendState::Readable; + self.events + .push_back(Event::Stream(StreamEvent::Readable { id: *id })); + } else { + self.unregister_stream(*id, AssociationError::Reset); + } } } self.reconfig_requests diff --git a/rtc-sctp/src/association/stats.rs b/rtc-sctp/src/association/stats.rs index 41ea7368..f9819dac 100644 --- a/rtc-sctp/src/association/stats.rs +++ b/rtc-sctp/src/association/stats.rs @@ -9,46 +9,57 @@ pub struct AssociationStats { } impl AssociationStats { + /// Counts one DATA chunk sent. pub fn inc_datas(&mut self) { self.n_datas += 1; } + /// The number of DATA chunks sent. pub fn get_num_datas(&mut self) -> u64 { self.n_datas } + /// Counts one SACK chunk received. pub fn inc_sacks(&mut self) { self.n_sacks += 1; } + /// The number of SACK chunks received. pub fn get_num_sacks(&mut self) -> u64 { self.n_sacks } + /// Counts one T3-rtx retransmission timeout. pub fn inc_t3timeouts(&mut self) { self.n_t3timeouts += 1; } + /// The number of T3-rtx retransmission timeouts, a signal of loss or a stalled path. pub fn get_num_t3timeouts(&mut self) -> u64 { self.n_t3timeouts } + /// Counts one delayed-acknowledgement timeout. pub fn inc_ack_timeouts(&mut self) { self.n_ack_timeouts += 1; } + /// The number of delayed-acknowledgement timeouts. pub fn get_num_ack_timeouts(&mut self) -> u64 { self.n_ack_timeouts } + /// Counts one fast retransmission. pub fn inc_fast_retrans(&mut self) { self.n_fast_retrans += 1; } + /// The number of fast retransmissions, triggered by SACK gap reports rather than a timeout. pub fn get_num_fast_retrans(&mut self) -> u64 { self.n_fast_retrans } + /// Zeroes every counter. pub fn reset(&mut self) { self.n_datas = 0; self.n_sacks = 0; diff --git a/rtc-sctp/src/association/stream.rs b/rtc-sctp/src/association/stream.rs index cbef49dc..96320457 100644 --- a/rtc-sctp/src/association/stream.rs +++ b/rtc-sctp/src/association/stream.rs @@ -2,7 +2,7 @@ use crate::association::Association; use crate::association::state::AssociationState; use crate::chunk::chunk_payload_data::{ChunkPayloadData, PayloadProtocolIdentifier}; use crate::queue::reassembly_queue::{Chunks, ReassemblyQueue}; -use crate::{ErrorCauseCode, Event, Side}; +use crate::{AssociationError, ErrorCauseCode, Event, Side}; use shared::error::{Error, Result}; use crate::util::{ByteSlice, BytesArray, BytesChunk, BytesSource}; @@ -18,7 +18,10 @@ pub type StreamId = u16; #[non_exhaustive] pub enum StreamEvent { /// One or more new streams has been opened - Opened { id: StreamId }, + Opened { + /// Which stream was opened. + id: StreamId, + }, /// A currently open stream has data or errors waiting to be read Readable { /// Which stream is now readable @@ -70,6 +73,7 @@ pub enum StreamEvent { /// Reliability type for stream #[derive(Default, Debug, Copy, Clone, PartialEq)] +#[non_exhaustive] pub enum ReliabilityType { /// ReliabilityTypeReliable is used for reliable transmission #[default] @@ -123,7 +127,15 @@ impl Stream<'_> { if let Some(s) = self.association.streams.get_mut(&self.stream_identifier) && (s.state == RecvSendState::ReadWritable || s.state == RecvSendState::Readable) { - Ok(s.reassembly_queue.read()) + let chunks = s.reassembly_queue.read(); + if s.reset_pending && !s.reassembly_queue.is_readable() { + // The peer reset this stream while data was still queued for + // delivery; now that the consumer has drained the queue, + // complete the deferred teardown. + self.association + .unregister_stream(self.stream_identifier, AssociationError::Reset); + } + Ok(chunks) } else { Err(Error::ErrStreamClosed) } @@ -159,9 +171,8 @@ impl Stream<'_> { /// /// Unlike [`write_with_ppi`](Self::write_with_ppi), which takes a `&[u8]` and /// must copy it into a freshly allocated buffer, this enqueues the payload - /// zero-copy: each fragment is a refcounted slice of `data` (see - /// [`BytesChunk`]). Prefer this on the hot send path when the caller already - /// owns the payload as `Bytes`. + /// zero-copy: each fragment is a refcounted slice of `data`. Prefer this on the + /// hot send path when the caller already owns the payload as `Bytes`. /// /// Returns the number of bytes successfully written. pub fn write_chunk_with_ppi( @@ -230,6 +241,7 @@ impl Stream<'_> { } } + /// Whether this stream has data or an error waiting to be read. pub fn is_readable(&self) -> bool { if let Some(s) = self.association.streams.get(&self.stream_identifier) { s.state == RecvSendState::Readable || s.state == RecvSendState::ReadWritable @@ -238,6 +250,7 @@ impl Stream<'_> { } } + /// Whether this stream can currently accept more data. pub fn is_writable(&self) -> bool { if let Some(s) = self.association.streams.get(&self.stream_identifier) { s.state == RecvSendState::Writable || s.state == RecvSendState::ReadWritable @@ -388,6 +401,7 @@ impl Stream<'_> { } #[derive(Default, Debug, Copy, Clone, Eq, PartialEq)] +#[non_exhaustive] pub enum RecvSendState { #[default] Closed = 0, @@ -423,6 +437,10 @@ pub struct StreamState { pub(crate) buffered_amount: usize, pub(crate) buffered_amount_low: usize, pub(crate) buffered_amount_high: usize, + /// The peer reset this incoming stream while the reassembly queue still + /// held deliverable data. Teardown is deferred until the consumer drains + /// the queue via `read_sctp`; no new inbound DATA is accepted meanwhile. + pub(crate) reset_pending: bool, } impl StreamState { pub(crate) fn new( @@ -445,6 +463,7 @@ impl StreamState { buffered_amount: 0, buffered_amount_low: 0, buffered_amount_high: u32::MAX as usize, + reset_pending: false, } } diff --git a/rtc-sctp/src/association/timer.rs b/rtc-sctp/src/association/timer.rs index 8de4374d..83de856f 100644 --- a/rtc-sctp/src/association/timer.rs +++ b/rtc-sctp/src/association/timer.rs @@ -7,12 +7,24 @@ const NO_MAX_RETRANS: usize = usize::MAX; const TIMER_COUNT: usize = 6; #[derive(Debug, Copy, Clone)] +/// Retransmission limits for the association's timers. +/// +/// Each field caps how many times the corresponding timer may fire before the association is +/// abandoned. `Default` follows the RFC 4960 recommendations. pub struct TimerConfig { + /// How many times INIT may be retransmitted (T1-init) before the association fails. pub max_t1_init_retrans: usize, + /// How many times COOKIE-ECHO may be retransmitted (T1-cookie). pub max_t1_cookie_retrans: usize, + /// How many times SHUTDOWN may be retransmitted (T2-shutdown). pub max_t2_shutdown_retrans: usize, + /// How many times a DATA chunk may be retransmitted on T3-rtx expiry. + /// + /// Defaults to unlimited, leaving reliability to the per-stream partial-reliability settings. pub max_t3_rtx_retrans: usize, + /// How many times a RE-CONFIG chunk (stream reset) may be retransmitted. pub max_reconfig_retrans: usize, + /// How many times a delayed SACK may be retransmitted. pub max_ack_retrans: usize, } diff --git a/rtc-sctp/src/chunk/chunk_payload_data.rs b/rtc-sctp/src/chunk/chunk_payload_data.rs index abf154c6..06f2a759 100644 --- a/rtc-sctp/src/chunk/chunk_payload_data.rs +++ b/rtc-sctp/src/chunk/chunk_payload_data.rs @@ -15,13 +15,22 @@ pub(crate) const PAYLOAD_DATA_HEADER_SIZE: usize = 12; /// #[derive(Default, Debug, Copy, Clone, PartialEq)] #[repr(C)] +#[non_exhaustive] pub enum PayloadProtocolIdentifier { + /// `WebRTC DCEP` (50): a Data Channel Establishment Protocol control message. Dcep = 50, + /// `WebRTC String` (51): a non-empty UTF-8 string message. String = 51, + /// `WebRTC Binary` (53): a non-empty binary message. Binary = 53, + /// `WebRTC String Empty` (56): an empty string message. + /// + /// Needed because SCTP cannot carry a zero-length payload. StringEmpty = 56, + /// `WebRTC Binary Empty` (57): an empty binary message. BinaryEmpty = 57, #[default] + /// An identifier this crate does not recognise. Unknown, } diff --git a/rtc-sctp/src/config.rs b/rtc-sctp/src/config.rs index 55fa1819..faeab175 100644 --- a/rtc-sctp/src/config.rs +++ b/rtc-sctp/src/config.rs @@ -39,56 +39,69 @@ impl Default for TransportConfig { } impl TransportConfig { + /// Sets the SCTP port. WebRTC always uses 5000. pub fn with_sctp_port(mut self, value: u16) -> Self { self.sctp_port = value; self } + /// Sets the advertised receive window (a_rwnd), bounding how much unacknowledged data a peer + /// may have in flight toward this endpoint. pub fn with_max_receive_buffer_size(mut self, value: u32) -> Self { self.max_receive_buffer_size = value; self } + /// Sets the largest message this endpoint will accept. pub fn with_max_message_size(mut self, value: u32) -> Self { self.max_message_size = value; self } + /// Sets how many outbound streams to request during the handshake. pub fn with_max_num_outbound_streams(mut self, value: u16) -> Self { self.max_num_outbound_streams = value; self } + /// Sets how many inbound streams this endpoint will accept. pub fn with_max_num_inbound_streams(mut self, value: u16) -> Self { self.max_num_inbound_streams = value; self } + /// Overrides the retransmission limits; see [`TimerConfig`]. pub fn with_timer_config(mut self, value: TimerConfig) -> Self { self.timer_config = value; self } + /// The configured SCTP port. pub fn sctp_port(&self) -> u16 { self.sctp_port } + /// The configured receive window in bytes. pub fn max_receive_buffer_size(&self) -> u32 { self.max_receive_buffer_size } + /// The configured maximum message size in bytes. pub fn max_message_size(&self) -> u32 { self.max_message_size } + /// The configured outbound stream count. pub fn max_num_outbound_streams(&self) -> u16 { self.max_num_outbound_streams } + /// The configured inbound stream count. pub fn max_num_inbound_streams(&self) -> u16 { self.max_num_inbound_streams } + /// The configured retransmission limits. pub fn timer_config(&self) -> TimerConfig { self.timer_config } diff --git a/rtc-sctp/src/endpoint/endpoint_test.rs b/rtc-sctp/src/endpoint/endpoint_test.rs index 81dc6d07..e13300f5 100644 --- a/rtc-sctp/src/endpoint/endpoint_test.rs +++ b/rtc-sctp/src/endpoint/endpoint_test.rs @@ -2078,6 +2078,71 @@ fn test_assoc_reset_close_both_ways() -> Result<()> { Ok(()) } +/// Regression test: an incoming stream reset must not discard messages that +/// were received (and reassembled) but not yet delivered to the application. +/// +/// A peer that sends N messages and then immediately closes the channel +/// produces DATA chunks and the outgoing-stream-reset RECONFIG so close +/// together that the receiver processes them in the same input batch. The +/// stream teardown must be deferred until the consumer drains the reassembly +/// queue via `read_sctp`. +#[test] +fn test_assoc_reset_defers_teardown_until_undelivered_data_read() -> Result<()> { + let si: u16 = 1; + let msgs: Vec = (0..4u8).map(|i| Bytes::from(vec![b'A' + i; 256])).collect(); + + let (mut pair, client_ch, server_ch) = create_association_pair(AckMode::NoDelay, 0)?; + establish_session_pair(&mut pair, client_ch, server_ch, si)?; + + // Send all messages, then immediately reset the stream (close-after-send) + // without letting the server read in between. The server then handles the + // tail DATA chunk(s) and the RECONFIG in the same batch. + for msg in &msgs { + let n = pair + .client_stream(client_ch, si)? + .write_sctp(msg, PayloadProtocolIdentifier::Binary)?; + assert_eq!(msg.len(), n); + } + pair.client_stream(client_ch, si)?.stop()?; // send reset + + pair.drive(); + + // Every message must still be deliverable even though the reset has + // already been processed (and answered "Success - Performed"). + let mut buf = vec![0u8; 1024]; + for (i, msg) in msgs.iter().enumerate() { + let chunks = pair + .server_stream(server_ch, si) + .unwrap_or_else(|e| panic!("stream gone before reading message {i}: {e:?}")) + .read_sctp()? + .unwrap_or_else(|| panic!("no data for message {i}")); + let n = chunks.read(&mut buf)?; + assert_eq!(&buf[..n], msg.as_ref(), "message {i} mismatch"); + } + + // Only after the queue is drained should the stream be torn down. + assert!( + pair.server_stream(server_ch, si).is_err(), + "stream should be unregistered once drained" + ); + + // ... and the close (AssociationLost/Reset) surfaced to the consumer. + let mut saw_reset = false; + while let Some(ev) = pair.server_conn_mut(server_ch).poll() { + if let Event::AssociationLost { reason, id } = ev { + assert_eq!(id, si); + assert_matches!(reason, AssociationError::Reset); + saw_reset = true; + } + } + assert!(saw_reset, "AssociationLost(Reset) should be emitted"); + + pair.drive(); + close_association_pair(&mut pair, client_ch, server_ch, si); + + Ok(()) +} + #[test] fn test_assoc_abort() -> Result<()> { //let _guard = subscribe(); diff --git a/rtc-sctp/src/endpoint/mod.rs b/rtc-sctp/src/endpoint/mod.rs index 41a303f0..15085363 100644 --- a/rtc-sctp/src/endpoint/mod.rs +++ b/rtc-sctp/src/endpoint/mod.rs @@ -355,6 +355,7 @@ impl IndexMut for Slab { /// Event resulting from processing a single datagram #[allow(clippy::large_enum_variant)] // Not passed around extensively +#[non_exhaustive] pub enum DatagramEvent { /// The datagram is redirected to its `Association` AssociationEvent(AssociationEvent), @@ -366,6 +367,7 @@ pub enum DatagramEvent { /// /// These arise before any I/O has been performed. #[derive(Debug, Error, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum ConnectError { /// The endpoint can no longer create new associations /// diff --git a/rtc-sctp/src/fuzzing.rs b/rtc-sctp/src/fuzzing.rs index fcf40523..03c51f3c 100644 --- a/rtc-sctp/src/fuzzing.rs +++ b/rtc-sctp/src/fuzzing.rs @@ -1,3 +1,9 @@ +//! Entry points for fuzz targets and benchmarks. +//! +//! Each function drives one encode or decode step over a raw byte slice, so a fuzzer or a +//! benchmark can reach the packet codec without establishing an association first. Gated behind +//! `cfg(fuzzing)` or the `bench` feature; not part of the supported API and not subject to the +//! crate's stability guarantees. use bytes::Bytes; use shared::error::Result; diff --git a/rtc-sctp/src/lib.rs b/rtc-sctp/src/lib.rs index 390281fb..3d299b75 100644 --- a/rtc-sctp/src/lib.rs +++ b/rtc-sctp/src/lib.rs @@ -1,18 +1,54 @@ -//! Low-level protocol logic for the SCTP protocol +//! SCTP for the Sans-I/O WebRTC stack. //! -//! sctp-proto contains a fully deterministic implementation of SCTP protocol logic. It contains -//! no networking code and does not get any relevant timestamps from the operating system. Most -//! users may want to use the futures-based sctp-async API instead. +//! The Stream Control Transmission Protocol ([RFC 4960]) with the extensions WebRTC data +//! channels need: partial reliability ([RFC 3758]) and stream reset / reconfiguration +//! ([RFC 6525]). In WebRTC, SCTP runs *over* DTLS rather than over IP, and carries the data +//! channels described by [`rtc-datachannel`]. //! -//! The sctp-proto API might be of interest if you want to use it from a C or C++ project -//! through C bindings or if you want to use a different event loop than the one tokio provides. +//! This is a fully deterministic implementation of the protocol logic. It contains no +//! networking code and reads no clock of its own: you feed it datagrams and time, and poll it +//! for the datagrams and events it produces. That is what makes it testable without a network +//! and reusable under any executor. //! -//! The most important types are `Endpoint`, which conceptually represents the protocol state for -//! a single socket and mostly manages configuration and dispatches incoming datagrams to the -//! related `Association`. `Association` types contain the bulk of the protocol logic related to -//! managing a single association and all the related state (such as streams). +//! # Structure +//! +//! * [`Endpoint`] — the protocol state for one socket. It holds configuration and dispatches +//! inbound datagrams to the right association. +//! * [`Association`] — the bulk of the logic for a single +//! association: handshake, congestion control, retransmission, and its streams. +//! * [`Stream`] — one stream's reads, writes and reliability +//! settings. +//! * [`Chunks`] — a reassembled inbound message, delivered once every fragment has arrived. +//! +//! # Example +//! +//! Configuration is plain data, and the association is driven entirely by the caller — feed it +//! datagrams and time, poll it for output: +//! +//! ``` +//! use rtc_sctp::{EndpointConfig, TransportConfig}; +//! use std::sync::Arc; +//! +//! let transport = TransportConfig::default() +//! .with_max_message_size(65_536) +//! .with_max_num_outbound_streams(1024); +//! +//! let endpoint_config = Arc::new(EndpointConfig::new()); +//! assert_eq!(transport.max_message_size(), 65_536); +//! # let _ = endpoint_config; +//! ``` +//! +//! Most applications do not depend on this crate directly — the [`rtc`](https://docs.rs/rtc) +//! crate drives it as one layer of the peer-connection pipeline and exposes data channels, +//! and [`webrtc`](https://docs.rs/webrtc) wraps that in an async API. +//! +//! [RFC 4960]: https://datatracker.ietf.org/doc/html/rfc4960 +//! [RFC 3758]: https://datatracker.ietf.org/doc/html/rfc3758 +//! [RFC 6525]: https://datatracker.ietf.org/doc/html/rfc6525 +//! [`rtc-datachannel`]: https://docs.rs/rtc-datachannel #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] #![allow(clippy::bool_to_int_with_if)] @@ -51,6 +87,11 @@ pub use crate::queue::reassembly_queue::{Chunk, Chunks}; pub(crate) mod util; +/// Entry points for fuzz targets and benchmarks. +/// +/// Thin wrappers that drive one encode or decode step over a raw byte slice, so a fuzzer or +/// a benchmark can reach the packet codec without setting up an association. Gated behind +/// `cfg(fuzzing)` or the `bench` feature; not part of the supported API. #[cfg(any(fuzzing, feature = "bench"))] pub mod fuzzing; @@ -102,7 +143,10 @@ use crate::packet::PartialDecode; /// Payload in Incoming/outgoing Transmit #[derive(Debug)] +#[non_exhaustive] pub enum Payload { + /// An inbound packet whose header has been decoded but whose chunks have not. PartialDecode(PartialDecode), + /// Outbound packets, already encoded and ready to hand to the transport. RawEncode(Vec), } diff --git a/rtc-sctp/src/param/param_state_cookie.rs b/rtc-sctp/src/param/param_state_cookie.rs index 8c861716..f9fd6d7f 100644 --- a/rtc-sctp/src/param/param_state_cookie.rs +++ b/rtc-sctp/src/param/param_state_cookie.rs @@ -51,6 +51,8 @@ impl Param for ParamStateCookie { impl ParamStateCookie { pub(crate) fn new() -> Self { + // This 256-bit value is an unpredictable challenge echoed by the peer. SCTP remains usable + // without an RTC crypto provider, so this deliberately uses `rand`'s thread-local CSPRNG. let mut cookie = BytesMut::new(); cookie.resize(32, 0); rand::rng().fill(cookie.as_mut()); @@ -60,3 +62,21 @@ impl ParamStateCookie { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generated_state_cookies_have_full_entropy_width() { + assert_eq!(ParamStateCookie::new().cookie.len(), 32); + } + + #[test] + fn generated_state_cookies_are_distinct() { + assert_ne!( + ParamStateCookie::new().cookie, + ParamStateCookie::new().cookie + ); + } +} diff --git a/rtc-sctp/src/queue/reassembly_queue.rs b/rtc-sctp/src/queue/reassembly_queue.rs index 60eb5672..16c0b0f1 100644 --- a/rtc-sctp/src/queue/reassembly_queue.rs +++ b/rtc-sctp/src/queue/reassembly_queue.rs @@ -19,7 +19,9 @@ pub struct Chunk { pub struct Chunks { /// used only with the ordered chunks pub ssn: u16, + /// The payload protocol identifier shared by every fragment of this message. pub ppi: PayloadProtocolIdentifier, + /// The fragments, in order, that make up one complete message. pub chunks: Vec, offset: usize, index: usize, @@ -27,10 +29,12 @@ pub struct Chunks { } impl Chunks { + /// Whether the reassembled message carries no bytes. pub fn is_empty(&self) -> bool { self.len() == 0 } + /// The total length in bytes of the reassembled message. pub fn len(&self) -> usize { let mut l = 0; for c in &self.chunks { @@ -60,7 +64,12 @@ impl Chunks { Ok(buf) } - // Concat all fragments into the buffer + /// Concatenates every fragment into `buf`, returning the number of bytes written. + /// + /// # Errors + /// + /// Returns [`Error::ErrShortBuffer`](shared::error::Error::ErrShortBuffer) if `buf` cannot + /// hold the whole message; the partial copy is left in place. pub fn read(&self, buf: &mut [u8]) -> Result { let mut n_written = 0; for c in &self.chunks { @@ -75,6 +84,10 @@ impl Chunks { Ok(n_written) } + /// Yields the next slice of the reassembled message, up to `max_length` bytes. + /// + /// Advances an internal cursor, so repeated calls walk the message; returns `None` once it is + /// exhausted. pub fn next(&mut self, max_length: usize) -> Option { if self.index >= self.chunks.len() { return None; diff --git a/rtc-sctp/src/util.rs b/rtc-sctp/src/util.rs index e96a2e98..3c945351 100644 --- a/rtc-sctp/src/util.rs +++ b/rtc-sctp/src/util.rs @@ -42,7 +42,11 @@ pub trait AssociationIdGenerator: Send + Sync { fn aid_lifetime(&self) -> Option; } -/// Generates purely random Association IDs of a certain length +/// Generates nonzero, unpredictable SCTP verification tags. +/// +/// SCTP is usable as a standalone crate and does not naturally own an RTC crypto provider. The +/// `rand` convenience API used here draws from its thread-local CSPRNG, preserving that standalone +/// API while satisfying the verification tag's security requirement. #[derive(Default, Debug, Clone, Copy)] pub struct RandomAssociationIdGenerator { lifetime: Option, @@ -63,7 +67,12 @@ impl RandomAssociationIdGenerator { impl AssociationIdGenerator for RandomAssociationIdGenerator { fn generate_aid(&mut self) -> AssociationId { - rand::random::() + loop { + let association_id = rand::random::(); + if association_id != 0 { + return association_id; + } + } } fn aid_lifetime(&self) -> Option { @@ -315,6 +324,14 @@ mod test { use super::*; + #[test] + fn random_association_ids_are_nonzero() { + let mut generator = RandomAssociationIdGenerator::new(); + for _ in 0..128 { + assert_ne!(generator.generate_aid(), 0); + } + } + #[test] fn test_bytes_chunk_pops_zero_copy_slices() { let data = Bytes::from(vec![0x5au8; 100]); diff --git a/rtc-sdp/Cargo.toml b/rtc-sdp/Cargo.toml index 9e32093b..47d45e75 100644 --- a/rtc-sdp/Cargo.toml +++ b/rtc-sdp/Cargo.toml @@ -14,7 +14,7 @@ categories.workspace = true [dependencies] shared = { workspace = true, default-features = false, features = [] } -url = "2.5.0" +url.workspace = true rand.workspace = true [dev-dependencies] diff --git a/rtc-sdp/src/description/common.rs b/rtc-sdp/src/description/common.rs index 7df05e09..e6288ac3 100644 --- a/rtc-sdp/src/description/common.rs +++ b/rtc-sdp/src/description/common.rs @@ -1,96 +1,115 @@ -use std::fmt; - -use super::session::ATTR_KEY_CANDIDATE; - -/// Information describes the "i=" field which provides textual information -/// about the session. -pub type Information = String; - -/// ConnectionInformation defines the representation for the "c=" field -/// containing connection data. -#[derive(Debug, Default, Clone)] -pub struct ConnectionInformation { - pub network_type: String, - pub address_type: String, - pub address: Option

, -} - -impl fmt::Display for ConnectionInformation { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(address) = &self.address { - write!(f, "{} {} {}", self.network_type, self.address_type, address,) - } else { - write!(f, "{} {}", self.network_type, self.address_type,) - } - } -} - -/// Address describes a structured address token from within the "c=" field. -#[derive(Debug, Default, Clone)] -pub struct Address { - pub address: String, - pub ttl: Option, - pub range: Option, -} - -impl fmt::Display for Address { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.address)?; - if let Some(t) = &self.ttl { - write!(f, "/{t}")?; - } - if let Some(r) = &self.range { - write!(f, "/{r}")?; - } - Ok(()) - } -} - -/// Bandwidth describes an optional field which denotes the proposed bandwidth -/// to be used by the session or media. -#[derive(Debug, Default, Clone)] -pub struct Bandwidth { - pub experimental: bool, - pub bandwidth_type: String, - pub bandwidth: u64, -} - -impl fmt::Display for Bandwidth { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let output = if self.experimental { "X-" } else { "" }; - write!(f, "{}{}:{}", output, self.bandwidth_type, self.bandwidth) - } -} - -/// EncryptionKey describes the "k=" which conveys encryption key information. -pub type EncryptionKey = String; - -/// Attribute describes the "a=" field which represents the primary means for -/// extending SDP. -#[derive(Debug, Default, Clone)] -pub struct Attribute { - pub key: String, - pub value: Option, -} - -impl fmt::Display for Attribute { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(value) = &self.value { - write!(f, "{}:{}", self.key, value) - } else { - write!(f, "{}", self.key) - } - } -} - -impl Attribute { - /// new constructs a new attribute - pub fn new(key: String, value: Option) -> Self { - Attribute { key, value } - } - - /// is_ice_candidate returns true if the attribute key equals "candidate". - pub fn is_ice_candidate(&self) -> bool { - self.key.as_str() == ATTR_KEY_CANDIDATE - } -} +//! Fields shared by session and media descriptions. +//! +//! `c=` connection data ([`ConnectionInformation`](crate::description::common::ConnectionInformation), [`Address`](crate::description::common::Address)), `b=` bandwidth +//! ([`Bandwidth`](crate::description::common::Bandwidth)) and `a=` attributes ([`Attribute`](crate::description::common::Attribute)) may appear at either level in SDP, with +//! the media-level value overriding the session-level one — so they are modelled once here. +//! +//! An [`Attribute`](crate::description::common::Attribute) with no value is a flag, which is how `a=rtcp-mux` and the direction +//! attributes are expressed. +use std::fmt; + +use super::session::ATTR_KEY_CANDIDATE; + +/// Information describes the "i=" field which provides textual information +/// about the session. +pub type Information = String; + +/// ConnectionInformation defines the representation for the "c=" field +/// containing connection data. +#[derive(Debug, Default, Clone)] +pub struct ConnectionInformation { + /// The network type, always `IN` (Internet) in practice. + pub network_type: String, + /// The address type, `IP4` or `IP6`. + pub address_type: String, + /// The connection address, absent for a bare `c=` line. + pub address: Option
, +} + +impl fmt::Display for ConnectionInformation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(address) = &self.address { + write!(f, "{} {} {}", self.network_type, self.address_type, address,) + } else { + write!(f, "{} {}", self.network_type, self.address_type,) + } + } +} + +/// Address describes a structured address token from within the "c=" field. +#[derive(Debug, Default, Clone)] +pub struct Address { + /// The address itself: a host name, IPv4/IPv6 literal, or multicast group. + pub address: String, + /// The multicast TTL, for multicast addresses. + pub ttl: Option, + /// The number of consecutive multicast addresses in the range. + pub range: Option, +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.address)?; + if let Some(t) = &self.ttl { + write!(f, "/{t}")?; + } + if let Some(r) = &self.range { + write!(f, "/{r}")?; + } + Ok(()) + } +} + +/// Bandwidth describes an optional field which denotes the proposed bandwidth +/// to be used by the session or media. +#[derive(Debug, Default, Clone)] +pub struct Bandwidth { + /// Whether the bandwidth type is experimental, written with an `X-` prefix. + pub experimental: bool, + /// The bandwidth modifier, such as `AS` (application-specific) or `CT` (conference total). + pub bandwidth_type: String, + /// The proposed bandwidth in kilobits per second. + pub bandwidth: u64, +} + +impl fmt::Display for Bandwidth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let output = if self.experimental { "X-" } else { "" }; + write!(f, "{}{}:{}", output, self.bandwidth_type, self.bandwidth) + } +} + +/// EncryptionKey describes the "k=" which conveys encryption key information. +pub type EncryptionKey = String; + +/// Attribute describes the "a=" field which represents the primary means for +/// extending SDP. +#[derive(Debug, Default, Clone)] +pub struct Attribute { + /// The attribute name, the part before the `:`. + pub key: String, + /// The attribute value, or `None` for a flag attribute such as `a=rtcp-mux`. + pub value: Option, +} + +impl fmt::Display for Attribute { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(value) = &self.value { + write!(f, "{}:{}", self.key, value) + } else { + write!(f, "{}", self.key) + } + } +} + +impl Attribute { + /// new constructs a new attribute + pub fn new(key: String, value: Option) -> Self { + Attribute { key, value } + } + + /// is_ice_candidate returns true if the attribute key equals "candidate". + pub fn is_ice_candidate(&self) -> bool { + self.key.as_str() == ATTR_KEY_CANDIDATE + } +} diff --git a/rtc-sdp/src/description/media.rs b/rtc-sdp/src/description/media.rs index 28992599..2f22a6e8 100644 --- a/rtc-sdp/src/description/media.rs +++ b/rtc-sdp/src/description/media.rs @@ -1,312 +1,333 @@ -use std::collections::HashMap; -use std::fmt; - -use url::Url; - -use crate::description::common::*; -use crate::extmap::*; -use crate::util::{Codec, merge_codecs, parse_fmtp, parse_rtcp_fb, parse_rtpmap}; - -/// Constants for extmap key -pub const EXT_MAP_VALUE_TRANSPORT_CC_KEY: u16 = 3; -pub const EXT_MAP_VALUE_TRANSPORT_CC_URI: &str = - "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"; - -fn ext_map_uri() -> HashMap { - let mut m = HashMap::new(); - m.insert( - EXT_MAP_VALUE_TRANSPORT_CC_KEY, - EXT_MAP_VALUE_TRANSPORT_CC_URI, - ); - m -} - -/// MediaDescription represents a media type. -/// -/// ## Specifications -/// -/// * [RFC 4566 §5.14] -/// -/// [RFC 4566 §5.14]: https://tools.ietf.org/html/rfc4566#section-5.14 -#[derive(Debug, Default, Clone)] -pub struct MediaDescription { - /// `m= / ...` - /// - /// - pub media_name: MediaName, - - /// `i=` - /// - /// - pub media_title: Option, - - /// `c= ` - /// - /// - pub connection_information: Option, - - /// `b=:` - /// - /// - pub bandwidth: Vec, - - /// `k=` - /// - /// `k=:` - /// - /// - pub encryption_key: Option, - - /// Attributes are the primary means for extending SDP. Attributes may - /// be defined to be used as "session-level" attributes, "media-level" - /// attributes, or both. - /// - /// - pub attributes: Vec, -} - -impl MediaDescription { - /// Returns whether an attribute exists - pub fn has_attribute(&self, key: &str) -> bool { - self.attributes.iter().any(|a| a.key == key) - } - - /// attribute returns the value of an attribute and if it exists - pub fn attribute(&self, key: &str) -> Option> { - for a in &self.attributes { - if a.key == key { - return Some(a.value.as_ref().map(|s| s.as_ref())); - } - } - None - } - - pub fn codecs(&self) -> HashMap { - let mut codecs: HashMap = HashMap::new(); - - for a in &self.attributes { - let attr = a.to_string(); - if attr.starts_with("rtpmap:") { - if let Ok(codec) = parse_rtpmap(&attr) { - merge_codecs(codec, &mut codecs); - } - } else if attr.starts_with("fmtp:") { - if let Ok(codec) = parse_fmtp(&attr) { - merge_codecs(codec, &mut codecs); - } - } else if attr.starts_with("rtcp-fb:") - && let Ok(codec) = parse_rtcp_fb(&attr) - { - merge_codecs(codec, &mut codecs); - } - } - - codecs - } - - /// new_jsep_media_description creates a new MediaName with - /// some settings that are required by the JSEP spec. - pub fn new_jsep_media_description(codec_type: String, _codec_prefs: Vec<&str>) -> Self { - MediaDescription { - media_name: MediaName { - media: codec_type, - port: RangedPort { - value: 9, - range: None, - }, - protos: vec![ - "UDP".to_string(), - "TLS".to_string(), - "RTP".to_string(), - "SAVPF".to_string(), - ], - formats: vec![], - }, - media_title: None, - connection_information: Some(ConnectionInformation { - network_type: "IN".to_string(), - address_type: "IP4".to_string(), - address: Some(Address { - address: "0.0.0.0".to_string(), - ttl: None, - range: None, - }), - }), - bandwidth: vec![], - encryption_key: None, - attributes: vec![], - } - } - - /// with_property_attribute adds a property attribute 'a=key' to the media description - pub fn with_property_attribute(mut self, key: String) -> Self { - self.attributes.push(Attribute::new(key, None)); - self - } - - /// with_value_attribute adds a value attribute 'a=key:value' to the media description - pub fn with_value_attribute(mut self, key: String, value: String) -> Self { - self.attributes.push(Attribute::new(key, Some(value))); - self - } - - /// with_fingerprint adds a fingerprint to the media description - pub fn with_fingerprint(self, algorithm: String, value: String) -> Self { - self.with_value_attribute("fingerprint".to_owned(), algorithm + " " + &value) - } - - /// with_ice_credentials adds ICE credentials to the media description - pub fn with_ice_credentials(self, username: String, password: String) -> Self { - self.with_value_attribute("ice-ufrag".to_string(), username) - .with_value_attribute("ice-pwd".to_string(), password) - } - - /// with_codec adds codec information to the media description - pub fn with_codec( - mut self, - payload_type: u8, - name: String, - clockrate: u32, - channels: u16, - fmtp: String, - ) -> Self { - self.media_name.formats.push(payload_type.to_string()); - let rtpmap = if channels > 0 { - format!("{payload_type} {name}/{clockrate}/{channels}") - } else { - format!("{payload_type} {name}/{clockrate}") - }; - - if !fmtp.is_empty() { - self.with_value_attribute("rtpmap".to_string(), rtpmap) - .with_value_attribute("fmtp".to_string(), format!("{payload_type} {fmtp}")) - } else { - self.with_value_attribute("rtpmap".to_string(), rtpmap) - } - } - - /// with_media_source adds media source information to the media description - pub fn with_media_source( - self, - ssrc: u32, - cname: String, - stream_id: String, - track_id: String, - ) -> Self { - self. - with_value_attribute("ssrc".to_string(), format!("{ssrc} cname:{cname}")). // Deprecated but not phased out? - with_value_attribute("ssrc".to_string(), format!("{ssrc} msid:{stream_id} {track_id}")). - with_value_attribute("ssrc".to_string(), format!("{ssrc} mslabel:{stream_id}")). // Deprecated but not phased out? - with_value_attribute("ssrc".to_string(), format!("{ssrc} label:{track_id}")) - // Deprecated but not phased out? - } - - /// with_candidate adds an ICE candidate to the media description - /// Deprecated: use WithICECandidate instead - pub fn with_candidate(self, value: String) -> Self { - self.with_value_attribute("candidate".to_string(), value) - } - - pub fn with_extmap(self, e: ExtMap) -> Self { - self.with_property_attribute(e.marshal()) - } - - /// with_transport_cc_extmap adds an extmap to the media description - pub fn with_transport_cc_extmap(self) -> Self { - let uri = { - let m = ext_map_uri(); - if let Some(uri_str) = m.get(&EXT_MAP_VALUE_TRANSPORT_CC_KEY) { - Url::parse(uri_str).ok() - } else { - None - } - }; - - let e = ExtMap { - value: EXT_MAP_VALUE_TRANSPORT_CC_KEY, - uri, - ..Default::default() - }; - - self.with_extmap(e) - } -} - -/// RangedPort supports special format for the media field "m=" port value. If -/// it may be necessary to specify multiple transport ports, the protocol allows -/// to write it as: `/` where number of ports is a an -/// offsetting range. -#[derive(Debug, Default, Clone)] -pub struct RangedPort { - pub value: isize, - pub range: Option, -} - -impl fmt::Display for RangedPort { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(range) = self.range { - write!(f, "{}/{}", self.value, range) - } else { - write!(f, "{}", self.value) - } - } -} - -/// MediaName describes the "m=" field storage structure. -#[derive(Debug, Default, Clone)] -pub struct MediaName { - pub media: String, - pub port: RangedPort, - pub protos: Vec, - pub formats: Vec, -} - -impl fmt::Display for MediaName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{} {}", self.media, self.port)?; - - let mut first = true; - for part in &self.protos { - if first { - first = false; - write!(f, " {part}")?; - } else { - write!(f, "/{part}")?; - } - } - - for part in &self.formats { - write!(f, " {part}")?; - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::MediaDescription; - - #[test] - fn test_attribute_missing() { - let media_description = MediaDescription::default(); - - assert_eq!(media_description.attribute("recvonly"), None); - } - - #[test] - fn test_attribute_present_with_no_value() { - let media_description = - MediaDescription::default().with_property_attribute("recvonly".to_owned()); - - assert_eq!(media_description.attribute("recvonly"), Some(None)); - } - - #[test] - fn test_attribute_present_with_value() { - let media_description = - MediaDescription::default().with_value_attribute("ptime".to_owned(), "1".to_owned()); - - assert_eq!(media_description.attribute("ptime"), Some(Some("1"))); - } -} +//! The `m=` media description. +//! +//! One [`MediaDescription`](crate::description::media::MediaDescription) is a media type, a transport port and protocol, a list of formats, +//! and the attributes that describe them. For RTP media the formats are payload types, and +//! [`MediaDescription::codecs`](crate::description::media::MediaDescription::codecs) assembles them into [`Codec`](crate::util::Codec)s by joining the +//! `a=rtpmap`, `a=fmtp` and `a=rtcp-fb` attributes that belong to each. +//! +//! [`RangedPort`](crate::description::media::RangedPort) exists because a media section may claim consecutive ports (`/`), +//! which RTP/RTCP without multiplexing needs. +use std::collections::HashMap; +use std::fmt; + +use url::Url; + +use crate::description::common::*; +use crate::extmap::*; +use crate::util::{Codec, merge_codecs, parse_fmtp, parse_rtcp_fb, parse_rtpmap}; + +/// Constants for extmap key +pub const EXT_MAP_VALUE_TRANSPORT_CC_KEY: u16 = 3; +/// The transport-wide congestion control header-extension URI. +pub const EXT_MAP_VALUE_TRANSPORT_CC_URI: &str = + "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"; + +fn ext_map_uri() -> HashMap { + let mut m = HashMap::new(); + m.insert( + EXT_MAP_VALUE_TRANSPORT_CC_KEY, + EXT_MAP_VALUE_TRANSPORT_CC_URI, + ); + m +} + +/// MediaDescription represents a media type. +/// +/// ## Specifications +/// +/// * [RFC 4566 §5.14] +/// +/// [RFC 4566 §5.14]: https://tools.ietf.org/html/rfc4566#section-5.14 +#[derive(Debug, Default, Clone)] +pub struct MediaDescription { + /// `m= / ...` + /// + /// + pub media_name: MediaName, + + /// `i=` + /// + /// + pub media_title: Option, + + /// `c= ` + /// + /// + pub connection_information: Option, + + /// `b=:` + /// + /// + pub bandwidth: Vec, + + /// `k=` + /// + /// `k=:` + /// + /// + pub encryption_key: Option, + + /// Attributes are the primary means for extending SDP. Attributes may + /// be defined to be used as "session-level" attributes, "media-level" + /// attributes, or both. + /// + /// + pub attributes: Vec, +} + +impl MediaDescription { + /// Returns whether an attribute exists + pub fn has_attribute(&self, key: &str) -> bool { + self.attributes.iter().any(|a| a.key == key) + } + + /// attribute returns the value of an attribute and if it exists + pub fn attribute(&self, key: &str) -> Option> { + for a in &self.attributes { + if a.key == key { + return Some(a.value.as_ref().map(|s| s.as_ref())); + } + } + None + } + + /// The codecs this media section offers, keyed by payload type. + /// + /// Assembled from the `m=` format list plus the `a=rtpmap`, `a=fmtp` and `a=rtcp-fb` + /// attributes that describe them. + pub fn codecs(&self) -> HashMap { + let mut codecs: HashMap = HashMap::new(); + + for a in &self.attributes { + let attr = a.to_string(); + if attr.starts_with("rtpmap:") { + if let Ok(codec) = parse_rtpmap(&attr) { + merge_codecs(codec, &mut codecs); + } + } else if attr.starts_with("fmtp:") { + if let Ok(codec) = parse_fmtp(&attr) { + merge_codecs(codec, &mut codecs); + } + } else if attr.starts_with("rtcp-fb:") + && let Ok(codec) = parse_rtcp_fb(&attr) + { + merge_codecs(codec, &mut codecs); + } + } + + codecs + } + + /// new_jsep_media_description creates a new MediaName with + /// some settings that are required by the JSEP spec. + pub fn new_jsep_media_description(codec_type: String, _codec_prefs: Vec<&str>) -> Self { + MediaDescription { + media_name: MediaName { + media: codec_type, + port: RangedPort { + value: 9, + range: None, + }, + protos: vec![ + "UDP".to_string(), + "TLS".to_string(), + "RTP".to_string(), + "SAVPF".to_string(), + ], + formats: vec![], + }, + media_title: None, + connection_information: Some(ConnectionInformation { + network_type: "IN".to_string(), + address_type: "IP4".to_string(), + address: Some(Address { + address: "0.0.0.0".to_string(), + ttl: None, + range: None, + }), + }), + bandwidth: vec![], + encryption_key: None, + attributes: vec![], + } + } + + /// with_property_attribute adds a property attribute 'a=key' to the media description + pub fn with_property_attribute(mut self, key: String) -> Self { + self.attributes.push(Attribute::new(key, None)); + self + } + + /// with_value_attribute adds a value attribute 'a=key:value' to the media description + pub fn with_value_attribute(mut self, key: String, value: String) -> Self { + self.attributes.push(Attribute::new(key, Some(value))); + self + } + + /// with_fingerprint adds a fingerprint to the media description + pub fn with_fingerprint(self, algorithm: String, value: String) -> Self { + self.with_value_attribute("fingerprint".to_owned(), algorithm + " " + &value) + } + + /// with_ice_credentials adds ICE credentials to the media description + pub fn with_ice_credentials(self, username: String, password: String) -> Self { + self.with_value_attribute("ice-ufrag".to_string(), username) + .with_value_attribute("ice-pwd".to_string(), password) + } + + /// with_codec adds codec information to the media description + pub fn with_codec( + mut self, + payload_type: u8, + name: String, + clockrate: u32, + channels: u16, + fmtp: String, + ) -> Self { + self.media_name.formats.push(payload_type.to_string()); + let rtpmap = if channels > 0 { + format!("{payload_type} {name}/{clockrate}/{channels}") + } else { + format!("{payload_type} {name}/{clockrate}") + }; + + if !fmtp.is_empty() { + self.with_value_attribute("rtpmap".to_string(), rtpmap) + .with_value_attribute("fmtp".to_string(), format!("{payload_type} {fmtp}")) + } else { + self.with_value_attribute("rtpmap".to_string(), rtpmap) + } + } + + /// with_media_source adds media source information to the media description + pub fn with_media_source( + self, + ssrc: u32, + cname: String, + stream_id: String, + track_id: String, + ) -> Self { + self. + with_value_attribute("ssrc".to_string(), format!("{ssrc} cname:{cname}")). // Deprecated but not phased out? + with_value_attribute("ssrc".to_string(), format!("{ssrc} msid:{stream_id} {track_id}")). + with_value_attribute("ssrc".to_string(), format!("{ssrc} mslabel:{stream_id}")). // Deprecated but not phased out? + with_value_attribute("ssrc".to_string(), format!("{ssrc} label:{track_id}")) + // Deprecated but not phased out? + } + + /// with_candidate adds an ICE candidate to the media description + /// Deprecated: use WithICECandidate instead + pub fn with_candidate(self, value: String) -> Self { + self.with_value_attribute("candidate".to_string(), value) + } + + /// Appends an `a=extmap` header-extension declaration. + pub fn with_extmap(self, e: ExtMap) -> Self { + self.with_property_attribute(e.marshal()) + } + + /// with_transport_cc_extmap adds an extmap to the media description + pub fn with_transport_cc_extmap(self) -> Self { + let uri = { + let m = ext_map_uri(); + if let Some(uri_str) = m.get(&EXT_MAP_VALUE_TRANSPORT_CC_KEY) { + Url::parse(uri_str).ok() + } else { + None + } + }; + + let e = ExtMap { + value: EXT_MAP_VALUE_TRANSPORT_CC_KEY, + uri, + ..Default::default() + }; + + self.with_extmap(e) + } +} + +/// RangedPort supports special format for the media field "m=" port value. If +/// it may be necessary to specify multiple transport ports, the protocol allows +/// to write it as: `/` where number of ports is a an +/// offsetting range. +#[derive(Debug, Default, Clone)] +pub struct RangedPort { + /// The first port number. + pub value: isize, + /// How many consecutive ports the media uses, for `/` form. + pub range: Option, +} + +impl fmt::Display for RangedPort { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(range) = self.range { + write!(f, "{}/{}", self.value, range) + } else { + write!(f, "{}", self.value) + } + } +} + +/// MediaName describes the "m=" field storage structure. +#[derive(Debug, Default, Clone)] +pub struct MediaName { + /// The media type: `audio`, `video`, or `application` for data channels. + pub media: String, + /// The transport port, possibly a range. + pub port: RangedPort, + /// The transport protocol tokens, such as `UDP`, `TLS`, `RTP`, `SAVPF`. + pub protos: Vec, + /// The payload types (for RTP media) or format tokens this section offers. + pub formats: Vec, +} + +impl fmt::Display for MediaName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} {}", self.media, self.port)?; + + let mut first = true; + for part in &self.protos { + if first { + first = false; + write!(f, " {part}")?; + } else { + write!(f, "/{part}")?; + } + } + + for part in &self.formats { + write!(f, " {part}")?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::MediaDescription; + + #[test] + fn test_attribute_missing() { + let media_description = MediaDescription::default(); + + assert_eq!(media_description.attribute("recvonly"), None); + } + + #[test] + fn test_attribute_present_with_no_value() { + let media_description = + MediaDescription::default().with_property_attribute("recvonly".to_owned()); + + assert_eq!(media_description.attribute("recvonly"), Some(None)); + } + + #[test] + fn test_attribute_present_with_value() { + let media_description = + MediaDescription::default().with_value_attribute("ptime".to_owned(), "1".to_owned()); + + assert_eq!(media_description.attribute("ptime"), Some(Some("1"))); + } +} diff --git a/rtc-sdp/src/description/mod.rs b/rtc-sdp/src/description/mod.rs index 0cab15c0..ab4a88d9 100644 --- a/rtc-sdp/src/description/mod.rs +++ b/rtc-sdp/src/description/mod.rs @@ -1,6 +1,9 @@ -#[cfg(test)] -mod description_test; - -pub mod common; -pub mod media; -pub mod session; +#[cfg(test)] +mod description_test; + +/// Field types shared by session and media descriptions (`c=`, `b=`, `a=`). +pub mod common; +/// The `m=` media description and its fields. +pub mod media; +/// The whole session description, plus the well-known attribute keys. +pub mod session; diff --git a/rtc-sdp/src/description/session.rs b/rtc-sdp/src/description/session.rs index 44d4f68d..b533e32a 100644 --- a/rtc-sdp/src/description/session.rs +++ b/rtc-sdp/src/description/session.rs @@ -1,3 +1,14 @@ +//! The session description — a whole SDP document. +//! +//! A [`SessionDescription`](crate::description::session::SessionDescription) is the session-level fields (`v=`, `o=`, `s=`, `t=`, …) followed by +//! any number of [`MediaDescription`](crate::description::media::MediaDescription)s. `unmarshal` parses one +//! from a string and `marshal` prints it back; the round trip is faithful, including attributes +//! this crate does not interpret. +//! +//! The `ATTR_KEY_*` constants name the `a=` attributes WebRTC relies on — `mid`, `msid`, +//! `setup`, `rtcp-mux`, `extmap` and the direction flags — and the `SEMANTIC_TOKEN_*` constants +//! name the grouping semantics used with `a=group` and `a=msid-semantic`. + use std::collections::HashMap; use std::convert::TryFrom; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -14,32 +25,55 @@ use super::media::*; /// Constants for SDP attributes used in JSEP pub const ATTR_KEY_CANDIDATE: &str = "candidate"; +/// `a=end-of-candidates`: ICE gathering for this section is complete. pub const ATTR_KEY_END_OF_CANDIDATES: &str = "end-of-candidates"; +/// `a=identity`: an identity assertion for the session. pub const ATTR_KEY_IDENTITY: &str = "identity"; +/// `a=group`: groups m-lines, as BUNDLE does. pub const ATTR_KEY_GROUP: &str = "group"; +/// `a=ssrc`: declares an SSRC and its attributes. pub const ATTR_KEY_SSRC: &str = "ssrc"; +/// `a=ssrc-group`: relates SSRCs, such as an RTX stream to its primary. pub const ATTR_KEY_SSRC_GROUP: &str = "ssrc-group"; +/// `a=msid`: associates a track with a media stream. pub const ATTR_KEY_MSID: &str = "msid"; +/// `a=msid-semantic`: declares the semantics used by `a=msid`. pub const ATTR_KEY_MSID_SEMANTIC: &str = "msid-semantic"; +/// `a=setup`: the DTLS role — `active`, `passive` or `actpass`. pub const ATTR_KEY_CONNECTION_SETUP: &str = "setup"; +/// `a=mid`: the media identification tag that names this m-line. pub const ATTR_KEY_MID: &str = "mid"; +/// `a=ice-lite`: the endpoint is an ICE-lite implementation. pub const ATTR_KEY_ICELITE: &str = "ice-lite"; +/// `a=rtcp-mux`: RTP and RTCP share one port. pub const ATTR_KEY_RTCPMUX: &str = "rtcp-mux"; +/// `a=rtcp-rsize`: reduced-size RTCP is supported. pub const ATTR_KEY_RTCPRSIZE: &str = "rtcp-rsize"; +/// `a=inactive`: neither send nor receive. pub const ATTR_KEY_INACTIVE: &str = "inactive"; +/// `a=recvonly`: receive only. pub const ATTR_KEY_RECV_ONLY: &str = "recvonly"; +/// `a=sendonly`: send only. pub const ATTR_KEY_SEND_ONLY: &str = "sendonly"; +/// `a=sendrecv`: send and receive. pub const ATTR_KEY_SEND_RECV: &str = "sendrecv"; +/// `a=extmap`: maps an RTP header-extension URI to an id. pub const ATTR_KEY_EXT_MAP: &str = "extmap"; +/// `a=extmap-allow-mixed`: one- and two-byte header extensions may be mixed. pub const ATTR_KEY_EXTMAP_ALLOW_MIXED: &str = "extmap-allow-mixed"; +/// `a=max-message-size`: the largest SCTP message this endpoint accepts. pub const ATTR_KEY_MAX_MESSAGE_SIZE: &str = "max-message-size"; /// Constants for semantic tokens used in JSEP pub const SEMANTIC_TOKEN_LIP_SYNCHRONIZATION: &str = "LS"; +/// `FID`: flow identification — the SSRCs carry the same content, as RTX does. pub const SEMANTIC_TOKEN_FLOW_IDENTIFICATION: &str = "FID"; +/// `FEC`: one SSRC carries error-correction data for another. pub const SEMANTIC_TOKEN_FORWARD_ERROR_CORRECTION: &str = "FEC"; // https://datatracker.ietf.org/doc/html/rfc5956#section-4.1 +/// `FEC-FR`: the FEC framework grouping semantic. pub const SEMANTIC_TOKEN_FORWARD_ERROR_CORRECTION_FRAMEWORK: &str = "FEC-FR"; +/// `WMS`: WebRTC media streams, used with `a=msid-semantic`. pub const SEMANTIC_TOKEN_WEBRTC_MEDIA_STREAMS: &str = "WMS"; /// Version describes the value provided by the "v=" field which gives @@ -50,11 +84,17 @@ pub type Version = isize; /// originator of the session plus a session identifier and version number. #[derive(Debug, Default, Clone)] pub struct Origin { + /// The originator's user name, or `-` when withheld. pub username: String, + /// A globally unique session identifier. pub session_id: u64, + /// Incremented on each modification of the session description. pub session_version: u64, + /// The network type, always `IN` in practice. pub network_type: String, + /// The address type, `IP4` or `IP6`. pub address_type: String, + /// The originator's address. WebRTC does not use it and sends a placeholder. pub unicast_address: String, } @@ -74,6 +114,7 @@ impl fmt::Display for Origin { } impl Origin { + /// An empty session description with SDP version 0. pub fn new() -> Self { Origin { username: "".to_owned(), @@ -104,7 +145,9 @@ pub type PhoneNumber = String; /// repeated sessions scheduling. #[derive(Debug, Default, Clone)] pub struct TimeZone { + /// The time at which the offset takes effect. pub adjustment_time: u64, + /// The offset from the session's base time, in seconds. pub offset: i64, } @@ -134,7 +177,9 @@ pub struct TimeDescription { /// stop times. #[derive(Debug, Default, Clone)] pub struct Timing { + /// Session start time in NTP seconds; `0` means unbounded. pub start_time: u64, + /// Session stop time in NTP seconds; `0` means unbounded. pub stop_time: u64, } @@ -148,8 +193,11 @@ impl fmt::Display for Timing { /// represents the intervals and durations for repeated scheduled sessions. #[derive(Debug, Default, Clone)] pub struct RepeatTime { + /// How often the session repeats, in seconds. pub interval: i64, + /// How long each repetition lasts, in seconds. pub duration: i64, + /// Offsets from the start time at which the session repeats. pub offsets: Vec, } diff --git a/rtc-sdp/src/direction/mod.rs b/rtc-sdp/src/direction/mod.rs index 23e5e22e..ca0e604f 100644 --- a/rtc-sdp/src/direction/mod.rs +++ b/rtc-sdp/src/direction/mod.rs @@ -1,51 +1,52 @@ -use std::fmt; - -#[cfg(test)] -mod direction_test; - -/// Direction is a marker for transmission direction of an endpoint -#[derive(Default, Debug, PartialEq, Eq, Clone)] -pub enum Direction { - #[default] - Unspecified = 0, - /// Direction::SendRecv is for bidirectional communication - SendRecv = 1, - /// Direction::SendOnly is for outgoing communication - SendOnly = 2, - /// Direction::RecvOnly is for incoming communication - RecvOnly = 3, - /// Direction::Inactive is for no communication - Inactive = 4, -} - -const DIRECTION_SEND_RECV_STR: &str = "sendrecv"; -const DIRECTION_SEND_ONLY_STR: &str = "sendonly"; -const DIRECTION_RECV_ONLY_STR: &str = "recvonly"; -const DIRECTION_INACTIVE_STR: &str = "inactive"; -const DIRECTION_UNSPECIFIED_STR: &str = "Unspecified"; - -impl fmt::Display for Direction { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - Direction::SendRecv => DIRECTION_SEND_RECV_STR, - Direction::SendOnly => DIRECTION_SEND_ONLY_STR, - Direction::RecvOnly => DIRECTION_RECV_ONLY_STR, - Direction::Inactive => DIRECTION_INACTIVE_STR, - _ => DIRECTION_UNSPECIFIED_STR, - }; - write!(f, "{s}") - } -} - -impl Direction { - /// new defines a procedure for creating a new direction from a raw string. - pub fn new(raw: &str) -> Self { - match raw { - DIRECTION_SEND_RECV_STR => Direction::SendRecv, - DIRECTION_SEND_ONLY_STR => Direction::SendOnly, - DIRECTION_RECV_ONLY_STR => Direction::RecvOnly, - DIRECTION_INACTIVE_STR => Direction::Inactive, - _ => Direction::Unspecified, - } - } -} +use std::fmt; + +#[cfg(test)] +mod direction_test; + +/// Direction is a marker for transmission direction of an endpoint +#[derive(Default, Debug, PartialEq, Eq, Clone)] +pub enum Direction { + #[default] + /// No direction attribute was present. + Unspecified = 0, + /// Direction::SendRecv is for bidirectional communication + SendRecv = 1, + /// Direction::SendOnly is for outgoing communication + SendOnly = 2, + /// Direction::RecvOnly is for incoming communication + RecvOnly = 3, + /// Direction::Inactive is for no communication + Inactive = 4, +} + +const DIRECTION_SEND_RECV_STR: &str = "sendrecv"; +const DIRECTION_SEND_ONLY_STR: &str = "sendonly"; +const DIRECTION_RECV_ONLY_STR: &str = "recvonly"; +const DIRECTION_INACTIVE_STR: &str = "inactive"; +const DIRECTION_UNSPECIFIED_STR: &str = "Unspecified"; + +impl fmt::Display for Direction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Direction::SendRecv => DIRECTION_SEND_RECV_STR, + Direction::SendOnly => DIRECTION_SEND_ONLY_STR, + Direction::RecvOnly => DIRECTION_RECV_ONLY_STR, + Direction::Inactive => DIRECTION_INACTIVE_STR, + _ => DIRECTION_UNSPECIFIED_STR, + }; + write!(f, "{s}") + } +} + +impl Direction { + /// new defines a procedure for creating a new direction from a raw string. + pub fn new(raw: &str) -> Self { + match raw { + DIRECTION_SEND_RECV_STR => Direction::SendRecv, + DIRECTION_SEND_ONLY_STR => Direction::SendOnly, + DIRECTION_RECV_ONLY_STR => Direction::RecvOnly, + DIRECTION_INACTIVE_STR => Direction::Inactive, + _ => Direction::Unspecified, + } + } +} diff --git a/rtc-sdp/src/extmap/mod.rs b/rtc-sdp/src/extmap/mod.rs index e6afe040..6ebf25c1 100644 --- a/rtc-sdp/src/extmap/mod.rs +++ b/rtc-sdp/src/extmap/mod.rs @@ -1,3 +1,12 @@ +//! `a=extmap` RTP header-extension declarations. +//! +//! An [`ExtMap`](crate::extmap::ExtMap) binds a header-extension URI to the small integer id that will appear in RTP +//! packets. Both sides must agree, which is the whole point of negotiating it in SDP: the id is +//! per-session, while the URI is the stable name. +//! +//! The `*_URI` constants are the extensions this stack uses — audio level, video orientation, +//! absolute send time, transport-wide CC, and the SDES ids that make simulcast demultiplexing +//! possible. #[cfg(test)] mod extmap_test; @@ -11,27 +20,41 @@ use url::Url; /// Default ext values pub const DEF_EXT_MAP_VALUE_ABS_SEND_TIME: usize = 1; +/// The default id this crate assigns to the transport-wide CC extension. pub const DEF_EXT_MAP_VALUE_TRANSPORT_CC: usize = 2; +/// The default id assigned to the SDES `mid` extension. pub const DEF_EXT_MAP_VALUE_SDES_MID: usize = 3; +/// The default id assigned to the SDES RTP stream id extension. pub const DEF_EXT_MAP_VALUE_SDES_RTP_STREAM_ID: usize = 4; +/// The absolute-send-time extension URI, used for bandwidth estimation. pub const ABS_SEND_TIME_URI: &str = "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"; +/// The transport-wide congestion control extension URI. pub const TRANSPORT_CC_URI: &str = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"; +/// The SDES `mid` extension URI, which tags each packet with its m-line. pub const SDES_MID_URI: &str = "urn:ietf:params:rtp-hdrext:sdes:mid"; +/// The SDES RTP stream id (RID) extension URI, which identifies a simulcast layer. pub const SDES_RTP_STREAM_ID_URI: &str = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"; +/// The SDES repaired RTP stream id extension URI, which identifies an RTX layer's target. pub const SDES_REPAIR_RTP_STREAM_ID_URI: &str = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"; +/// The audio-level extension URI, carrying per-packet loudness. pub const AUDIO_LEVEL_URI: &str = "urn:ietf:params:rtp-hdrext:ssrc-audio-level"; +/// The video-orientation (CVO) extension URI, carrying rotation flags. pub const VIDEO_ORIENTATION_URI: &str = "urn:3gpp:video-orientation"; /// ExtMap represents the activation of a single RTP header extension #[derive(Debug, Clone, Default)] pub struct ExtMap { + /// The id this extension is negotiated under, as it appears in RTP packets. pub value: u16, + /// The direction the extension applies in, if the attribute restricted it. pub direction: Direction, + /// The extension's canonical URI. pub uri: Option, + /// Extension-specific attributes trailing the URI. pub ext_attr: Option, } diff --git a/rtc-sdp/src/lib.rs b/rtc-sdp/src/lib.rs index 90581d3f..06fbadcf 100644 --- a/rtc-sdp/src/lib.rs +++ b/rtc-sdp/src/lib.rs @@ -1,9 +1,66 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! SDP parsing and serialization. +//! +//! The Session Description Protocol ([RFC 8866], superseding [RFC 4566]) as WebRTC uses +//! it: offers and answers, media sections, and the attributes that carry codecs, ICE +//! candidates, DTLS fingerprints and header-extension mappings ([RFC 8285]). +//! +//! # Structure +//! +//! * [`SessionDescription`] — a whole session description: `unmarshal` one from a string, +//! `marshal` it back, or build one up section by section. +//! * [`MediaDescription`] — one `m=` section, with its attributes, formats and connection +//! data. +//! * [`extmap`] — `a=extmap` header-extension declarations and the well-known extension +//! URIs. +//! * [`direction`] — `sendrecv`/`sendonly`/`recvonly`/`inactive`. +//! +//! # Example +//! +//! ``` +//! use rtc_sdp::SessionDescription; +//! use std::io::Cursor; +//! +//! # fn example() -> Result<(), Box> { +//! let sdp = "v=0\r\n\ +//! o=- 0 0 IN IP4 127.0.0.1\r\n\ +//! s=-\r\n\ +//! t=0 0\r\n\ +//! m=audio 9 UDP/TLS/RTP/SAVPF 111\r\n\ +//! a=mid:0\r\n\ +//! a=sendrecv\r\n"; +//! +//! let desc = SessionDescription::unmarshal(&mut Cursor::new(sdp))?; +//! for media in &desc.media_descriptions { +//! assert_eq!(media.media_name.media, "audio"); +//! assert_eq!(media.attribute("mid").flatten(), Some("0")); +//! } +//! +//! // Printing it back yields valid SDP. +//! assert!(desc.marshal().starts_with("v=0")); +//! # Ok(()) +//! # } +//! ``` +//! +//! This crate is deliberately a *syntax* layer: it parses and prints SDP faithfully and +//! leaves negotiation semantics ([RFC 8829]) to the [`rtc`](https://docs.rs/rtc) crate, +//! which re-exports it as `rtc::sdp`. +//! +//! [RFC 8866]: https://datatracker.ietf.org/doc/html/rfc8866 +//! [RFC 4566]: https://datatracker.ietf.org/doc/html/rfc4566 +//! [RFC 8285]: https://datatracker.ietf.org/doc/html/rfc8285 +//! [RFC 8829]: https://datatracker.ietf.org/doc/html/rfc8829 + +/// Session and media descriptions — the `v=`/`m=` structure of an SDP document. pub mod description; +/// Transmission direction (`sendrecv`, `sendonly`, `recvonly`, `inactive`). pub mod direction; +/// `a=extmap` RTP header-extension declarations and the well-known extension URIs. pub mod extmap; +/// Parsing helpers plus the codec and connection-role types shared across descriptions. pub mod util; pub(crate) mod lexer; diff --git a/rtc-sdp/src/util/mod.rs b/rtc-sdp/src/util/mod.rs index 92846d8f..3f901ae7 100644 --- a/rtc-sdp/src/util/mod.rs +++ b/rtc-sdp/src/util/mod.rs @@ -6,12 +6,14 @@ use shared::error::{Error, Result}; use std::collections::HashMap; use std::fmt; +/// The `a=` line prefix. pub const ATTRIBUTE_KEY: &str = "a="; /// ConnectionRole indicates which of the end points should initiate the connection establishment #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] pub enum ConnectionRole { #[default] + /// No `a=setup` attribute was present. Unspecified, /// ConnectionRoleActive indicates the endpoint will initiate an outgoing connection. @@ -80,12 +82,20 @@ pub(crate) fn new_session_id() -> u64 { // Codec represents a codec #[derive(Debug, Clone, Default, PartialEq, Eq)] +/// One codec offered by a media section, assembled from its `a=rtpmap`, `a=fmtp` and +/// `a=rtcp-fb` attributes. pub struct Codec { + /// The RTP payload type that identifies this codec in the stream. pub payload_type: u8, + /// The encoding name, such as `VP8` or `opus`. pub name: String, + /// The RTP clock rate in Hz. pub clock_rate: u32, + /// Codec-specific encoding parameters — the channel count, for audio. pub encoding_parameters: String, + /// The `a=fmtp` format parameters, verbatim. pub fmtp: String, + /// The `a=rtcp-fb` feedback types negotiated for this codec, such as `nack` or `goog-remb`. pub rtcp_feedback: Vec, } diff --git a/rtc-shared/Cargo.toml b/rtc-shared/Cargo.toml index c29db68a..1ff1c19b 100644 --- a/rtc-shared/Cargo.toml +++ b/rtc-shared/Cargo.toml @@ -12,22 +12,16 @@ keywords.workspace = true categories.workspace = true [features] -default = ["crypto", "ifaces", "marshal", "replay"] -crypto = [] +default = ["ifaces", "marshal", "replay"] ifaces = [] marshal = [] replay = [] [dependencies] thiserror.workspace = true -substring = "1.4.5" +substring.workspace = true bytes.workspace = true -aes-gcm = { version = "0.10.3", features = ["std"] } -url = "2.5.0" -rcgen.workspace = true -sec1 = { version = "0.7.3", features = ["std"] } -p256 = { version = "0.13.2", features = ["default", "ecdh", "ecdsa"] } -aes = "0.8.4" +url.workspace = true rand.workspace = true serde.workspace = true @@ -43,4 +37,3 @@ winapi = { version = "0.3.9", features = [ "winerror", "ws2ipdef", ] } - diff --git a/rtc-shared/src/crypto/mod.rs b/rtc-shared/src/crypto/mod.rs deleted file mode 100644 index 78b28e03..00000000 --- a/rtc-shared/src/crypto/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -use crate::error::Result; - -/// KeyingMaterialExporter to extract keying material. -/// -/// This trait sits here to avoid getting a direct dependency between -/// the dtls and srtp crates. -pub trait KeyingMaterialExporter { - fn export_keying_material(&self, label: &str, context: &[u8], length: usize) - -> Result>; -} diff --git a/rtc-shared/src/error.rs b/rtc-shared/src/error.rs index ff035c04..6fc89ecd 100644 --- a/rtc-shared/src/error.rs +++ b/rtc-shared/src/error.rs @@ -1,3 +1,9 @@ +//! The error type shared across the stack. +//! +//! One [`Error`](crate::error::Error) enum spans every layer — buffers, UDP, ICE, DTLS, SCTP, SRTP, RTP/RTCP, SDP and +//! the data channel — so a value can propagate from the innermost codec to the application +//! without conversion at each boundary. The higher-level crates re-export it, which is why an +//! application only ever imports one error type. #![allow(dead_code)] use std::io; @@ -24,112 +30,165 @@ pub type Result = std::result::Result; #[derive(Error, Debug, PartialEq)] #[non_exhaustive] pub enum Error { + /// Buffer: full. #[error("buffer: full")] ErrBufferFull, + /// Buffer: closed. #[error("buffer: closed")] ErrBufferClosed, + /// Buffer: short. #[error("buffer: short")] ErrBufferShort, + /// Packet too big. #[error("packet too big")] ErrPacketTooBig, + /// I/O timeout. #[error("i/o timeout")] ErrTimeout, + /// UDP: listener closed. #[error("udp: listener closed")] ErrClosedListener, + /// UDP: listen queue exceeded. #[error("udp: listen queue exceeded")] ErrListenQueueExceeded, + /// UDP: listener accept ch closed. #[error("udp: listener accept ch closed")] ErrClosedListenerAcceptCh, + /// Obs cannot be nil. #[error("obs cannot be nil")] ErrObsCannotBeNil, + /// Se of closed network connection. #[error("se of closed network connection")] ErrUseClosedNetworkConn, + /// Addr is not a net.UDPAddr. #[error("addr is not a net.UDPAddr")] ErrAddrNotUdpAddr, + /// Something went wrong with locAddr. #[error("something went wrong with locAddr")] ErrLocAddr, + /// Already closed. #[error("already closed")] ErrAlreadyClosed, + /// No remAddr defined. #[error("no remAddr defined")] ErrNoRemAddr, + /// Address already in use. #[error("address already in use")] ErrAddressAlreadyInUse, + /// No such UDPConn. #[error("no such UDPConn")] ErrNoSuchUdpConn, + /// Cannot remove unspecified IP by the specified IP. #[error("cannot remove unspecified IP by the specified IP")] ErrCannotRemoveUnspecifiedIp, + /// No address assigned. #[error("no address assigned")] ErrNoAddressAssigned, + /// 1:1 NAT requires more than one mapping. #[error("1:1 NAT requires more than one mapping")] ErrNatRequriesMapping, + /// Length mismtach between mappedIPs and localIPs. #[error("length mismtach between mappedIPs and localIPs")] ErrMismatchLengthIp, + /// Non-udp translation is not supported yet. #[error("non-udp translation is not supported yet")] ErrNonUdpTranslationNotSupported, + /// No associated local address. #[error("no associated local address")] ErrNoAssociatedLocalAddress, + /// No NAT binding found. #[error("no NAT binding found")] ErrNoNatBindingFound, + /// Has no permission. #[error("has no permission")] ErrHasNoPermission, + /// Host name must not be empty. #[error("host name must not be empty")] ErrHostnameEmpty, + /// Failed to parse IP address. #[error("failed to parse IP address")] ErrFailedToParseIpaddr, + /// No interface is available. #[error("no interface is available")] ErrNoInterface, + /// Not found. #[error("not found")] ErrNotFound, + /// Unexpected network. #[error("unexpected network")] ErrUnexpectedNetwork, + /// Can't assign requested address. #[error("can't assign requested address")] ErrCantAssignRequestedAddr, + /// Unknown network. #[error("unknown network")] ErrUnknownNetwork, + /// No router linked. #[error("no router linked")] ErrNoRouterLinked, + /// Invalid port number. #[error("invalid port number")] ErrInvalidPortNumber, + /// Unexpected type-switch failure. #[error("unexpected type-switch failure")] ErrUnexpectedTypeSwitchFailure, + /// Bind failed. #[error("bind failed")] ErrBindFailed, + /// End port is less than the start. #[error("end port is less than the start")] ErrEndPortLessThanStart, + /// Port space exhausted. #[error("port space exhausted")] ErrPortSpaceExhausted, + /// Vnet is not enabled. #[error("vnet is not enabled")] ErrVnetDisabled, + /// Invalid local IP in static_ips. #[error("invalid local IP in static_ips")] ErrInvalidLocalIpInStaticIps, + /// Mapped in static_ips is beyond subnet. #[error("mapped in static_ips is beyond subnet")] ErrLocalIpBeyondStaticIpsSubset, + /// All static_ips must have associated local IPs. #[error("all static_ips must have associated local IPs")] ErrLocalIpNoStaticsIpsAssociated, + /// Router already started. #[error("router already started")] ErrRouterAlreadyStarted, + /// Router already stopped. #[error("router already stopped")] ErrRouterAlreadyStopped, + /// Static IP is beyond subnet. #[error("static IP is beyond subnet")] ErrStaticIpIsBeyondSubnet, + /// Address space exhausted. #[error("address space exhausted")] ErrAddressSpaceExhausted, + /// No IP address is assigned for eth0. #[error("no IP address is assigned for eth0")] ErrNoIpaddrEth0, + /// Invalid mask. #[error("Invalid mask")] ErrInvalidMask, //ExportKeyingMaterial errors + /// TLS handshake is in progress. #[error("tls handshake is in progress")] HandshakeInProgress, + /// Context is not supported for export_keying_material. #[error("context is not supported for export_keying_material")] ContextUnsupported, + /// Export_keying_material can not be used with a reserved label. #[error("export_keying_material can not be used with a reserved label")] ReservedExportKeyingMaterial, + /// No cipher suite for export_keying_material. #[error("no cipher suite for export_keying_material")] CipherSuiteUnset, + /// Export_keying_material hash. #[error("export_keying_material hash: {0}")] Hash(String), + /// Mutex poison. #[error("mutex poison: {0}")] PoisonError(String), @@ -217,346 +276,509 @@ pub enum Error { /// Packet status chunk is not 2 bytes. #[error("Packet status chunk must be 2 bytes")] PacketStatusChunkLength, + /// Invalid bitrate. #[error("Invalid bitrate")] InvalidBitrate, + /// Wrong chunk type. #[error("Wrong chunk type")] WrongChunkType, + /// Struct contains unexpected member type. #[error("Struct contains unexpected member type")] BadStructMemberType, + /// Cannot read into non-pointer. #[error("Cannot read into non-pointer")] BadReadParameter, + /// Invalid block size. #[error("Invalid block size")] InvalidBlockSize, //RTP errors + /// RTP header size insufficient. #[error("RTP header size insufficient")] ErrHeaderSizeInsufficient, + /// RTP header size insufficient for extension. #[error("RTP header size insufficient for extension")] ErrHeaderSizeInsufficientForExtension, + /// Buffer too small. #[error("buffer too small")] ErrBufferTooSmall, + /// Extension not enabled. #[error("extension not enabled")] ErrHeaderExtensionsNotEnabled, + /// Extension not found. #[error("extension not found")] ErrHeaderExtensionNotFound, + /// Header extension id must be between 1 and 14 for RFC 5285 extensions. #[error("header extension id must be between 1 and 14 for RFC 5285 extensions")] ErrRfc8285oneByteHeaderIdrange, + /// Header extension payload must be 16bytes or less for RFC 5285 one byte extensions. #[error("header extension payload must be 16bytes or less for RFC 5285 one byte extensions")] ErrRfc8285oneByteHeaderSize, + /// Header extension id must be between 1 and 255 for RFC 5285 extensions. #[error("header extension id must be between 1 and 255 for RFC 5285 extensions")] ErrRfc8285twoByteHeaderIdrange, + /// Header extension payload must be 255bytes or less for RFC 5285 two byte extensions. #[error("header extension payload must be 255bytes or less for RFC 5285 two byte extensions")] ErrRfc8285twoByteHeaderSize, + /// Header extension id must be 0 for none RFC 5285 extensions. #[error("header extension id must be 0 for none RFC 5285 extensions")] ErrRfc3550headerIdrange, + /// Packet is not large enough. #[error("packet is not large enough")] ErrShortPacket, + /// Invalid nil packet. #[error("invalid nil packet")] ErrNilPacket, + /// Too many PDiff. #[error("too many PDiff")] ErrTooManyPDiff, + /// Too many spatial layers. #[error("too many spatial layers")] ErrTooManySpatialLayers, + /// NALU Type is unhandled. #[error("NALU Type is unhandled")] ErrUnhandledNaluType, + /// Corrupted H265 packet. #[error("corrupted h265 packet")] ErrH265CorruptedPacket, + /// Invalid H265 packet type. #[error("invalid h265 packet type")] ErrInvalidH265PacketType, + /// Payload is too small for OBU extension header. #[error("payload is too small for OBU extension header")] ErrPayloadTooSmallForObuExtensionHeader, + /// Payload is too small for OBU payload size. #[error("payload is too small for OBU payload size")] ErrPayloadTooSmallForObuPayloadSize, + /// Extension_payload must be in 32-bit words. #[error("extension_payload must be in 32-bit words")] HeaderExtensionPayloadNot32BitWords, + /// Audio level overflow. #[error("audio level overflow")] AudioLevelOverflow, + /// Playout delay overflow. #[error("playout delay overflow")] PlayoutDelayOverflow, + /// Payload is not large enough. #[error("payload is not large enough")] PayloadIsNotLargeEnough, + /// STAP-A declared size is larger than buffer. #[error("STAP-A declared size({0}) is larger than buffer({1})")] StapASizeLargerThanBuffer(usize, usize), + /// NALU type is currently not handled. #[error("nalu type {0} is currently not handled")] NaluTypeIsNotHandled(u8), //SRTP + /// Duplicated packet. #[error("duplicated packet")] ErrDuplicated, + /// SRTP master key is not long enough. #[error("SRTP master key is not long enough")] ErrShortSrtpMasterKey, + /// SRTP master salt is not long enough. #[error("SRTP master salt is not long enough")] ErrShortSrtpMasterSalt, + /// No such SRTP Profile. #[error("no such SRTP Profile")] ErrNoSuchSrtpProfile, + /// IndexOverKdr > 0 is not supported yet. #[error("indexOverKdr > 0 is not supported yet")] ErrNonZeroKdrNotSupported, + /// Exporter called with wrong label. #[error("exporter called with wrong label")] ErrExporterWrongLabel, + /// No config provided. #[error("no config provided")] ErrNoConfig, + /// No conn provided. #[error("no conn provided")] ErrNoConn, + /// Failed to verify auth tag. #[error("failed to verify auth tag")] ErrFailedToVerifyAuthTag, + /// Packet is too short to be RTP packet. #[error("packet is too short to be RTP packet")] ErrTooShortRtp, + /// Packet is too short to be RTCP packet. #[error("packet is too short to be RTCP packet")] ErrTooShortRtcp, + /// Payload differs. #[error("payload differs")] ErrPayloadDiffers, + /// Started channel used incorrectly, should only be closed. #[error("started channel used incorrectly, should only be closed")] ErrStartedChannelUsedIncorrectly, + /// Stream has not been inited, unable to close. #[error("stream has not been inited, unable to close")] ErrStreamNotInited, + /// Stream is already closed. #[error("stream is already closed")] ErrStreamAlreadyClosed, + /// Stream is already inited. #[error("stream is already inited")] ErrStreamAlreadyInited, + /// Failed to cast child. #[error("failed to cast child")] ErrFailedTypeAssertion, + /// Exceeded the maximum number of packets. #[error("exceeded the maximum number of packets")] ErrExceededMaxPackets, + /// Index_over_kdr > 0 is not supported yet. #[error("index_over_kdr > 0 is not supported yet")] UnsupportedIndexOverKdr, + /// Invalid master key length for AES_256_cm. #[error("invalid master key length for aes_256_cm")] InvalidMasterKeyLength, + /// Invalid master salt length for AES_256_cm. #[error("invalid master salt length for aes_256_cm")] InvalidMasterSaltLength, + /// Out_len > 32 is not supported for AES_256_cm. #[error("out_len > 32 is not supported for aes_256_cm")] UnsupportedOutLength, + /// SRTP Master Key must be len , got. #[error("SRTP Master Key must be len {0}, got {1}")] SrtpMasterKeyLength(usize, usize), + /// SRTP Salt must be len , got. #[error("SRTP Salt must be len {0}, got {1}")] SrtpSaltLength(usize, usize), + /// SyntaxError. #[error("SyntaxError: {0}")] ExtMapParse(String), + /// SSRC not exist in SRTP_SSRC_state. #[error("ssrc {0} not exist in srtp_ssrc_state")] SsrcMissingFromSrtp(u32), + /// SRTP SSRC= index=: duplicated. #[error("srtp ssrc={0} index={1}: duplicated")] SrtpSsrcDuplicated(u32, u16), + /// Srtcp SSRC= index=: duplicated. #[error("srtcp ssrc={0} index={1}: duplicated")] SrtcpSsrcDuplicated(u32, usize), + /// SSRC not exist in srtcp_SSRC_state. #[error("ssrc {0} not exist in srtcp_ssrc_state")] SsrcMissingFromSrtcp(u32), + /// Stream with SSRC exists. #[error("Stream with ssrc {0} exists")] StreamWithSsrcExists(u32), + /// Session RTP/RTCP type must be same as input buffer. #[error("Session RTP/RTCP type must be same as input buffer")] SessionRtpRtcpTypeMismatch, + /// Session EOF. #[error("Session EOF")] SessionEof, + /// Too short SRTP packet: only bytes, expected > bytes. #[error("too short SRTP packet: only {0} bytes, expected > {1} bytes")] SrtpTooSmall(usize, usize), + /// Too short SRTCP packet: only bytes, expected > bytes. #[error("too short SRTCP packet: only {0} bytes, expected > {1} bytes")] SrtcpTooSmall(usize, usize), + /// Failed to verify RTP auth tag. #[error("failed to verify rtp auth tag")] RtpFailedToVerifyAuthTag, + /// Too short auth tag: only bytes, expected > bytes. #[error("too short auth tag: only {0} bytes, expected > {1} bytes")] RtcpInvalidLengthAuthTag(usize, usize), + /// Failed to verify RTCP auth tag. #[error("failed to verify rtcp auth tag")] RtcpFailedToVerifyAuthTag, + /// SessionSRTP has been closed. #[error("SessionSRTP has been closed")] SessionSrtpAlreadyClosed, + /// This stream is not a RTPStream. #[error("this stream is not a RTPStream")] InvalidRtpStream, + /// This stream is not a RTCPStream. #[error("this stream is not a RTCPStream")] InvalidRtcpStream, //STUN errors + /// Attribute not found. #[error("attribute not found")] ErrAttributeNotFound, + /// Transaction is stopped. #[error("transaction is stopped")] ErrTransactionStopped, + /// Transaction not exists. #[error("transaction not exists")] ErrTransactionNotExists, + /// Transaction exists with same id. #[error("transaction exists with same id")] ErrTransactionExists, + /// Agent is closed. #[error("agent is closed")] ErrAgentClosed, + /// Transaction is timed out. #[error("transaction is timed out")] ErrTransactionTimeOut, + /// No default reason for ErrorCode. #[error("no default reason for ErrorCode")] ErrNoDefaultReason, + /// Unexpected EOF. #[error("unexpected EOF")] ErrUnexpectedEof, + /// Attribute size is invalid. #[error("attribute size is invalid")] ErrAttributeSizeInvalid, + /// Attribute size overflow. #[error("attribute size overflow")] ErrAttributeSizeOverflow, + /// Attempt to decode to nil message. #[error("attempt to decode to nil message")] ErrDecodeToNil, + /// Unexpected EOF: not enough bytes to read header. #[error("unexpected EOF: not enough bytes to read header")] ErrUnexpectedHeaderEof, + /// Integrity check failed. #[error("integrity check failed")] ErrIntegrityMismatch, + /// Fingerprint check failed. #[error("fingerprint check failed")] ErrFingerprintMismatch, + /// FINGERPRINT before MESSAGE-INTEGRITY attribute. #[error("FINGERPRINT before MESSAGE-INTEGRITY attribute")] ErrFingerprintBeforeIntegrity, + /// Bad UNKNOWN-ATTRIBUTES size. #[error("bad UNKNOWN-ATTRIBUTES size")] ErrBadUnknownAttrsSize, + /// Invalid length of IP value. #[error("invalid length of IP value")] ErrBadIpLength, + /// No connection provided. #[error("no connection provided")] ErrNoConnection, + /// Client is closed. #[error("client is closed")] ErrClientClosed, + /// No agent is set. #[error("no agent is set")] ErrNoAgent, + /// Collector is closed. #[error("collector is closed")] ErrCollectorClosed, + /// Unsupported network. #[error("unsupported network")] ErrUnsupportedNetwork, + /// Invalid URL. #[error("invalid url")] ErrInvalidUrl, + /// Unknown scheme type. #[error("unknown scheme type")] ErrSchemeType, + /// Invalid hostname. #[error("invalid hostname")] ErrHost, // TURN errors + /// TURN: RelayAddress must be valid IP to use RelayAddressGeneratorStatic. #[error("turn: RelayAddress must be valid IP to use RelayAddressGeneratorStatic")] ErrRelayAddressInvalid, + /// TURN: PacketConnConfigs and ConnConfigs are empty, unable to proceed. #[error("turn: PacketConnConfigs and ConnConfigs are empty, unable to proceed")] ErrNoAvailableConns, + /// TURN: PacketConnConfig must have a non-nil Conn. #[error("turn: PacketConnConfig must have a non-nil Conn")] ErrConnUnset, + /// TURN: ListenerConfig must have a non-nil Listener. #[error("turn: ListenerConfig must have a non-nil Listener")] ErrListenerUnset, + /// TURN: RelayAddressGenerator has invalid ListeningAddress. #[error("turn: RelayAddressGenerator has invalid ListeningAddress")] ErrListeningAddressInvalid, + /// TURN: RelayAddressGenerator in RelayConfig is unset. #[error("turn: RelayAddressGenerator in RelayConfig is unset")] ErrRelayAddressGeneratorUnset, + /// TURN: max retries exceeded. #[error("turn: max retries exceeded")] ErrMaxRetriesExceeded, + /// TURN: MaxPort must be not 0. #[error("turn: MaxPort must be not 0")] ErrMaxPortNotZero, + /// TURN: MaxPort must be not 0. #[error("turn: MaxPort must be not 0")] ErrMinPortNotZero, + /// TURN: MaxPort less than MinPort. #[error("turn: MaxPort less than MinPort")] ErrMaxPortLessThanMinPort, + /// TURN: relay_conn cannot not be nil. #[error("turn: relay_conn cannot not be nil")] ErrNilConn, + /// TURN: TODO. #[error("turn: TODO")] ErrTodo, + /// TURN: already listening. #[error("turn: already listening")] ErrAlreadyListening, + /// TURN: Server failed to close. #[error("turn: Server failed to close")] ErrFailedToClose, + /// TURN: failed to retransmit transaction. #[error("turn: failed to retransmit transaction")] ErrFailedToRetransmitTransaction, + /// All retransmissions failed. #[error("all retransmissions failed")] ErrAllRetransmissionsFailed, + /// No binding found for channel. #[error("no binding found for channel")] ErrChannelBindNotFound, + /// STUN server address is not set for the client. #[error("STUN server address is not set for the client")] ErrStunserverAddressNotSet, + /// Only one Allocate caller is allowed. #[error("only one Allocate() caller is allowed")] ErrOneAllocateOnly, + /// Already allocated. #[error("already allocated")] ErrAlreadyAllocated, + /// Non-STUN message from STUN server. #[error("non-STUN message from STUN server")] ErrNonStunmessage, + /// Failed to decode STUN message. #[error("failed to decode STUN message")] ErrFailedToDecodeStun, + /// Unexpected STUN request message. #[error("unexpected STUN request message")] ErrUnexpectedStunrequestMessage, + /// Channel number not in [0x4000, 0x7FFF]. #[error("channel number not in [0x4000, 0x7FFF]")] ErrInvalidChannelNumber, + /// ChannelData length != len(Data). #[error("channelData length != len(Data)")] ErrBadChannelDataLength, + /// Invalid value for requested family attribute. #[error("invalid value for requested family attribute")] ErrInvalidRequestedFamilyValue, + /// Fake error. #[error("fake error")] ErrFakeErr, + /// Use of closed network connection. #[error("use of closed network connection")] ErrClosed, + /// Addr is not a net.UDPAddr. #[error("addr is not a net.UDPAddr")] ErrUdpaddrCast, + /// Try-lock is already locked. #[error("try-lock is already locked")] ErrDoubleLock, + /// Transaction closed. #[error("transaction closed")] ErrTransactionClosed, + /// Wait_for_result called on non-result transaction. #[error("wait_for_result called on non-result transaction")] ErrWaitForResultOnNonResultTransaction, + /// Failed to build refresh request. #[error("failed to build refresh request")] ErrFailedToBuildRefreshRequest, + /// Failed to refresh allocation. #[error("failed to refresh allocation")] ErrFailedToRefreshAllocation, + /// Failed to get lifetime from refresh response. #[error("failed to get lifetime from refresh response")] ErrFailedToGetLifetime, + /// Too short buffer. #[error("too short buffer")] ErrShortBuffer, + /// Unexpected response type. #[error("unexpected response type")] ErrUnexpectedResponse, + /// AllocatePacketConn must be set. #[error("AllocatePacketConn must be set")] ErrAllocatePacketConnMustBeSet, + /// AllocateConn must be set. #[error("AllocateConn must be set")] ErrAllocateConnMustBeSet, + /// LeveledLogger must be set. #[error("LeveledLogger must be set")] ErrLeveledLoggerMustBeSet, + /// You cannot use the same channel number with different peer. #[error("you cannot use the same channel number with different peer")] ErrSameChannelDifferentPeer, + /// Allocations must not be created with nil FivTuple. #[error("allocations must not be created with nil FivTuple")] ErrNilFiveTuple, + /// Allocations must not be created with nil FiveTuple.src_addr. #[error("allocations must not be created with nil FiveTuple.src_addr")] ErrNilFiveTupleSrcAddr, + /// Allocations must not be created with nil FiveTuple.dst_addr. #[error("allocations must not be created with nil FiveTuple.dst_addr")] ErrNilFiveTupleDstAddr, + /// Allocations must not be created with nil turnSocket. #[error("allocations must not be created with nil turnSocket")] ErrNilTurnSocket, + /// Allocations must not be created with a lifetime of 0. #[error("allocations must not be created with a lifetime of 0")] ErrLifetimeZero, + /// Allocation attempt created with duplicate FiveTuple. #[error("allocation attempt created with duplicate FiveTuple")] ErrDupeFiveTuple, + /// Failed to cast net.Addr to *net.UDPAddr. #[error("failed to cast net.Addr to *net.UDPAddr")] ErrFailedToCastUdpaddr, + /// Failed to generate nonce. #[error("failed to generate nonce")] ErrFailedToGenerateNonce, + /// Failed to send error message. #[error("failed to send error message")] ErrFailedToSendError, + /// Duplicated Nonce generated, discarding request. #[error("duplicated Nonce generated, discarding request")] ErrDuplicatedNonce, + /// No such user exists. #[error("no such user exists")] ErrNoSuchUser, + /// Unexpected class. #[error("unexpected class")] ErrUnexpectedClass, + /// Unexpected method. #[error("unexpected method")] ErrUnexpectedMethod, + /// Failed to handle. #[error("failed to handle")] ErrFailedToHandle, + /// Unhandled STUN packet. #[error("unhandled STUN packet")] ErrUnhandledStunpacket, + /// Unable to handle ChannelData. #[error("unable to handle ChannelData")] ErrUnableToHandleChannelData, + /// Failed to create STUN message from packet. #[error("failed to create stun message from packet")] ErrFailedToCreateStunpacket, + /// Failed to create channel data from packet. #[error("failed to create channel data from packet")] ErrFailedToCreateChannelData, + /// Relay already allocated for 5-TUPLE. #[error("relay already allocated for 5-TUPLE")] ErrRelayAlreadyAllocatedForFiveTuple, + /// RequestedTransport must be UDP. #[error("RequestedTransport must be UDP")] ErrRequestedTransportMustBeUdp, + /// No support for DONT-FRAGMENT. #[error("no support for DONT-FRAGMENT")] ErrNoDontFragmentSupport, + /// Request must not contain RESERVATION-TOKEN and EVEN-PORT. #[error("Request must not contain RESERVATION-TOKEN and EVEN-PORT")] ErrRequestWithReservationTokenAndEvenPort, + /// No allocation found. #[error("no allocation found")] ErrNoAllocationFound, + /// Unable to handle send-indication, no permission added. #[error("unable to handle send-indication, no permission added")] ErrNoPermission, + /// Packet write smaller than packet. #[error("packet write smaller than packet")] ErrShortWrite, + /// No such channel bind. #[error("no such channel bind")] ErrNoSuchChannelBind, + /// Failed writing to socket. #[error("failed writing to socket")] ErrFailedWriteSocket, @@ -694,186 +916,279 @@ pub enum Error { #[error("conn with same remote addr already exists")] ErrTcpRemoteAddrAlreadyExists, + /// Failed to send packet. #[error("failed to send packet")] ErrSendPacket, + /// Attribute not long enough to be ICE candidate. #[error("attribute not long enough to be ICE candidate")] ErrAttributeTooShortIceCandidate, + /// Could not parse component. #[error("could not parse component")] ErrParseComponent, + /// Could not parse priority. #[error("could not parse priority")] ErrParsePriority, + /// Could not parse port. #[error("could not parse port")] ErrParsePort, + /// Could not parse related addresses. #[error("could not parse related addresses")] ErrParseRelatedAddr, + /// Could not parse type. #[error("could not parse type")] ErrParseType, + /// Unknown candidate type. #[error("unknown candidate type")] ErrUnknownCandidateType, + /// Failed to get XOR-MAPPED-ADDRESS response. #[error("failed to get XOR-MAPPED-ADDRESS response")] ErrGetXorMappedAddrResponse, + /// Connection with same remote address already exists. #[error("connection with same remote address already exists")] ErrConnectionAddrAlreadyExist, + /// Error reading streaming packet. #[error("error reading streaming packet")] ErrReadingStreamingPacket, + /// Error writing to. #[error("error writing to")] ErrWriting, + /// Error closing connection. #[error("error closing connection")] ErrClosingConnection, + /// Unable to determine networkType. #[error("unable to determine networkType")] ErrDetermineNetworkType, + /// Missing protocol scheme. #[error("missing protocol scheme")] ErrMissingProtocolScheme, + /// Too many colons in address. #[error("too many colons in address")] ErrTooManyColonsAddr, + /// Unexpected error trying to read. #[error("unexpected error trying to read")] ErrRead, + /// Unknown role. #[error("unknown role")] ErrUnknownRole, + /// Username mismatch. #[error("username mismatch")] ErrMismatchUsername, + /// The ICE conn can't write STUN messages. #[error("the ICE conn can't write STUN messages")] ErrIceWriteStunMessage, + /// URL parse: relative URL without a base. #[error("url parse: relative URL without a base")] ErrUrlParse, + /// Candidate IP could not be found. #[error("Candidate IP could not be found")] ErrCandidateIpNotFound, // DTLS errors + /// Conn is closed. #[error("conn is closed")] ErrConnClosed, + /// Read/write timeout. #[error("read/write timeout")] ErrDeadlineExceeded, + /// Context is not supported for export_keying_material. #[error("context is not supported for export_keying_material")] ErrContextUnsupported, + /// Packet is too short. #[error("packet is too short")] ErrDtlspacketInvalidLength, + /// Handshake is in progress. #[error("handshake is in progress")] ErrHandshakeInProgress, + /// Invalid content type. #[error("invalid content type")] ErrInvalidContentType, + /// Invalid mac. #[error("invalid mac")] ErrInvalidMac, + /// Packet length and declared length do not match. #[error("packet length and declared length do not match")] ErrInvalidPacketLength, + /// Export_keying_material can not be used with a reserved label. #[error("export_keying_material can not be used with a reserved label")] ErrReservedExportKeyingMaterial, + /// Client sent certificate verify but we have no certificate to verify. #[error("client sent certificate verify but we have no certificate to verify")] ErrCertificateVerifyNoCertificate, + /// Client+server do not support any shared cipher suites. #[error("client+server do not support any shared cipher suites")] ErrCipherSuiteNoIntersection, + /// Server hello can not be created without a cipher suite. #[error("server hello can not be created without a cipher suite")] ErrCipherSuiteUnset, + /// Client sent certificate but did not verify it. #[error("client sent certificate but did not verify it")] ErrClientCertificateNotVerified, + /// Server required client verification, but got none. #[error("server required client verification, but got none")] ErrClientCertificateRequired, + /// Server responded with SRTP Profile we do not support. #[error("server responded with SRTP Profile we do not support")] ErrClientNoMatchingSrtpProfile, + /// Client required Extended Master Secret extension, but server does not support it. #[error("client required Extended Master Secret extension, but server does not support it")] ErrClientRequiredButNoServerEms, + /// Server hello can not be created without a compression method. #[error("server hello can not be created without a compression method")] ErrCompressionMethodUnset, + /// Client+server cookie does not match. #[error("client+server cookie does not match")] ErrCookieMismatch, + /// Cookie must not be longer then 255 bytes. #[error("cookie must not be longer then 255 bytes")] ErrCookieTooLong, + /// PSK Identity Hint provided but PSK is nil. #[error("PSK Identity Hint provided but PSK is nil")] ErrIdentityNoPsk, + /// No certificate provided. #[error("no certificate provided")] ErrInvalidCertificate, + /// Cipher spec invalid. #[error("cipher spec invalid")] ErrInvalidCipherSpec, + /// Invalid or unknown cipher suite. #[error("invalid or unknown cipher suite")] ErrInvalidCipherSuite, + /// Unable to determine if ClientKeyExchange is a public key or PSK Identity. #[error("unable to determine if ClientKeyExchange is a public key or PSK Identity")] ErrInvalidClientKeyExchange, + /// Invalid or unknown compression method. #[error("invalid or unknown compression method")] ErrInvalidCompressionMethod, + /// ECDSA signature contained zero or negative values. #[error("ECDSA signature contained zero or negative values")] ErrInvalidEcdsasignature, + /// Invalid or unknown elliptic curve type. #[error("invalid or unknown elliptic curve type")] ErrInvalidEllipticCurveType, + /// Invalid extension type. #[error("invalid extension type")] ErrInvalidExtensionType, + /// Invalid hash algorithm. #[error("invalid hash algorithm")] ErrInvalidHashAlgorithm, + /// Invalid named curve. #[error("invalid named curve")] ErrInvalidNamedCurve, + /// Invalid private key type. #[error("invalid private key type")] ErrInvalidPrivateKey, + /// Named curve and private key type does not match. #[error("named curve and private key type does not match")] ErrNamedCurveAndPrivateKeyMismatch, + /// Invalid server name format. #[error("invalid server name format")] ErrInvalidSniFormat, + /// Invalid signature algorithm. #[error("invalid signature algorithm")] ErrInvalidSignatureAlgorithm, + /// Expected and actual key signature do not match. #[error("expected and actual key signature do not match")] ErrKeySignatureMismatch, + /// Conn can not be created with a nil nextConn. #[error("Conn can not be created with a nil nextConn")] ErrNilNextConn, + /// Connection can not be created, no CipherSuites satisfy this Config. #[error("connection can not be created, no CipherSuites satisfy this Config")] ErrNoAvailableCipherSuites, + /// Connection can not be created, no SignatureScheme satisfy this Config. #[error("connection can not be created, no SignatureScheme satisfy this Config")] ErrNoAvailableSignatureSchemes, + /// No certificates configured. #[error("no certificates configured")] ErrNoCertificates, + /// No config provided. #[error("no config provided")] ErrNoConfigProvided, + /// Client requested zero or more elliptic curves that are not supported by the server. #[error("client requested zero or more elliptic curves that are not supported by the server")] ErrNoSupportedEllipticCurves, + /// Unsupported protocol version. #[error("unsupported protocol version")] ErrUnsupportedProtocolVersion, + /// Certificate and PSK provided. #[error("Certificate and PSK provided")] ErrPskAndCertificate, + /// PSK and PSK Identity Hint must both be set for client. #[error("PSK and PSK Identity Hint must both be set for client")] ErrPskAndIdentityMustBeSetForClient, + /// SRTP support was requested but server did not respond with use_SRTP extension. #[error("SRTP support was requested but server did not respond with use_srtp extension")] ErrRequestedButNoSrtpExtension, + /// Certificate is mandatory for server. #[error("Certificate is mandatory for server")] ErrServerMustHaveCertificate, + /// Client requested SRTP but we have no matching profiles. #[error("client requested SRTP but we have no matching profiles")] ErrServerNoMatchingSrtpProfile, + /// Server requires the Extended Master Secret extension, but the client does not support it. #[error( "server requires the Extended Master Secret extension, but the client does not support it" )] ErrServerRequiredButNoClientEms, + /// Expected and actual verify data does not match. #[error("expected and actual verify data does not match")] ErrVerifyDataMismatch, + /// Handshake message unset, unable to marshal. #[error("handshake message unset, unable to marshal")] ErrHandshakeMessageUnset, + /// Invalid flight number. #[error("invalid flight number")] ErrInvalidFlight, + /// Unable to generate key signature, unimplemented. #[error("unable to generate key signature, unimplemented")] ErrKeySignatureGenerateUnimplemented, + /// Unable to verify key signature, unimplemented. #[error("unable to verify key signature, unimplemented")] ErrKeySignatureVerifyUnimplemented, + /// Data length and declared length do not match. #[error("data length and declared length do not match")] ErrLengthMismatch, + /// Buffer not long enough to contain nonce. #[error("buffer not long enough to contain nonce")] ErrNotEnoughRoomForNonce, + /// Feature has not been implemented yet. #[error("feature has not been implemented yet")] ErrNotImplemented, + /// Sequence number overflow. #[error("sequence number overflow")] ErrSequenceNumberOverflow, + /// Unable to marshal fragmented handshakes. #[error("unable to marshal fragmented handshakes")] ErrUnableToMarshalFragmented, + /// Invalid state machine transition. #[error("invalid state machine transition")] ErrInvalidFsmTransition, + /// ApplicationData with epoch of 0. #[error("ApplicationData with epoch of 0")] ErrApplicationDataEpochZero, + /// Unhandled contentType. #[error("unhandled contentType")] ErrUnhandledContextType, + /// Context canceled. #[error("context canceled")] ErrContextCanceled, + /// Empty fragment. #[error("empty fragment")] ErrEmptyFragment, + /// Alert is Fatal or Close Notify. #[error("Alert is Fatal or Close Notify")] ErrAlertFatalOrClose, + /// Fragment buffer overflow. New size is greater than specified max. #[error( "Fragment buffer overflow. New size {new_size} is greater than specified max {max_size}" )] - ErrFragmentBufferOverflow { new_size: usize, max_size: usize }, + ErrFragmentBufferOverflow { + /// The size the buffer would have grown to. + new_size: usize, + /// The configured maximum. + max_size: usize, + }, + /// Client transport is not set yet. #[error("Client transport is not set yet")] ErrClientTransportNotSet, @@ -903,336 +1218,496 @@ pub enum Error { NoServerConfig, //SCTP errors + /// Raw is too small for a SCTP chunk. #[error("raw is too small for a SCTP chunk")] ErrChunkHeaderTooSmall, + /// Not enough data left in SCTP packet to satisfy requested length. #[error("not enough data left in SCTP packet to satisfy requested length")] ErrChunkHeaderNotEnoughSpace, + /// Chunk PADDING is non-zero at offset. #[error("chunk PADDING is non-zero at offset")] ErrChunkHeaderPaddingNonZero, + /// Chunk has invalid length. #[error("chunk has invalid length")] ErrChunkHeaderInvalidLength, + /// ChunkType is not of type ABORT. #[error("ChunkType is not of type ABORT")] ErrChunkTypeNotAbort, + /// Failed build Abort Chunk. #[error("failed build Abort Chunk")] ErrBuildAbortChunkFailed, + /// ChunkType is not of type COOKIEACK. #[error("ChunkType is not of type COOKIEACK")] ErrChunkTypeNotCookieAck, + /// ChunkType is not of type COOKIEECHO. #[error("ChunkType is not of type COOKIEECHO")] ErrChunkTypeNotCookieEcho, + /// ChunkType is not of type ctError. #[error("ChunkType is not of type ctError")] ErrChunkTypeNotCt, + /// Failed build Error Chunk. #[error("failed build Error Chunk")] ErrBuildErrorChunkFailed, + /// Failed to marshal stream. #[error("failed to marshal stream")] ErrMarshalStreamFailed, + /// Chunk too short. #[error("chunk too short")] ErrChunkTooShort, + /// ChunkType is not of type ForwardTsn. #[error("ChunkType is not of type ForwardTsn")] ErrChunkTypeNotForwardTsn, + /// ChunkType is not of type HEARTBEAT. #[error("ChunkType is not of type HEARTBEAT")] ErrChunkTypeNotHeartbeat, + /// ChunkType is not of type HEARTBEATACK. #[error("ChunkType is not of type HEARTBEATACK")] ErrChunkTypeNotHeartbeatAck, + /// Heartbeat is not long enough to contain Heartbeat Info. #[error("heartbeat is not long enough to contain Heartbeat Info")] ErrHeartbeatNotLongEnoughInfo, + /// Failed to parse param type. #[error("failed to parse param type")] ErrParseParamTypeFailed, + /// Heartbeat should only have HEARTBEAT param. #[error("heartbeat should only have HEARTBEAT param")] ErrHeartbeatParam, + /// Failed unmarshalling param in Heartbeat Chunk. #[error("failed unmarshalling param in Heartbeat Chunk")] ErrHeartbeatChunkUnmarshal, + /// Unimplemented. #[error("unimplemented")] ErrUnimplemented, + /// Heartbeat Ack must have one param. #[error("heartbeat Ack must have one param")] ErrHeartbeatAckParams, + /// Heartbeat Ack must have one param, and it should be a HeartbeatInfo. #[error("heartbeat Ack must have one param, and it should be a HeartbeatInfo")] ErrHeartbeatAckNotHeartbeatInfo, + /// Unable to marshal parameter for Heartbeat Ack. #[error("unable to marshal parameter for Heartbeat Ack")] ErrHeartbeatAckMarshalParam, + /// Raw is too small for error cause. #[error("raw is too small for error cause")] ErrErrorCauseTooSmall, + /// Unhandled ParamType. #[error("unhandled ParamType: {typ}")] - ErrParamTypeUnhandled { typ: u16 }, + ErrParamTypeUnhandled { + /// The raw parameter type that was not recognised. + typ: u16, + }, + /// Unexpected ParamType. #[error("unexpected ParamType")] ErrParamTypeUnexpected, + /// Param header too short. #[error("param header too short")] ErrParamHeaderTooShort, + /// Param self reported length is shorter than header length. #[error("param self reported length is shorter than header length")] ErrParamHeaderSelfReportedLengthShorter, + /// Param self reported length is longer than header length. #[error("param self reported length is longer than header length")] ErrParamHeaderSelfReportedLengthLonger, + /// Failed to parse param type. #[error("failed to parse param type")] ErrParamHeaderParseFailed, + /// Packet to short. #[error("packet to short")] ErrParamPacketTooShort, + /// Outgoing SSN reset request parameter too short. #[error("outgoing SSN reset request parameter too short")] ErrSsnResetRequestParamTooShort, + /// Failed unmarshalling SSN reset request parameter in RE-CONFIG chunk. #[error("failed unmarshalling SSN reset request parameter in RE-CONFIG chunk")] ErrUnmarshalSsnResetRequestParam, + /// Reconfig response parameter too short. #[error("reconfig response parameter too short")] ErrReconfigRespParamTooShort, + /// Failed unmarshalling re-configuration response parameter in RE-CONFIG chunk. #[error("failed unmarshalling re-configuration response parameter in RE-CONFIG chunk")] ErrUnmarshalReconfigRespParam, + /// Invalid algorithm type. #[error("invalid algorithm type")] ErrInvalidAlgorithmType, + /// Failed to parse param type. #[error("failed to parse param type")] ErrInitChunkParseParamTypeFailed, + /// Failed unmarshalling param in Init Chunk. #[error("failed unmarshalling param in Init Chunk")] ErrInitChunkUnmarshalParam, + /// Unable to marshal parameter for INIT/INITACK. #[error("unable to marshal parameter for INIT/INITACK")] ErrInitAckMarshalParam, + /// ChunkType is not of type INIT. #[error("ChunkType is not of type INIT")] ErrChunkTypeNotTypeInit, + /// Chunk Value isn't long enough for mandatory parameters exp. #[error("chunk Value isn't long enough for mandatory parameters exp")] ErrChunkValueNotLongEnough, + /// ChunkType of type INIT flags must be all 0. #[error("ChunkType of type INIT flags must be all 0")] ErrChunkTypeInitFlagZero, + /// Failed to unmarshal INIT body. #[error("failed to unmarshal INIT body")] ErrChunkTypeInitUnmarshalFailed, + /// Failed marshaling INIT common data. #[error("failed marshaling INIT common data")] ErrChunkTypeInitMarshalFailed, + /// ChunkType of type INIT ACK InitiateTag must not be 0. #[error("ChunkType of type INIT ACK InitiateTag must not be 0")] ErrChunkTypeInitInitiateTagZero, + /// INIT ACK inbound stream request must be > 0. #[error("INIT ACK inbound stream request must be > 0")] ErrInitInboundStreamRequestZero, + /// INIT ACK outbound stream request must be > 0. #[error("INIT ACK outbound stream request must be > 0")] ErrInitOutboundStreamRequestZero, + /// INIT ACK Advertised Receiver Window Credit (a_rwnd) must be >= 1500. #[error("INIT ACK Advertised Receiver Window Credit (a_rwnd) must be >= 1500")] ErrInitAdvertisedReceiver1500, + /// Packet is smaller than the header size. #[error("packet is smaller than the header size")] ErrChunkPayloadSmall, + /// ChunkType is not of type PayloadData. #[error("ChunkType is not of type PayloadData")] ErrChunkTypeNotPayloadData, + /// Failed unmarshalling payload data chunk. #[error("failed unmarshalling payload data chunk")] ErrChunkUnmarshalPayloadData, + /// ChunkType is not of type Reconfig. #[error("ChunkType is not of type Reconfig")] ErrChunkTypeNotReconfig, + /// ChunkReconfig has invalid ParamA. #[error("ChunkReconfig has invalid ParamA")] ErrChunkReconfigInvalidParamA, + /// Failed to parse param type. #[error("failed to parse param type")] ErrChunkParseParamTypeFailed, + /// Unable to marshal parameter A for reconfig. #[error("unable to marshal parameter A for reconfig")] ErrChunkMarshalParamAReconfigFailed, + /// Unable to marshal parameter B for reconfig. #[error("unable to marshal parameter B for reconfig")] ErrChunkMarshalParamBReconfigFailed, + /// ChunkType is not of type SACK. #[error("ChunkType is not of type SACK")] ErrChunkTypeNotSack, + /// SACK Chunk size is not large enough to contain header. #[error("SACK Chunk size is not large enough to contain header")] ErrSackSizeNotLargeEnoughInfo, + /// Failed unmarshalling SACK chunk. #[error("failed unmarshalling SACK chunk")] ErrChunkUnmarshalSack, + /// Invalid chunk size. #[error("invalid chunk size")] ErrInvalidChunkSize, + /// ChunkType is not of type SHUTDOWN. #[error("ChunkType is not of type SHUTDOWN")] ErrChunkTypeNotShutdown, + /// Failed unmarshalling shutdown chunk. #[error("failed unmarshalling shutdown chunk")] ErrChunkUnmarshalShutdown, + /// ChunkType is not of type SHUTDOWN-ACK. #[error("ChunkType is not of type SHUTDOWN-ACK")] ErrChunkTypeNotShutdownAck, + /// ChunkType is not of type SHUTDOWN-COMPLETE. #[error("ChunkType is not of type SHUTDOWN-COMPLETE")] ErrChunkTypeNotShutdownComplete, + /// Raw is smaller than the minimum length for a SCTP packet. #[error("raw is smaller than the minimum length for a SCTP packet")] ErrPacketRawTooSmall, + /// Unable to parse SCTP chunk, not enough data for complete header. #[error("unable to parse SCTP chunk, not enough data for complete header")] ErrParseSctpChunkNotEnoughData, + /// Failed to unmarshal, contains unknown chunk type. #[error("failed to unmarshal, contains unknown chunk type")] ErrUnmarshalUnknownChunkType, + /// Checksum mismatch theirs. #[error("checksum mismatch theirs")] ErrChecksumMismatch, + /// Unexpected chunk popped (unordered). #[error("unexpected chunk popped (unordered)")] ErrUnexpectedChuckPoppedUnordered, + /// Unexpected chunk popped (ordered). #[error("unexpected chunk popped (ordered)")] ErrUnexpectedChuckPoppedOrdered, + /// Unexpected q state (should've been selected). #[error("unexpected q state (should've been selected)")] ErrUnexpectedQState, + /// Try again. #[error("try again")] ErrTryAgain, + /// Abort chunk, with following errors. #[error("abort chunk, with following errors: {0}")] ErrAbortChunk(String), + /// Shutdown called in non-Established state. #[error("shutdown called in non-Established state")] ErrShutdownNonEstablished, + /// Association closed before connecting. #[error("association closed before connecting")] ErrAssociationClosedBeforeConn, + /// Association init failed. #[error("association init failed")] ErrAssociationInitFailed, + /// Association handshake closed. #[error("association handshake closed")] ErrAssociationHandshakeClosed, + /// Silently discard. #[error("silently discard")] ErrSilentlyDiscard, + /// The init not stored to send. #[error("the init not stored to send")] ErrInitNotStoredToSend, + /// CookieEcho not stored to send. #[error("cookieEcho not stored to send")] ErrCookieEchoNotStoredToSend, + /// SCTP packet must not have a source port of 0. #[error("sctp packet must not have a source port of 0")] ErrSctpPacketSourcePortZero, + /// SCTP packet must not have a destination port of 0. #[error("sctp packet must not have a destination port of 0")] ErrSctpPacketDestinationPortZero, + /// Init chunk must not be bundled with any other chunk. #[error("init chunk must not be bundled with any other chunk")] ErrInitChunkBundled, + /// Init chunk expects a verification tag of 0 on the packet when out-of-the-blue. #[error("init chunk expects a verification tag of 0 on the packet when out-of-the-blue")] ErrInitChunkVerifyTagNotZero, + /// Todo: handle Init when in state. #[error("todo: handle Init when in state")] ErrHandleInitState, + /// No cookie in InitAck. #[error("no cookie in InitAck")] ErrInitAckNoCookie, + /// There already exists a stream with identifier. #[error("there already exists a stream with identifier")] ErrStreamAlreadyExist, + /// Failed to create a stream with identifier. #[error("Failed to create a stream with identifier")] ErrStreamCreateFailed, + /// Unable to be popped from inflight queue TSN. #[error("unable to be popped from inflight queue TSN")] ErrInflightQueueTsnPop, + /// Requested non-existent TSN. #[error("requested non-existent TSN")] ErrTsnRequestNotExist, + /// Sending reset packet in non-Established state. #[error("sending reset packet in non-Established state")] ErrResetPacketInStateNotExist, + /// Unexpected parameter type. #[error("unexpected parameter type")] ErrParameterType, + /// Sending payload data in non-Established state. #[error("sending payload data in non-Established state")] ErrPayloadDataStateNotExist, + /// Unhandled chunk type. #[error("unhandled chunk type")] ErrChunkTypeUnhandled, + /// Handshake failed (INIT ACK). #[error("handshake failed (INIT ACK)")] ErrHandshakeInitAck, + /// Handshake failed (COOKIE ECHO). #[error("handshake failed (COOKIE ECHO)")] ErrHandshakeCookieEcho, + /// Outbound packet larger than maximum message size. #[error("outbound packet larger than maximum message size")] ErrOutboundPacketTooLarge, + /// Stream closed. #[error("Stream closed")] ErrStreamClosed, + /// Stream not existed. #[error("Stream not existed")] ErrStreamNotExisted, + /// Association not existed. #[error("Association not existed")] ErrAssociationNotExisted, + /// Transport not existed. #[error("Transport not existed")] ErrTransportNoExisted, + /// Io EOF. #[error("Io EOF")] ErrEof, + /// Invalid SystemTime. #[error("Invalid SystemTime")] ErrInvalidSystemTime, + /// Net Conn read error. #[error("Net Conn read error")] ErrNetConnRead, + /// Max Data Channel ID. #[error("Max Data Channel ID")] ErrMaxDataChannelID, //Data Channel + /// DataChannel message is not long enough to determine type: (expected: , actual: ). #[error( "DataChannel message is not long enough to determine type: (expected: {expected}, actual: {actual})" )] - UnexpectedEndOfBuffer { expected: usize, actual: usize }, + UnexpectedEndOfBuffer { + /// The number of bytes the parser required. + expected: usize, + /// The number of bytes actually available. + actual: usize, + }, + /// Unknown MessageType. #[error("Unknown MessageType {0}")] InvalidMessageType(u8), + /// Unknown ChannelType. #[error("Unknown ChannelType {0}")] InvalidChannelType(u8), + /// Unknown PayloadProtocolIdentifier. #[error("Unknown PayloadProtocolIdentifier {0}")] InvalidPayloadProtocolIdentifier(u8), + /// Unknow Protocol. #[error("Unknow Protocol")] UnknownProtocol, //Media + /// Stream is nil. #[error("stream is nil")] ErrNilStream, + /// Incomplete frame header. #[error("incomplete frame header")] ErrIncompleteFrameHeader, + /// Incomplete frame data. #[error("incomplete frame data")] ErrIncompleteFrameData, + /// Incomplete file header. #[error("incomplete file header")] ErrIncompleteFileHeader, + /// IVF signature mismatch. #[error("IVF signature mismatch")] ErrSignatureMismatch, + /// IVF version unknown, parser may not parse correctly. #[error("IVF version unknown, parser may not parse correctly")] ErrUnknownIVFVersion, + /// File not opened. #[error("file not opened")] ErrFileNotOpened, + /// Invalid nil packet. #[error("invalid nil packet")] ErrInvalidNilPacket, + /// Bad header signature. #[error("bad header signature")] ErrBadIDPageSignature, + /// Wrong header, expected beginning of stream. #[error("wrong header, expected beginning of stream")] ErrBadIDPageType, + /// Payload for id page must be 19 bytes. #[error("payload for id page must be 19 bytes")] ErrBadIDPageLength, + /// Bad payload signature. #[error("bad payload signature")] ErrBadIDPagePayloadSignature, + /// Not enough data for payload header. #[error("not enough data for payload header")] ErrShortPageHeader, + /// Bad OpusTags signature. #[error("bad OpusTags signature")] ErrBadOpusTagsSignature, + /// Unsupported channel mapping family. #[error("unsupported channel mapping family")] ErrUnsupportedChannelMappingFamily, + /// Data is not a H264 bitstream. #[error("data is not a H264 bitstream")] ErrDataIsNotH264Stream, + /// Data is not a H265 bitstream. #[error("data is not a H265 bitstream")] ErrDataIsNotH265Stream, + /// Io EOF. #[error("Io EOF")] ErrIoEOF, // mDNS + /// MDNS: port not support, only 5353 is supported. #[error("mDNS: port not support, only 5353 is supported")] ErrMDNSPortNotSupported, + /// MDNS: connection is closed. #[error("mDNS: connection is closed")] ErrMDNSConnectionClosed, + /// MDNS: query not found. #[error("mDNS: query not found")] ErrMDNSQueryNotFound, + /// MDNS: parsing/packing of this type isn't available yet. #[error("mDNS: parsing/packing of this type isn't available yet")] ErrNotStarted, + /// MDNS: parsing/packing of this section has completed. #[error("mDNS: parsing/packing of this section has completed")] ErrSectionDone, + /// MDNS: parsing/packing of this section is header. #[error("mDNS: parsing/packing of this section is header")] ErrSectionHeader, + /// MDNS: insufficient data for base length type. #[error("mDNS: insufficient data for base length type")] ErrBaseLen, + /// MDNS: insufficient data for calculated length type. #[error("mDNS: insufficient data for calculated length type")] ErrCalcLen, + /// MDNS: segment prefix is reserved. #[error("mDNS: segment prefix is reserved")] ErrReserved, + /// MDNS: too many pointers (>10). #[error("mDNS: too many pointers (>10)")] ErrTooManyPtr, + /// MDNS: invalid pointer. #[error("mDNS: invalid pointer")] ErrInvalidPtr, + /// MDNS: nil resource body. #[error("mDNS: nil resource body")] ErrNilResourceBody, + /// MDNS: insufficient data for resource body length. #[error("mDNS: insufficient data for resource body length")] ErrResourceLen, + /// MDNS: segment length too long. #[error("mDNS: segment length too long")] ErrSegTooLong, + /// MDNS: zero length segment. #[error("mDNS: zero length segment")] ErrZeroSegLen, + /// MDNS: resource length too long. #[error("mDNS: resource length too long")] ErrResTooLong, + /// MDNS: too many Questions to pack (>65535). #[error("mDNS: too many Questions to pack (>65535)")] ErrTooManyQuestions, + /// MDNS: too many Answers to pack (>65535). #[error("mDNS: too many Answers to pack (>65535)")] ErrTooManyAnswers, + /// MDNS: too many Authorities to pack (>65535). #[error("mDNS: too many Authorities to pack (>65535)")] ErrTooManyAuthorities, + /// MDNS: too many Additionals to pack (>65535). #[error("mDNS: too many Additionals to pack (>65535)")] ErrTooManyAdditionals, + /// MDNS: name is not in canonical format (it must end with a .). #[error("mDNS: name is not in canonical format (it must end with a .)")] ErrNonCanonicalName, + /// MDNS: character string exceeds maximum length (255). #[error("mDNS: character string exceeds maximum length (255)")] ErrStringTooLong, + /// MDNS: compressed name in SRV resource data. #[error("mDNS: compressed name in SRV resource data")] ErrCompressedSrv, + /// MDNS: empty builder msg. #[error("mDNS: empty builder msg")] ErrEmptyBuilderMsg, @@ -1247,6 +1722,12 @@ pub enum Error { #[error("data channel closed")] ErrDataChannelClosed, + /// ErrDataChannelNotOpen indicates a send was attempted on a data channel + /// whose underlying SCTP stream has not been established yet — its + /// `ready_state` is still `connecting`. Wait for the channel to open. + #[error("data channel is not open yet")] + ErrDataChannelNotOpen, + /// ErrDataChannelNonExist indicates an operation executed when the data /// channel not existed. #[error("data channel not existed")] @@ -1416,12 +1897,15 @@ pub enum Error { #[error("unable to start track, codec is not supported by remote")] ErrUnsupportedCodec, + /// Invalid state error. #[error("Invalid state error")] InvalidStateError, + /// Invalid modification error. #[error("Invalid modification error")] InvalidModificationError, + /// Range error. #[error("Range error {0}")] RangeError(String), @@ -1439,6 +1923,7 @@ pub enum Error { #[error("new track must be of the same kind as previous")] ErrRTPSenderNewTrackHasIncorrectKind, + /// New track has incorrect envelope. #[error("new track has incorrect envelope")] ErrRTPSenderNewTrackHasIncorrectEnvelope, @@ -1463,6 +1948,7 @@ pub enum Error { #[error("a header extension must be registered with the same direction each time")] ErrRegisterHeaderExtensionInvalidDirection, + /// Invalid direction. #[error("invalid direction")] ErrInvalidDirection, @@ -1477,164 +1963,243 @@ pub enum Error { #[error("simulcast probe limit has been reached, new SSRC has been discarded")] ErrSimulcastProbeOverflow, + /// Enable detaching by calling webrtc.DetachDataChannels. #[error("enable detaching by calling webrtc.DetachDataChannels()")] ErrDetachNotEnabled, + /// Datachannel not opened yet, try calling Detach from OnOpen. #[error("datachannel not opened yet, try calling Detach from OnOpen")] ErrDetachBeforeOpened, + /// The DTLS transport has not started yet. #[error("the DTLS transport has not started yet")] ErrDtlsTransportNotStarted, + /// Failed extracting keys from DTLS for SRTP. #[error("failed extracting keys from DTLS for SRTP")] ErrDtlsKeyExtractionFailed, + /// Failed to start SRTP. #[error("failed to start SRTP")] ErrFailedToStartSRTP, + /// Failed to start SRTCP. #[error("failed to start SRTCP")] ErrFailedToStartSRTCP, + /// Attempted to start DTLSTransport that is not in new state. #[error("attempted to start DTLSTransport that is not in new state")] ErrInvalidDTLSStart, + /// Peer didn't provide certificate via DTLS. #[error("peer didn't provide certificate via DTLS")] ErrNoRemoteCertificate, + /// Identity provider is not implemented. #[error("identity provider is not implemented")] ErrIdentityProviderNotImplemented, + /// Remote certificate does not match any fingerprint. #[error("remote certificate does not match any fingerprint")] ErrNoMatchingCertificateFingerprint, + /// Unsupported fingerprint algorithm. #[error("unsupported fingerprint algorithm")] ErrUnsupportedFingerprintAlgorithm, + /// ICE connection not started. #[error("ICE connection not started")] ErrICEConnectionNotStarted, + /// Unknown candidate type. #[error("unknown candidate type")] ErrICECandidateTypeUnknown, + /// Cannot convert ICE.CandidateType into webrtc.ICECandidateType, invalid type. #[error("cannot convert ice.CandidateType into webrtc.ICECandidateType, invalid type")] ErrICEInvalidConvertCandidateType, + /// ICEAgent does not exist. #[error("ICEAgent does not exist")] ErrICEAgentNotExist, + /// Unable to convert ICE candidates to ICECandidates. #[error("unable to convert ICE candidates to ICECandidates")] ErrICECandidatesConversionFailed, + /// Unknown ICE Role. #[error("unknown ICE Role")] ErrICERoleUnknown, + /// Unknown protocol. #[error("unknown protocol")] ErrICEProtocolUnknown, + /// Gatherer not started. #[error("gatherer not started")] ErrICEGathererNotStarted, + /// Unknown network type. #[error("unknown network type")] ErrNetworkTypeUnknown, + /// New SDP does not match previous offer. #[error("new sdp does not match previous offer")] ErrSDPDoesNotMatchOffer, + /// New SDP does not match previous answer. #[error("new sdp does not match previous answer")] ErrSDPDoesNotMatchAnswer, + /// Provided value is not a valid enum value of type SDPType. #[error("provided value is not a valid enum value of type SDPType")] ErrPeerConnSDPTypeInvalidValue, + /// Invalid state change op. #[error("invalid state change op")] ErrPeerConnStateChangeInvalid, + /// Unhandled state change op. #[error("unhandled state change op")] ErrPeerConnStateChangeUnhandled, + /// Invalid SDP type supplied to SetLocalDescription. #[error("invalid SDP type supplied to SetLocalDescription()")] ErrPeerConnSDPTypeInvalidValueSetLocalDescription, + /// RemoteDescription contained media section without mid value. #[error("remoteDescription contained media section without mid value")] ErrPeerConnRemoteDescriptionWithoutMidValue, + /// LocalDescription contained media section without mid value. #[error("localDescription contained media section without mid value")] ErrPeerConnLocalDescriptionWithoutMidValue, + /// RemoteDescription has not been set yet. #[error("remoteDescription has not been set yet")] ErrPeerConnRemoteDescriptionNil, + /// LocalDescription has not been set yet. #[error("localDescription has not been set yet")] ErrPeerConnLocalDescriptionNil, + /// Single media section has an explicit SSRC. #[error("single media section has an explicit SSRC")] ErrPeerConnSingleMediaSectionHasExplicitSSRC, + /// Could not add transceiver for remote SSRC. #[error("could not add transceiver for remote SSRC")] ErrPeerConnRemoteSSRCAddTransceiver, + /// Mid RTP Extensions required for Simulcast. #[error("mid RTP Extensions required for Simulcast")] ErrPeerConnSimulcastMidRTPExtensionRequired, + /// Stream id RTP Extensions required for Simulcast. #[error("stream id RTP Extensions required for Simulcast")] ErrPeerConnSimulcastStreamIDRTPExtensionRequired, + /// Incoming SSRC failed Simulcast probing. #[error("incoming SSRC failed Simulcast probing")] ErrPeerConnSimulcastIncomingSSRCFailed, + /// Failed collecting stats. #[error("failed collecting stats")] ErrPeerConnStatsCollectionFailed, + /// Add_transceiver_from_kind only accepts one RTPTransceiverInit. #[error("add_transceiver_from_kind only accepts one RTPTransceiverInit")] ErrPeerConnAddTransceiverFromKindOnlyAcceptsOne, + /// Add_transceiver_from_track only accepts one RTPTransceiverInit. #[error("add_transceiver_from_track only accepts one RTPTransceiverInit")] ErrPeerConnAddTransceiverFromTrackOnlyAcceptsOne, + /// Add_transceiver_from_kind currently only supports recvonly. #[error("add_transceiver_from_kind currently only supports recvonly")] ErrPeerConnAddTransceiverFromKindSupport, + /// Add_transceiver_from_track currently only supports sendonly and sendrecv. #[error("add_transceiver_from_track currently only supports sendonly and sendrecv")] ErrPeerConnAddTransceiverFromTrackSupport, + /// TODO set_identity_provider. #[error("TODO set_identity_provider")] ErrPeerConnSetIdentityProviderNotImplemented, + /// Write_RTCP failed to open write_stream. #[error("write_rtcp failed to open write_stream")] ErrPeerConnWriteRTCPOpenWriteStream, + /// Cannot find transceiver with mid. #[error("cannot find transceiver with mid")] ErrPeerConnTransceiverMidNil, + /// DTLSTransport must not be nil. #[error("DTLSTransport must not be nil")] ErrRTPReceiverDTLSTransportNil, + /// Receive has already been called. #[error("Receive has already been called")] ErrRTPReceiverReceiveAlreadyCalled, + /// Unable to find stream for Track with SSRC. #[error("unable to find stream for Track with SSRC")] ErrRTPReceiverWithSSRCTrackStreamNotFound, + /// No trackStreams found for SSRC. #[error("no trackStreams found for SSRC")] ErrRTPReceiverForSSRCTrackStreamNotFound, + /// No trackStreams found for RID. #[error("no trackStreams found for RID")] ErrRTPReceiverForRIDTrackStreamNotFound, + /// Invalid RTP Receiver transition. #[error("invalid RTP Receiver transition")] ErrRTPReceiverStateChangeInvalid, + /// Track must not be nil. #[error("Track must not be nil")] ErrRTPSenderTrackNil, + /// RTPSender not existed. #[error("RTPSender not existed")] ErrRTPSenderNotExisted, + /// Sender Track has been removed or replaced to nil. #[error("Sender Track has been removed or replaced to nil")] ErrRTPSenderTrackRemoved, + /// Sender cannot add encoding as rid is empty. #[error("Sender cannot add encoding as rid is empty")] ErrRTPSenderRidNil, + /// Sender cannot add encoding as there is no base track. #[error("Sender cannot add encoding as there is no base track")] ErrRTPSenderNoBaseEncoding, + /// Sender cannot add encoding as provided track does not match base track. #[error("Sender cannot add encoding as provided track does not match base track")] ErrRTPSenderBaseEncodingMismatch, + /// Sender cannot encoding due to RID collision. #[error("Sender cannot encoding due to RID collision")] ErrRTPSenderRIDCollision, + /// Sender does not have track for RID. #[error("Sender does not have track for RID")] ErrRTPSenderNoTrackForRID, + /// RTPReceiver not existed. #[error("RTPReceiver not existed")] ErrRTPReceiverNotExisted, + /// DTLSTransport must not be nil. #[error("DTLSTransport must not be nil")] ErrRTPSenderDTLSTransportNil, + /// Send has already been called. #[error("Send has already been called")] ErrRTPSenderSendAlreadyCalled, + /// RTPTransceiver not existed. #[error("RTPTransceiver not existed")] ErrRTPTransceiverNotExisted, + /// ErrRTPSenderTrackNil. #[error("errRTPSenderTrackNil")] ErrRTPTransceiverCannotChangeMid, + /// Invalid state change in RTPTransceiver.setSending. #[error("invalid state change in RTPTransceiver.setSending")] ErrRTPTransceiverSetSendingInvalidState, + /// Unsupported codec type by this transceiver. #[error("unsupported codec type by this transceiver")] ErrRTPTransceiverCodecUnsupported, + /// DTLS not established. #[error("DTLS not established")] ErrSCTPTransportDTLS, + /// Add_transceiver_SDP called with 0 transceivers. #[error("add_transceiver_sdp() called with 0 transceivers")] ErrSDPZeroTransceivers, + /// Invalid Media Section. Media + DataChannel both enabled. #[error("invalid Media Section. Media + DataChannel both enabled")] ErrSDPMediaSectionMediaDataChanInvalid, + /// Invalid Media Section Track Index. #[error("invalid Media Section Track Index")] ErrSDPMediaSectionTrackInvalid, + /// Set_answering_dtlsrole must DTLSRoleClient or DTLSRoleServer. #[error("set_answering_dtlsrole must DTLSRoleClient or DTLSRoleServer")] ErrSettingEngineSetAnsweringDTLSRole, + /// Can't rollback from stable state. #[error("can't rollback from stable state")] ErrSignalingStateCannotRollback, + /// Invalid proposed signaling state transition. #[error("invalid proposed signaling state transition: {0}")] ErrSignalingStateProposedTransitionInvalid(String), + /// Cannot convert to StatsICECandidatePairStateSucceeded invalid ICE candidate state. #[error("cannot convert to StatsICECandidatePairStateSucceeded invalid ice candidate state")] ErrStatsICECandidateStateInvalid, + /// ICETransport can only be called in ICETransportStateNew. #[error("ICETransport can only be called in ICETransportStateNew")] ErrICETransportNotInNew, + /// Bad Certificate PEM format. #[error("bad Certificate PEM format")] ErrCertificatePEMFormatError, + /// SCTP is not established. #[error("SCTP is not established")] ErrSCTPNotEstablished, + /// DataChannel is not opened. #[error("DataChannel is not opened")] ErrClosedPipe, + /// Interceptor is not bind. #[error("Interceptor is not bind")] ErrInterceptorNotBind, + /// Excessive retries in CreateOffer. #[error("excessive retries in CreateOffer")] ErrExcessiveRetries, + /// Not long enough to be a RTP Packet. #[error("not long enough to be a RTP Packet")] ErrRTPTooShort, @@ -1646,82 +2211,110 @@ pub enum Error { SimulcastRidParseErrorUnknownDirection, //SDP + /// Codec not found. #[error("codec not found")] CodecNotFound, + /// Missing whitespace. #[error("missing whitespace")] MissingWhitespace, + /// Missing colon. #[error("missing colon")] MissingColon, + /// Payload type not found. #[error("payload type not found")] PayloadTypeNotFound, + /// SdpInvalidSyntax. #[error("SdpInvalidSyntax: {0}")] SdpInvalidSyntax(String), + /// SdpInvalidValue. #[error("SdpInvalidValue: {0}")] SdpInvalidValue(String), + /// SDP: empty time_descriptions. #[error("sdp: empty time_descriptions")] SdpEmptyTimeDescription, + /// Parse extmap. #[error("parse extmap: {0}")] ParseExtMap(String), + /// A syntax error at a known offset in the input, rendered with the offending character marked. #[error("{} --> {} <-- {}", .s.substring(0,*.p), .s.substring(*.p, *.p+1), .s.substring(*.p+1, .s.len()) )] - SyntaxError { s: String, p: usize }, + SyntaxError { + /// The input being parsed. + s: String, + /// The byte offset of the offending character in `s`. + p: usize, + }, //Third Party Error - #[error("{0}")] - Sec1(#[source] sec1::Error), - #[error("{0}")] - P256(#[source] P256Error), - #[error("{0}")] - RcGen(#[from] rcgen::Error), + /// Invalid PEM. #[error("invalid PEM: {0}")] InvalidPEM(String), - #[error("aes gcm: {0}")] - AesGcm(#[from] aes_gcm::Error), + /// Parse ip. #[error("parse ip: {0}")] ParseIp(#[from] net::AddrParseError), + /// Parse int. #[error("parse int: {0}")] ParseInt(#[from] ParseIntError), + /// An underlying I/O error. #[error("{0}")] Io(#[source] IoError), + /// URL parse. #[error("url parse: {0}")] Url(#[from] url::ParseError), + /// UTF-8. #[error("utf8: {0}")] Utf8(#[from] FromUtf8Error), + /// An error from the standard library or another boxed source. #[error("{0}")] Std(#[source] StdError), - #[error("{0}")] - Aes(#[from] aes::cipher::InvalidLength), - //Other Errors + /// Other RTCP Err. #[error("Other RTCP Err: {0}")] OtherRtcpErr(String), + /// Other RTP Err. #[error("Other RTP Err: {0}")] OtherRtpErr(String), + /// Other SRTP Err. #[error("Other SRTP Err: {0}")] OtherSrtpErr(String), + /// Other STUN Err. #[error("Other STUN Err: {0}")] OtherStunErr(String), + /// Other TURN Err. #[error("Other TURN Err: {0}")] OtherTurnErr(String), + /// Other ICE Err. #[error("Other ICE Err: {0}")] OtherIceErr(String), + /// Other DTLS Err. #[error("Other DTLS Err: {0}")] OtherDtlsErr(String), + /// Other SCTP Err. #[error("Other SCTP Err: {0}")] OtherSctpErr(String), + /// Other DataChannel Err. #[error("Other DataChannel Err: {0}")] OtherDataChannelErr(String), + /// Other Interceptor Err. #[error("Other Interceptor Err: {0}")] OtherInterceptorErr(String), + /// Other Media Err. #[error("Other Media Err: {0}")] OtherMediaErr(String), + /// Other mDNS Err. #[error("Other mDNS Err: {0}")] OtherMdnsErr(String), + /// Other SDP Err. #[error("Other SDP Err: {0}")] OtherSdpErr(String), + /// Other PeerConnection Err. #[error("Other PeerConnection Err: {0}")] OtherPeerConnectionErr(String), + /// A provider-neutral cryptographic operation failed. + #[error("crypto: {0}")] + Crypto(String), #[error("{0}")] + /// An error that does not fit any other variant, carrying a description. Other(String), } @@ -1795,34 +2388,6 @@ impl From> for Error { } } -impl From for Error { - fn from(e: sec1::Error) -> Self { - Error::Sec1(e) - } -} - -/// Wrapper around [`p256::elliptic_curve::Error`] that implements [`PartialEq`]. -/// -/// `p256::elliptic_curve::Error` does not implement `PartialEq`, which is -/// required by the top-level [`enum@Error`] enum. This newtype always returns -/// `false` for equality comparisons, which is the safe conservative choice -/// for opaque cryptographic errors. -#[derive(Debug, Error)] -#[error("{0}")] -pub struct P256Error(#[source] p256::elliptic_curve::Error); - -impl PartialEq for P256Error { - fn eq(&self, _: &Self) -> bool { - false - } -} - -impl From for Error { - fn from(e: p256::elliptic_curve::Error) -> Self { - Error::P256(P256Error(e)) - } -} - impl From for Error { fn from(e: SystemTimeError) -> Self { Error::Other(e.to_string()) diff --git a/rtc-shared/src/ifaces/ffi/mod.rs b/rtc-shared/src/ifaces/ffi/mod.rs index 499bd7d7..abcabebc 100644 --- a/rtc-shared/src/ifaces/ffi/mod.rs +++ b/rtc-shared/src/ifaces/ffi/mod.rs @@ -8,6 +8,16 @@ mod unix; #[cfg(target_family = "unix")] pub use self::unix::ifaces; +/// Enumerates local network interfaces. +/// +/// This is the fallback for platforms that are neither Windows nor Unix: it always returns +/// [`ErrorKind::Unsupported`](std::io::ErrorKind::Unsupported). ICE gathering treats that as +/// "no host candidates from interface enumeration" and falls back to the addresses the caller +/// supplied explicitly. +/// +/// # Errors +/// +/// Always fails on such platforms. #[cfg(not(any(target_family = "windows", target_family = "unix")))] pub fn ifaces() -> Result, std::io::Error> { Err(std::io::Error::new( diff --git a/rtc-shared/src/ifaces/ffi/windows/mod.rs b/rtc-shared/src/ifaces/ffi/windows/mod.rs index c30f04fa..b6cdb0b3 100644 --- a/rtc-shared/src/ifaces/ffi/windows/mod.rs +++ b/rtc-shared/src/ifaces/ffi/windows/mod.rs @@ -182,6 +182,7 @@ bitflags! { } #[repr(C)] +#[non_exhaustive] pub enum IpPrefixOrigin { Other = 0, Manual, @@ -192,6 +193,7 @@ pub enum IpPrefixOrigin { } #[repr(C)] +#[non_exhaustive] pub enum IpSuffixOrigin { Other = 0, Manual, @@ -204,6 +206,7 @@ pub enum IpSuffixOrigin { #[derive(PartialEq, Eq)] #[repr(C)] +#[non_exhaustive] pub enum IpDadState { Invalid = 0, Tentative, @@ -213,6 +216,7 @@ pub enum IpDadState { } #[repr(C)] +#[non_exhaustive] pub enum IfOperStatus { Up = 1, Down = 2, @@ -224,6 +228,7 @@ pub enum IfOperStatus { } #[repr(C)] +#[non_exhaustive] pub enum NetIfConnectionType { Dedicated = 1, Passive = 2, @@ -232,6 +237,7 @@ pub enum NetIfConnectionType { } #[repr(C)] +#[non_exhaustive] pub enum TunnelType { None = 0, Other = 1, diff --git a/rtc-shared/src/ifaces/mod.rs b/rtc-shared/src/ifaces/mod.rs index b786659e..5cf8a068 100644 --- a/rtc-shared/src/ifaces/mod.rs +++ b/rtc-shared/src/ifaces/mod.rs @@ -1,26 +1,46 @@ +/// Platform-specific interface enumeration (FFI into the OS). pub mod ffi; pub use ffi::ifaces; #[derive(PartialEq, Eq, Debug, Clone)] +/// The next hop configured for an interface address. +#[non_exhaustive] pub enum NextHop { + /// The broadcast address of the attached network. Broadcast(::std::net::SocketAddr), + /// The destination address, for point-to-point links. Destination(::std::net::SocketAddr), } #[derive(PartialEq, Eq, Debug, Clone)] +/// The address family or link type an [`Interface`] entry describes. +#[non_exhaustive] pub enum Kind { + /// A raw packet-level (link layer) address. Packet, + /// A link-layer address, such as a MAC address. Link, + /// An IPv4 address. Ipv4, + /// An IPv6 address. Ipv6, + /// An address family this crate does not recognise, carrying the raw OS value. Unknow(i32), } #[derive(Debug, Clone)] +/// One address on one local network interface. +/// +/// ICE gathering walks these to produce host candidates. pub struct Interface { + /// The OS name of the interface, such as `en0` or `eth0`. pub name: String, + /// Which address family or link type this entry describes. pub kind: Kind, + /// The address itself, if the OS reported one. pub addr: Option<::std::net::SocketAddr>, + /// The netmask for [`Self::addr`], if the OS reported one. pub mask: Option<::std::net::SocketAddr>, + /// The broadcast or point-to-point destination address, if any. pub hop: Option, } diff --git a/rtc-shared/src/lib.rs b/rtc-shared/src/lib.rs index cb841d94..ec624af6 100644 --- a/rtc-shared/src/lib.rs +++ b/rtc-shared/src/lib.rs @@ -1,27 +1,83 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! Shared types and utilities for the Sans-I/O WebRTC stack. +//! +//! This crate holds what every other crate in the [`rtc`](https://docs.rs/rtc) stack needs: +//! the common [`Error`](error::Error) type, the [`Marshal`](marshal::Marshal)/[`Unmarshal`](marshal::Unmarshal) +//! traits that every protocol codec implements, and the transport plumbing that carries +//! bytes between the network and a protocol state machine. +//! +//! # Key types +//! +//! * [`TransportContext`] / [`TransportMessage`] — a datagram plus the 4-tuple and protocol +//! it arrived on or should be sent on. Every layer in the stack passes these around +//! instead of touching sockets. +//! * [`marshal`] — [`Marshal`](marshal::Marshal), [`Unmarshal`](marshal::Unmarshal) and +//! [`MarshalSize`](marshal::MarshalSize), the wire-format traits shared by STUN, RTP, +//! RTCP, SDP, DTLS and SCTP. +//! * [`error`] — the crate-wide [`Error`](error::Error) enum and `Result` alias, re-exported +//! by the higher-level crates so callers import from one place. +//! * [`crypto`], [`replay_detector`] — primitives shared by DTLS and SRTP. +//! * [`tcp_framing`] — RFC 4571 length-prefixed framing, for ICE-TCP candidates. +//! * [`ifaces`] — local interface enumeration used during ICE candidate gathering. +//! +//! # Feature flags +//! +//! `crypto`, `ifaces`, `marshal` and `replay` are all enabled by default; each gates the +//! correspondingly named module so that dependents can compile only what they use. +//! +//! # Example +//! +//! Every protocol codec in the stack implements the same three traits, so encoding and decoding +//! look the same whichever layer you are at: +//! +//! ``` +//! use bytes::Bytes; +//! use rtc_shared::marshal::{Marshal, MarshalSize, Unmarshal}; +//! +//! # fn round_trip(value: T) +//! # -> Result<(), Box> { +//! // Size the buffer, encode into it, then decode the result back. +//! let n = value.marshal_size(); +//! let encoded = value.marshal()?; +//! assert_eq!(encoded.len(), n); +//! +//! let mut buf = Bytes::from(encoded.to_vec()); +//! assert_eq!(T::unmarshal(&mut buf)?, value); +//! # Ok(()) +//! # } +//! ``` +//! +//! Most applications do not depend on this crate directly — the [`rtc`](https://docs.rs/rtc) +//! crate re-exports what it needs as `rtc::shared`. + #[cfg(target_family = "windows")] #[macro_use] extern crate bitflags; -#[cfg(feature = "crypto")] -pub mod crypto; - #[cfg(feature = "ifaces")] +/// Local network interface enumeration, used to gather ICE host candidates. pub mod ifaces; #[cfg(feature = "marshal")] +/// The wire-format traits every protocol codec in the stack implements. pub mod marshal; #[cfg(feature = "replay")] +/// Replay protection for sequence-numbered packets, as DTLS and SRTP require. pub mod replay_detector; +/// The crate-wide error type shared by every protocol in the stack. pub mod error; +/// `serde` helpers for types that have no natural serialized form, such as [`std::time::Instant`]. pub mod serde; pub mod tcp_framing; +/// Conversions between monotonic, Unix and NTP time. pub mod time; pub(crate) mod transport; +/// Small shared helpers: packet demultiplexing predicates and random-string generation. pub mod util; pub use transport::{ diff --git a/rtc-shared/src/marshal/mod.rs b/rtc-shared/src/marshal/mod.rs index 10915115..0a7f3e6a 100644 --- a/rtc-shared/src/marshal/mod.rs +++ b/rtc-shared/src/marshal/mod.rs @@ -2,13 +2,33 @@ use bytes::{Buf, BytesMut}; use crate::error::{Error, Result}; +/// The encoded size of a value, in bytes. +/// +/// Implemented alongside [`Marshal`]/[`Unmarshal`] so a caller can size a buffer before +/// encoding, and so nested codecs can compute offsets without encoding twice. pub trait MarshalSize: Send + Sync { + /// The number of bytes [`Marshal::marshal_to`] will write for this value. fn marshal_size(&self) -> usize; } +/// Encodes a value into its wire format. +/// +/// Every protocol codec in the stack — STUN, RTP, RTCP, SDP, DTLS, SCTP — implements this +/// so that higher layers can serialize uniformly. pub trait Marshal: MarshalSize { + /// Encodes into `buf`, returning the number of bytes written. + /// + /// # Errors + /// + /// Fails if `buf` is shorter than [`MarshalSize::marshal_size`], or if the value itself is + /// not encodable (an out-of-range field, for instance). fn marshal_to(&self, buf: &mut [u8]) -> Result; + /// Encodes into a freshly allocated buffer sized by [`MarshalSize::marshal_size`]. + /// + /// # Errors + /// + /// Propagates any failure from [`Self::marshal_to`]. fn marshal(&self) -> Result { let l = self.marshal_size(); let mut buf = BytesMut::with_capacity(l); @@ -24,7 +44,13 @@ pub trait Marshal: MarshalSize { } } +/// Decodes a value from its wire format. pub trait Unmarshal: MarshalSize { + /// Decodes one value from `buf`, advancing it past the bytes consumed. + /// + /// # Errors + /// + /// Fails if `buf` is truncated, or if its contents are not a valid encoding of `Self`. fn unmarshal(buf: &mut B) -> Result where Self: Sized, diff --git a/rtc-shared/src/replay_detector/mod.rs b/rtc-shared/src/replay_detector/mod.rs index a8c4d40c..72adf020 100644 --- a/rtc-shared/src/replay_detector/mod.rs +++ b/rtc-shared/src/replay_detector/mod.rs @@ -5,13 +5,26 @@ mod replay_detector_test; use fixed_big_int::*; // ReplayDetector is the interface of sequence replay detector. +/// Tracks which sequence numbers have already been seen, so replayed packets can be dropped. +/// +/// Both DTLS and SRTP require this: an attacker who captures a packet must not be able to +/// have it accepted a second time. pub trait ReplayDetector: Send + Sync { - // Check returns true if given sequence number is not replayed. - // Call accept() to mark the packet is received properly. + /// Returns `true` if `seq` has not been seen before and is inside the window. + /// + /// This only tests; call [`Self::accept`] afterwards to record the packet as received. fn check(&mut self, seq: u64) -> bool; + /// Commits the sequence number from the preceding [`Self::check`] call as received. + /// + /// Split from `check` so a caller can validate a packet's authenticity first and only then + /// record it — a forged packet must not advance the window. fn accept(&mut self); } +/// A replay detector over a monotonically increasing sequence number that never wraps. +/// +/// Handles the full 64-bit range, which is what DTLS needs. See +/// [`WrappedSlidingWindowDetector`] for sequence numbers that do wrap. pub struct SlidingWindowDetector { accepted: bool, seq: u64, @@ -22,10 +35,11 @@ pub struct SlidingWindowDetector { } impl SlidingWindowDetector { - // New creates ReplayDetector. - // Created ReplayDetector doesn't allow wrapping. - // It can handle monotonically increasing sequence number up to - // full 64bit number. It is suitable for DTLS replay protection. + /// Creates a detector with a `window_size`-wide window over sequence numbers up to + /// `max_seq`. + /// + /// Does not allow wrapping: it handles monotonically increasing sequence numbers across + /// the full 64-bit range, which is what DTLS replay protection needs. pub fn new(window_size: usize, max_seq: u64) -> Self { SlidingWindowDetector { accepted: false, @@ -77,6 +91,10 @@ impl ReplayDetector for SlidingWindowDetector { } } +/// A replay detector for a sequence number that wraps at a known maximum. +/// +/// SRTP's 16-bit sequence numbers wrap, so the window has to interpret a large backwards +/// jump as a rollover rather than a replay. pub struct WrappedSlidingWindowDetector { accepted: bool, seq: u64, @@ -88,8 +106,10 @@ pub struct WrappedSlidingWindowDetector { } impl WrappedSlidingWindowDetector { - // WithWrap creates ReplayDetector allowing sequence wrapping. - // This is suitable for short bitwidth counter like SRTP and SRTCP. + /// Creates a detector with a `window_size`-wide window that allows the sequence number to + /// wrap at `max_seq`. + /// + /// Suitable for the short counters used by SRTP and SRTCP. pub fn new(window_size: usize, max_seq: u64) -> Self { WrappedSlidingWindowDetector { accepted: false, @@ -169,6 +189,9 @@ impl ReplayDetector for WrappedSlidingWindowDetector { } #[derive(Default)] +/// A detector that accepts everything. +/// +/// For contexts where replay protection is disabled or handled elsewhere. pub struct NoOpReplayDetector; impl ReplayDetector for NoOpReplayDetector { diff --git a/rtc-shared/src/serde.rs b/rtc-shared/src/serde.rs index 54a864dc..27d628ab 100644 --- a/rtc-shared/src/serde.rs +++ b/rtc-shared/src/serde.rs @@ -8,6 +8,14 @@ pub mod instant_to_epoch { use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + /// Serializes an [`Instant`] as a duration relative to the process's reference instant. + /// + /// [`Instant`] has no absolute representation, so this encodes the offset instead. Use + /// with `#[serde(with = "...")]`. + /// + /// # Errors + /// + /// Propagates any failure from the underlying serializer. pub fn serialize(instant: &Instant, serializer: S) -> Result where S: Serializer, @@ -24,6 +32,11 @@ pub mod instant_to_epoch { epoch_s.serialize(serializer) } + /// Deserializes an [`Instant`] from the relative offset written by [`serialize`]. + /// + /// # Errors + /// + /// Propagates any failure from the underlying deserializer. pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/rtc-shared/src/time.rs b/rtc-shared/src/time.rs index da7f3981..04a49b31 100644 --- a/rtc-shared/src/time.rs +++ b/rtc-shared/src/time.rs @@ -1,13 +1,25 @@ +//! Monotonic, Unix and NTP time. +//! +//! Protocol logic measures time with a monotonic [`Instant`](std::time::Instant), which cannot go +//! backwards but has no absolute meaning. RTCP timestamps need the opposite: wall-clock time in +//! NTP format. [`SystemInstant`](crate::time::SystemInstant) captures both once, so either can be derived from the other later +//! without re-reading a clock that may have been adjusted in between. use std::ops::Add; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +/// A monotonic [`Instant`] paired with the wall-clock time it was taken at. +/// +/// Sans-I/O protocol code measures time with a monotonic [`Instant`], but RTCP timestamps +/// and NTP fields need wall-clock time. Capturing both once lets either be derived from the +/// other later without re-reading the (non-monotonic) system clock. pub struct SystemInstant { instant: Instant, duration_since_unix_epoch: Duration, } impl SystemInstant { + /// Captures the current monotonic instant together with the current wall-clock time. pub fn now() -> Self { Self { instant: Instant::now(), @@ -17,23 +29,31 @@ impl SystemInstant { } } + /// Converts a Unix-epoch duration back into the monotonic [`Instant`] it corresponds to. pub fn instant(&self, duration_since_unix_epoch: Duration) -> Instant { self.instant + duration_since_unix_epoch - self.duration_since_unix_epoch } + /// The wall-clock time, as a duration since the Unix epoch, captured at construction. pub fn duration_since_unix_epoch(&self) -> Duration { self.duration_since_unix_epoch } + /// Converts the monotonic `now` into wall-clock time as a duration since the Unix epoch. pub fn unix(&self, now: Instant) -> Duration { now.duration_since(self.instant) .add(self.duration_since_unix_epoch) } + /// Converts the monotonic `now` into a 64-bit NTP timestamp, as RTCP Sender Reports carry. pub fn ntp(&self, now: Instant) -> u64 { SystemInstant::unix2ntp(self.unix(now)) } + /// Converts a Unix-epoch duration into a 64-bit NTP timestamp. + /// + /// The result is seconds since the NTP epoch (1900-01-01) in the high 32 bits and a binary + /// fraction of a second in the low 32. pub fn unix2ntp(duration_since_unix_epoch: Duration) -> u64 { let u = duration_since_unix_epoch.as_nanos() as u64; @@ -47,6 +67,9 @@ impl SystemInstant { s | f } + /// Converts a 64-bit NTP timestamp into a duration since the Unix epoch. + /// + /// The inverse of [`Self::unix2ntp`]. pub fn ntp2unix(ntp: u64) -> Duration { let mut s = ntp >> 32; let mut f = ntp & 0xFFFFFFFF; diff --git a/rtc-shared/src/transport.rs b/rtc-shared/src/transport.rs index ad150c3d..9293ed37 100644 --- a/rtc-shared/src/transport.rs +++ b/rtc-shared/src/transport.rs @@ -31,6 +31,7 @@ impl EcnCodepoint { /// Type of transport protocol, either UDP or TCP #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[non_exhaustive] pub enum TransportProtocol { /// UDP #[default] diff --git a/rtc-shared/src/util.rs b/rtc-shared/src/util.rs index 4f7cef6f..16dcd948 100644 --- a/rtc-shared/src/util.rs +++ b/rtc-shared/src/util.rs @@ -1,3 +1,10 @@ +//! Shared helpers: packet demultiplexing and random strings. +//! +//! WebRTC multiplexes STUN, DTLS and SRTP onto one port, so the first byte of a datagram decides +//! which layer receives it ([RFC 7983]). The `match_*` predicates implement those ranges, and +//! [`is_rtcp`](crate::util::is_rtcp) separates RTCP from RTP once a packet is known to be one of the two. +//! +//! [RFC 7983]: https://datatracker.ietf.org/doc/html/rfc7983 use crate::error::{Error, Result}; use rand::{RngExt, rng}; use std::net::{SocketAddr, ToSocketAddrs}; @@ -32,12 +39,20 @@ pub fn match_dtls(b: &[u8]) -> bool { match_range(20, 63)(b) } -// match_srtp_or_srtcp is a MatchFunc that accepts packets with the first byte in [128..191] -// as defied in RFC7983 +/// Returns `true` if `b` looks like SRTP or SRTCP: its first byte is in `[128, 191]`. +/// +/// One of the demultiplexing predicates from [RFC 7983], which is how a single port can carry +/// STUN, DTLS and SRTP at once. +/// +/// [RFC 7983]: https://datatracker.ietf.org/doc/html/rfc7983 pub fn match_srtp_or_srtcp(b: &[u8]) -> bool { match_range(128, 191)(b) } +/// Returns `true` if `buf` is RTCP rather than RTP. +/// +/// Distinguished by the payload-type byte: RTCP packet types occupy `[192, 223]`, which RTP +/// cannot use. Returns `false` for buffers too short to tell. pub fn is_rtcp(buf: &[u8]) -> bool { // Not long enough to determine RTP/RTCP if buf.len() < 4 { @@ -86,6 +101,9 @@ pub fn math_rand_alpha_number(n: usize) -> String { } //TODO: generates a random string for cryptographic usage. +/// Generates a random `n`-character string drawn from `runes`. +/// +/// Used for values that must be unguessable, such as ICE credentials and SDP identifiers. pub fn generate_crypto_random_string(n: usize, runes: &[u8]) -> String { let mut rng = rng(); diff --git a/rtc-srtp/Cargo.toml b/rtc-srtp/Cargo.toml index b44cfed0..a718aefe 100644 --- a/rtc-srtp/Cargo.toml +++ b/rtc-srtp/Cargo.toml @@ -12,34 +12,23 @@ keywords.workspace = true categories.workspace = true [features] -default = ["ring"] -ring = ["dep:ring"] -aws-lc-rs = ["dep:aws-lc-rs"] -openssl = ["dep:openssl"] -vendored-openssl = ["openssl/vendored"] +default = ["crypto-ring"] +crypto-ring = ["crypto/crypto-ring"] +crypto-aws-lc-rs = ["crypto/crypto-aws-lc-rs"] [dependencies] -shared = { workspace = true, default-features = false, features = ["crypto", "marshal", "replay"] } +shared = { workspace = true, default-features = false, features = ["marshal", "replay"] } rtp.workspace = true rtcp.workspace = true +crypto.workspace = true byteorder.workspace = true bytes.workspace = true -hmac = { version = "0.12.1", features = ["std", "reset"] } -sha1 = "0.10.6" -ctr = "0.9.2" -aes = "0.8.4" -subtle = "2.5.0" -# AES-GCM (AEAD_AES_128/256_GCM profiles) runs on ring's hardware-accelerated, -# single-pass AES-GCM; the AES-CM-HMAC-SHA1 profiles below stay on RustCrypto. -ring = { workspace = true, optional = true } -aws-lc-rs = { workspace = true, optional = true } -openssl = { version = "0.10.72", optional = true } [dev-dependencies] criterion.workspace = true -lazy_static = "1.4.0" +lazy_static.workspace = true [[bench]] name = "bench" -harness = false \ No newline at end of file +harness = false diff --git a/rtc-srtp/benches/README.md b/rtc-srtp/benches/README.md index c6923d4b..c30acabb 100644 --- a/rtc-srtp/benches/README.md +++ b/rtc-srtp/benches/README.md @@ -1,89 +1,150 @@ -### Benchmark Results +# SRTP benchmarks -MacBook Air M3 24 GB MacOS 26.2 - -``` +```bash cargo bench --package rtc-srtp --bench bench -Gnuplot not found, using plotters backend -SRTP/Encrypt/RTP time: [5.6858 µs 5.7234 µs 5.7694 µs] -Found 2 outliers among 100 measurements (2.00%) - 2 (2.00%) high severe -SRTP/Decrypt/RTP time: [5.6254 µs 5.6385 µs 5.6549 µs] -Found 3 outliers among 100 measurements (3.00%) - 2 (2.00%) high mild - 1 (1.00%) high severe -SRTP/Encrypt/RTCP time: [641.42 ns 643.53 ns 645.92 ns] -Found 7 outliers among 100 measurements (7.00%) - 3 (3.00%) high mild - 4 (4.00%) high severe -SRTP/Decrypt/RTCP time: [631.40 ns 633.15 ns 635.43 ns] -Found 8 outliers among 100 measurements (8.00%) - 5 (5.00%) high mild - 3 (3.00%) high severe ``` -``` - Finished `bench` profile [optimized] target(s) in 0.12s - Running benches/bench.rs (target/release/deps/bench-5824b5a56534ac1c) -Gnuplot not found, using plotters backend -SRTP/Encrypt/RTP time: [5.6013 µs 5.6042 µs 5.6073 µs] - change: [−1.5372% −1.1158% −0.7427%] (p = 0.00 < 0.05) - Change within noise threshold. -Found 13 outliers among 100 measurements (13.00%) - 5 (5.00%) low severe - 3 (3.00%) high mild - 5 (5.00%) high severe -SRTP/Decrypt/RTP time: [5.5840 µs 5.5879 µs 5.5937 µs] - change: [−1.8765% −1.4457% −1.0644%] (p = 0.00 < 0.05) - Performance has improved. -Found 3 outliers among 100 measurements (3.00%) - 1 (1.00%) high mild - 2 (2.00%) high severe -SRTP/Encrypt/RTCP time: [635.38 ns 638.62 ns 644.16 ns] - change: [−1.3894% −0.8625% −0.2766%] (p = 0.00 < 0.05) - Change within noise threshold. -Found 4 outliers among 100 measurements (4.00%) - 1 (1.00%) high mild - 3 (3.00%) high severe -SRTP/Decrypt/RTCP time: [627.27 ns 627.68 ns 628.08 ns] - change: [−1.6764% −1.3366% −1.0230%] (p = 0.00 < 0.05) - Performance has improved. -Found 4 outliers among 100 measurements (4.00%) - 2 (2.00%) high mild - 2 (2.00%) high severe +Benchmarks run against every enabled built-in provider, so `--features crypto-ring,crypto-aws-lc-rs` reports +both backends side by side under identical inputs. + +The groups are: + +* `Encrypt/*`, `Decrypt/*` — per-packet cost on an already-keyed context. This is the hot path. +* `Setup/*` — context construction: provider dispatch, RFC 3711 §4.3 key derivation, and the + cipher key schedule. Paid **once per one-way context**, not per packet. +The split is deliberate. The crypto-provider design concentrates provider indirection in setup: a +keyed cipher object is obtained once and used per packet with no further dispatch. If `Encrypt/*` +ever regresses while `Setup/*` holds steady, that property has broken. + +--- + +## G3 crypto-provider migration: measured impact + +All figures below were taken on **one machine** (Apple M1 Max, macOS 26.5.2) with identical +criterion settings, comparing a git worktree at `425494c` (P3 — the last commit before SRTP moved +to `rtc-crypto`) against the current tree. + +```bash +cargo bench --package rtc-srtp --bench bench -- --warm-up-time 2 --measurement-time 5 ``` +### AES-128-CM + HMAC-SHA1-80, 1200-byte payload + +| Benchmark | Pre-migration (`425494c`) | ring (default) | aws-lc-rs | +|---|---|---|---| +| `Encrypt/RTP` | 1.725-1.780 µs | 1.723 µs | 1.018 µs | +| `Decrypt/RTP` | 1.731-1.751 µs | 1.714 µs | 1.019 µs | +| `Encrypt/RTCP` | 295.0-295.6 ns | 296.0 ns | 347.2 ns | +| `Decrypt/RTCP` | 303.1-311.6 ns | 312.4 ns | 353.0 ns | + +**At parity with the pre-migration baseline** on the default provider, after the three fixes +below. `aws-lc-rs` is faster still on the RTP path. + +The baseline column gives the range over three independent runs; run-to-run spread on this machine +is about ±3%, so treat anything inside that as parity rather than a difference. See +`docs/benchmarking-crypto-migration.md` for the procedure. + +### AEAD-AES-128-GCM, 1200-byte payload + +| Benchmark | Current | +|---|---| +| `Encrypt/RTP/AEAD-AES-128-GCM/ring` | 326.8 ns | +| `Decrypt/RTP/AEAD-AES-128-GCM/ring` | 333.6 ns | + +No pre-migration equivalent exists — the old bench covered only the CM/HMAC profile. Worth noting +it is roughly **15× faster** than AES-CTR + HMAC-SHA1 on the same payload, consistent with making +one keyed AEAD call instead of a keystream pass plus a separately keyed HMAC. + +### Construction cost + +| Benchmark | Current | +|---|---| +| `Setup/AES-128-CM-HMAC-SHA1-80/ring` | 1.198 µs | +| `Setup/AEAD-AES-128-GCM/ring` | 925.5 ns | + +## Fixed: block-at-a-time AES-CTR + +`rtc-crypto`'s `AesCtr::apply_keystream` originally drove `encrypt_block` once per 16-byte block — +75 serialized AES calls for a 1200-byte payload, defeating the batching that lets AES-NI / ARMv8 +crypto instructions pipeline. It now delegates to the `ctr` crate, matching what the pre-migration +SRTP cipher used. + +This accounts for only a small part of the regression; the cipher was not the bottleneck. + +The effect is size-dependent and honestly mixed: + +| Benchmark | Manual per-block loop | `ctr` crate | Change | +|---|---|---|---| +| `Encrypt/RTP` (1200 B) | 5.462 µs | 4.949 µs | 9% faster | +| `Decrypt/RTP` (1200 B) | 5.478 µs | 4.945 µs | 10% faster | +| `Encrypt/RTCP` (24 B) | 956.8 ns | 1.034 µs | 8% slower | +| `Decrypt/RTCP` (24 B) | 960.0 ns | 1.057 µs | 10% slower | + +For a two-block RTCP packet the `ctr` crate's per-call setup exceeds the batching benefit. It is +retained because 1200-byte RTP dominates real media traffic and because it restores the +pre-migration implementation choice. Bit-exactness is covered by the RFC 3711 known-answer tests +in `rtc-srtp` and the `rtc-crypto` conformance suite, both passing unchanged. + +## Fixed: per-packet HMAC key setup + +Pre-migration, `CipherAesCmHmacSha1` held a pre-keyed `Hmac` built once during context +construction, so the ipad/opad key schedule was computed once. After the migration the auth tag +went through a stateless provider call that rebuilt the key on every packet — the same +anti-pattern the design rejects for ciphers in §15.8, applied to AEAD, stream, and CBC ciphers but +originally not to MACs. + +`RTCCrypto::new_hmac` now returns a keyed [`Mac`] object, mirroring the cipher factories, and +`rtc-srtp` keys its SRTP and SRTCP MACs once per context. The one-shot `hmac()` and `verify_hmac()` +methods were removed from the trait: they are exactly `new_hmac(..)?.sign(..)` and +`new_hmac(..)?.verify(..)`, and keeping them would preserve the path that invites per-packet +keying. + +| Benchmark | Per-packet keying | Pre-keyed `Mac` | Change | +|---|---|---|---| +| `Encrypt/RTP` (1200 B) | 4.949 µs | 4.606 µs | 7% faster | +| `Encrypt/RTCP` (24 B) | 1.034 µs | 642.0 ns | **38% faster** | +| `Decrypt/RTCP` (24 B) | 1.057 µs | 628.7 ns | **41% faster** | + +The gain is largest for small packets, where key setup dominated the message pass. Context setup +correspondingly rises (1.198 µs → ~2.09 µs) because two MACs are now keyed there — the intended +trade: once per context instead of once per packet. + +`rtc-srtp/tests/provider_profiles.rs` guards this with a counting provider asserting the MAC is +constructed exactly twice per context and not per packet. + +## Fixed: ring's software SHA-1 + +`ring` exposes SHA-1 only as `HMAC_SHA1_FOR_LEGACY_USE_ONLY` and does not use the ARMv8 SHA-1 +instructions. Measured directly over a 1212-byte message: + +| HMAC-SHA1, 1212 B | ns/call | +|---|---| +| `ring` | 4468.6 | +| RustCrypto `hmac` + `sha1` (pre-migration) | 1373.0 | + +3.3x, and the whole of the residual SRTP gap. The built-in providers are **composite** by design +(§2.4 — AES-CTR, CCM, CBC and MD5 already come from RustCrypto), so the `ring` provider now +composes RustCrypto's HMAC-SHA1 as well. SHA-256 stays on `ring`, which does use the hardware +instructions. `aws-lc-rs` keeps its own SHA-1, which is faster than both. + +This closes the gap without making `aws-lc-rs` the default, which would have imposed the +`aws-lc-sys` C toolchain on every downstream build. + +## Reproducing + +```bash +# Current numbers +cargo bench --package rtc-srtp --bench bench -- --warm-up-time 2 --measurement-time 5 + +# Same-machine pre-migration baseline +git worktree add /tmp/rtc-baseline 425494c +cd /tmp/rtc-baseline +cargo bench --package rtc-srtp --bench bench -- --warm-up-time 2 --measurement-time 5 + +# Both backends, identical inputs +cargo bench --package rtc-srtp --bench bench --features crypto-ring,crypto-aws-lc-rs ``` - Finished `bench` profile [optimized] target(s) in 0.13s - Running benches/bench.rs (target/release/deps/bench-5824b5a56534ac1c) -Gnuplot not found, using plotters backend -SRTP/Encrypt/RTP time: [5.6134 µs 5.6378 µs 5.6736 µs] - change: [+0.8457% +3.4526% +7.1365%] (p = 0.01 < 0.05) - Change within noise threshold. -Found 15 outliers among 100 measurements (15.00%) - 1 (1.00%) low severe - 1 (1.00%) low mild - 2 (2.00%) high mild - 11 (11.00%) high severe -SRTP/Decrypt/RTP time: [5.5814 µs 5.5867 µs 5.5940 µs] - change: [−0.2377% −0.1258% +0.0067%] (p = 0.04 < 0.05) - Change within noise threshold. -Found 7 outliers among 100 measurements (7.00%) - 1 (1.00%) low mild - 2 (2.00%) high mild - 4 (4.00%) high severe -SRTP/Encrypt/RTCP time: [639.54 ns 642.79 ns 646.92 ns] - change: [−0.1150% +0.4636% +0.9981%] (p = 0.11 > 0.05) - No change in performance detected. -Found 8 outliers among 100 measurements (8.00%) - 1 (1.00%) high mild - 7 (7.00%) high severe -SRTP/Decrypt/RTCP time: [629.80 ns 630.61 ns 631.60 ns] - change: [+0.5928% +1.5843% +3.3894%] (p = 0.01 < 0.05) - Change within noise threshold. -Found 7 outliers among 100 measurements (7.00%) - 1 (1.00%) high mild - 6 (6.00%) high severe - -``` \ No newline at end of file + +Cross-machine comparison is not meaningful. Earlier revisions of this file recorded results from a +MacBook Air M3; those predate P3 and are not comparable to anything above. diff --git a/rtc-srtp/benches/bench.rs b/rtc-srtp/benches/bench.rs index 8a24a950..9207a4f8 100644 --- a/rtc-srtp/benches/bench.rs +++ b/rtc-srtp/benches/bench.rs @@ -4,6 +4,12 @@ use criterion::{BenchmarkGroup, Criterion, criterion_main}; use rtc_srtp::{context::Context, protection_profile::ProtectionProfile}; use shared::marshal::Marshal; +/// The built-in provider, for tests only. Library code never resolves a default: every public +/// constructor takes the provider from its caller. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + const MASTER_KEY: &[u8] = &[ 96, 180, 31, 4, 119, 137, 128, 252, 75, 194, 252, 44, 63, 56, 61, 55, ]; @@ -20,6 +26,7 @@ fn benchmark_encrypt_rtp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), ) .unwrap(); @@ -63,6 +70,7 @@ fn benchmark_decrypt_rtp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), ) .unwrap(); @@ -72,6 +80,7 @@ fn benchmark_decrypt_rtp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), ) .unwrap(); @@ -113,6 +122,7 @@ fn benchmark_encrypt_rtcp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), ) .unwrap(); @@ -130,6 +140,7 @@ fn benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), ) .unwrap() .encrypt_rtcp(RAW_RTCP) @@ -141,6 +152,7 @@ fn benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), ) .unwrap(); @@ -149,6 +161,128 @@ fn benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1(g: &mut BenchmarkGroup) }); } +/// The built-in providers compiled into this benchmark, so the two backends can be compared +/// under identical inputs. +fn providers() -> Vec<(&'static str, std::sync::Arc)> { + let mut providers: Vec<(&'static str, std::sync::Arc)> = + Vec::new(); + #[cfg(feature = "crypto-ring")] + providers.push(( + "ring", + std::sync::Arc::new(crypto::providers::RingProvider::default()), + )); + #[cfg(feature = "crypto-aws-lc-rs")] + providers.push(( + "aws-lc-rs", + std::sync::Arc::new(crypto::providers::AwsLcRsProvider::default()), + )); + assert!( + !providers.is_empty(), + "enable `ring` or `aws-lc-rs` to run these benchmarks" + ); + providers +} + +/// Context construction: provider dispatch, SRTP key derivation (RFC 3711 section 4.3), and the +/// cipher key schedule. Paid once per one-way context, not per packet — the counterpart to the +/// `Encrypt/*` and `Decrypt/*` hot-path measurements above. +fn benchmark_context_setup(g: &mut BenchmarkGroup) { + for (name, provider) in providers() { + for (label, profile) in [ + ( + "AES-128-CM-HMAC-SHA1-80", + ProtectionProfile::Aes128CmHmacSha1_80, + ), + ("AEAD-AES-128-GCM", ProtectionProfile::AeadAes128Gcm), + ] { + g.bench_function(format!("Setup/{label}/{name}"), |b| { + b.iter(|| { + Context::new( + MASTER_KEY, + &master_salt_for(profile), + profile, + None, + None, + provider.crypto(), + ) + .unwrap() + }); + }); + } + } +} + +/// AEAD-AES-128-GCM packet path, per provider. +fn benchmark_aead_aes_128_gcm(g: &mut BenchmarkGroup) { + for (name, provider) in providers() { + let profile = ProtectionProfile::AeadAes128Gcm; + let salt = master_salt_for(profile); + let mut encrypt_ctx = + Context::new(MASTER_KEY, &salt, profile, None, None, provider.crypto()).unwrap(); + let mut decrypt_ctx = + Context::new(MASTER_KEY, &salt, profile, None, None, provider.crypto()).unwrap(); + + let mut pld = BytesMut::new(); + for i in 0..1200 { + pld.extend_from_slice(&[i as u8]); + } + + g.bench_function(format!("Encrypt/RTP/AEAD-AES-128-GCM/{name}"), |b| { + let mut seq = 1; + b.iter_batched( + || { + let pkt = rtp::packet::Packet { + header: rtp::header::Header { + sequence_number: seq, + timestamp: seq.into(), + ssrc: 0xcafebabe, + ..Default::default() + }, + payload: pld.clone().freeze(), + }; + seq = seq.wrapping_add(1); + pkt.marshal().unwrap() + }, + |raw| encrypt_ctx.encrypt_rtp(&raw).unwrap(), + criterion::BatchSize::SmallInput, + ); + }); + + // Pre-encrypt a run of packets so decryption never replays an index, which the replay + // detector would reject. + let mut encrypted = Vec::new(); + for seq in 1..=1024u16 { + let pkt = rtp::packet::Packet { + header: rtp::header::Header { + sequence_number: seq, + timestamp: seq.into(), + ssrc: 0xcafebabe, + ..Default::default() + }, + payload: pld.clone().freeze(), + }; + let raw = pkt.marshal().unwrap(); + encrypted.push(encrypt_ctx.encrypt_rtp(&raw).unwrap()); + } + + let mut index = 0usize; + g.bench_function(format!("Decrypt/RTP/AEAD-AES-128-GCM/{name}"), |b| { + b.iter(|| { + let packet = &encrypted[index % encrypted.len()]; + index += 1; + let _ = decrypt_ctx.decrypt_rtp(packet); + }); + }); + } +} + +/// The master salt length differs per protection profile. +fn master_salt_for(profile: ProtectionProfile) -> Vec { + let mut salt = MASTER_SALT.to_vec(); + salt.resize(profile.salt_len(), 0x5a); + salt +} + fn benches() { let mut c = Criterion::default().configure_from_args(); let mut g = c.benchmark_group("SRTP"); @@ -157,6 +291,8 @@ fn benches() { benchmark_decrypt_rtp_aes_128_cm_hmac_sha1(&mut g); benchmark_encrypt_rtcp_aes_128_cm_hmac_sha1(&mut g); benchmark_decrypt_rtcp_aes_128_cm_hmac_sha1(&mut g); + benchmark_aead_aes_128_gcm(&mut g); + benchmark_context_setup(&mut g); g.finish(); } diff --git a/rtc-srtp/examples/srtp_micro.rs b/rtc-srtp/examples/srtp_micro.rs index a537d4c6..a56be269 100644 --- a/rtc-srtp/examples/srtp_micro.rs +++ b/rtc-srtp/examples/srtp_micro.rs @@ -10,6 +10,12 @@ use std::hint::black_box; +/// The built-in provider, for tests only. Library code never resolves a default: every public +/// constructor takes the provider from its caller. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + use bytes::BytesMut; use rtc_srtp::option::srtp_replay_protection; use rtc_srtp::{context::Context, protection_profile::ProtectionProfile}; @@ -27,6 +33,7 @@ fn new_ctx() -> Context { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), ) .unwrap() } @@ -41,6 +48,7 @@ fn new_gcm_ctx() -> Context { ProtectionProfile::AeadAes128Gcm, None, None, + test_crypto_provider().crypto(), ) .unwrap() } @@ -54,6 +62,7 @@ fn new_ctx_replay() -> Context { ProtectionProfile::Aes128CmHmacSha1_80, Some(srtp_replay_protection(128)), None, + test_crypto_provider().crypto(), ) .unwrap() } diff --git a/rtc-srtp/src/cipher/cipher_aead_aes_gcm.rs b/rtc-srtp/src/cipher/cipher_aead_aes_gcm.rs index 8d62d0f4..b0d87c36 100644 --- a/rtc-srtp/src/cipher/cipher_aead_aes_gcm.rs +++ b/rtc-srtp/src/cipher/cipher_aead_aes_gcm.rs @@ -1,8 +1,8 @@ use byteorder::{BigEndian, ByteOrder}; use bytes::BytesMut; -use ring::aead::{AES_128_GCM, AES_256_GCM, Aad, Algorithm, LessSafeKey, Nonce, UnboundKey}; +use crypto::{AeadAlgorithm, AeadCipher, RTCCrypto, SecretVec}; -use super::{Cipher, Kdf}; +use super::{Cipher, Kdf, crypto_error}; use crate::key_derivation::*; use crate::protection_profile::ProtectionProfile; use shared::{ @@ -14,19 +14,11 @@ pub const CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN: usize = 16; const RTCP_ENCRYPTION_FLAG: u8 = 0x80; -/// AEAD Cipher based on AES. -/// -/// The AES-GCM AEAD (both the AES block cipher and the GHASH universal hash) is -/// provided by `ring`, which ships hardware-accelerated single-pass assembly for -/// x86_64 (AES-NI + CLMUL) and aarch64 (ARMv8 AES + PMULL) with runtime feature -/// detection. That is materially faster than the pure-Rust RustCrypto `aes-gcm`, -/// whose two-pass (encrypt-then-GHASH) design and cfg-gated intrinsics leave it -/// on a software fallback in a default build. `ring` is already a workspace -/// dependency (DTLS handshake signatures, STUN integrity). +/// Provider-backed AEAD cipher based on AES-GCM. pub(crate) struct CipherAeadAesGcm { profile: ProtectionProfile, - srtp_cipher: LessSafeKey, - srtcp_cipher: LessSafeKey, + srtp_cipher: Box, + srtcp_cipher: Box, srtp_session_salt: Vec, srtcp_session_salt: Vec, } @@ -56,16 +48,12 @@ impl Cipher for CipherAeadAesGcm { writer.extend_from_slice(payload); let (aad, plaintext) = writer.split_at_mut(header_len); - let tag = self - .srtp_cipher - .seal_in_place_separate_tag( - Nonce::assume_unique_for_key(nonce), - Aad::from(&aad[..]), - plaintext, - ) - .map_err(|_| Error::Other("SRTP AES-GCM seal failed".to_string()))?; - - writer.extend_from_slice(tag.as_ref()); + let mut tag = [0; CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN]; + self.srtp_cipher + .seal_in_place(&nonce, aad, plaintext, &mut tag) + .map_err(crypto_error)?; + + writer.extend_from_slice(&tag); Ok(writer) } @@ -89,23 +77,15 @@ impl Cipher for CipherAeadAesGcm { let nonce = self.rtp_initialization_vector(header, roc); - // ring's `open_in_place` decrypts a contiguous ciphertext||tag region in - // place and returns the plaintext slice; the header stays as AAD. Copy - // the wire packet once, decrypt, then drop the trailing tag. - let mut writer = BytesMut::with_capacity(ciphertext.len()); - writer.extend_from_slice(ciphertext); - let final_len = writer.len() - tag_len; + let tag_offset = ciphertext.len() - tag_len; + let tag = &ciphertext[tag_offset..]; + let mut writer = BytesMut::with_capacity(tag_offset); + writer.extend_from_slice(&ciphertext[..tag_offset]); - let (aad, ct_and_tag) = writer.split_at_mut(payload_offset); + let (aad, encrypted_payload) = writer.split_at_mut(payload_offset); self.srtp_cipher - .open_in_place( - Nonce::assume_unique_for_key(nonce), - Aad::from(&aad[..]), - ct_and_tag, - ) + .open_in_place(&nonce, aad, encrypted_payload, tag) .map_err(|_| Error::ErrFailedToVerifyAuthTag)?; - - writer.truncate(final_len); Ok(writer) } @@ -122,16 +102,12 @@ impl Cipher for CipherAeadAesGcm { BytesMut::with_capacity(decrypted.len() + self.aead_auth_tag_len() + SRTCP_INDEX_SIZE); writer.extend_from_slice(decrypted); - let tag = self - .srtcp_cipher - .seal_in_place_separate_tag( - Nonce::assume_unique_for_key(iv), - Aad::from(&aad[..]), - &mut writer[8..], - ) - .map_err(|_| Error::Other("SRTCP AES-GCM seal failed".to_string()))?; + let mut tag = [0; CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN]; + self.srtcp_cipher + .seal_in_place(&iv, &aad, &mut writer[8..], &mut tag) + .map_err(crypto_error)?; - writer.extend_from_slice(tag.as_ref()); + writer.extend_from_slice(&tag); writer.extend_from_slice(&aad[8..]); Ok(writer) @@ -157,23 +133,16 @@ impl Cipher for CipherAeadAesGcm { return Err(Error::ErrFailedToVerifyAuthTag); } - // Copy header(8) || ciphertext || tag (dropping the trailing ESRTCP index - // word), decrypt the ciphertext+tag region in place, then drop the tag. - let mut writer = BytesMut::with_capacity(tag_start + tag_len); - writer.extend_from_slice(&encrypted[..tag_start + tag_len]); + let tag = &encrypted[tag_start..tag_start + tag_len]; + let mut writer = BytesMut::with_capacity(tag_start); + writer.extend_from_slice(&encrypted[..tag_start]); { - let (_, ct_and_tag) = writer.split_at_mut(8); + let (_, encrypted_payload) = writer.split_at_mut(8); self.srtcp_cipher - .open_in_place( - Nonce::assume_unique_for_key(nonce), - Aad::from(&aad[..]), - ct_and_tag, - ) + .open_in_place(&nonce, &aad, encrypted_payload, tag) .map_err(|_| Error::ErrFailedToVerifyAuthTag)?; } - - writer.truncate(tag_start); Ok(writer) } @@ -191,11 +160,14 @@ impl CipherAeadAesGcm { profile: ProtectionProfile, master_key: &[u8], master_salt: &[u8], + crypto: &dyn RTCCrypto, ) -> Result { - let (algorithm, kdf): (&'static Algorithm, Kdf) = match profile { - ProtectionProfile::AeadAes128Gcm => (&AES_128_GCM, aes_cm_key_derivation), + let (algorithm, kdf): (AeadAlgorithm, Kdf) = match profile { + ProtectionProfile::AeadAes128Gcm => (AeadAlgorithm::Aes128Gcm, aes_cm_key_derivation), // AES_256_GCM must use AES_256_CM_PRF as per https://datatracker.ietf.org/doc/html/rfc7714#section-11 - ProtectionProfile::AeadAes256Gcm => (&AES_256_GCM, aes_256_cm_key_derivation), + ProtectionProfile::AeadAes256Gcm => { + (AeadAlgorithm::Aes256Gcm, aes_256_cm_key_derivation) + } _ => unreachable!(), }; @@ -205,17 +177,33 @@ impl CipherAeadAesGcm { ); assert_eq!(profile.salt_len(), master_salt.len()); - let build_cipher = |label: u8| -> Result { - let session_key = kdf(label, master_key, master_salt, 0, master_key.len())?; - let unbound = UnboundKey::new(algorithm, &session_key) - .map_err(|_| Error::Other("invalid SRTP AES-GCM session key".to_string()))?; - Ok(LessSafeKey::new(unbound)) + let build_cipher = |label: u8| -> Result> { + let session_key = SecretVec::new(kdf( + crypto, + label, + master_key, + master_salt, + 0, + master_key.len(), + )?); + let cipher = crypto + .new_aead(algorithm, session_key.as_ref()) + .map_err(crypto_error)?; + if cipher.tag_len() != CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN { + return Err(Error::Crypto(format!( + "SRTP AES-GCM requires a {}-byte tag, provider returned {}", + CIPHER_AEAD_AES_GCM_AUTH_TAG_LEN, + cipher.tag_len() + ))); + } + Ok(cipher) }; let srtp_cipher = build_cipher(LABEL_SRTP_ENCRYPTION)?; let srtcp_cipher = build_cipher(LABEL_SRTCP_ENCRYPTION)?; let srtp_session_salt = kdf( + crypto, LABEL_SRTP_SALT, master_key, master_salt, @@ -224,6 +212,7 @@ impl CipherAeadAesGcm { )?; let srtcp_session_salt = kdf( + crypto, LABEL_SRTCP_SALT, master_key, master_salt, @@ -310,7 +299,9 @@ mod tests { let master_key = vec![0u8; profile.key_len()]; let master_salt = vec![0u8; 12]; - let mut cipher = CipherAeadAesGcm::new(profile, &master_key, &master_salt).unwrap(); + let provider = crypto::default_provider().unwrap(); + let mut cipher = + CipherAeadAesGcm::new(profile, &master_key, &master_salt, provider.crypto()).unwrap(); let header = rtp::Header { ssrc: 0x12345678, @@ -332,7 +323,9 @@ mod tests { let master_key = vec![0u8; profile.key_len()]; let master_salt = vec![0u8; 12]; - let mut cipher = CipherAeadAesGcm::new(profile, &master_key, &master_salt).unwrap(); + let provider = crypto::default_provider().unwrap(); + let mut cipher = + CipherAeadAesGcm::new(profile, &master_key, &master_salt, provider.crypto()).unwrap(); let header = rtp::Header { ssrc: 0x12345678, @@ -353,7 +346,9 @@ mod tests { let master_key = vec![0u8; profile.key_len()]; let master_salt = vec![0u8; 12]; - let mut cipher = CipherAeadAesGcm::new(profile, &master_key, &master_salt).unwrap(); + let provider = crypto::default_provider().unwrap(); + let mut cipher = + CipherAeadAesGcm::new(profile, &master_key, &master_salt, provider.crypto()).unwrap(); let header = rtp::Header { ssrc: 0x12345678, diff --git a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/ctrcipher.rs b/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs similarity index 50% rename from rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/ctrcipher.rs rename to rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs index 23296ab2..96db595c 100644 --- a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/ctrcipher.rs +++ b/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1.rs @@ -1,33 +1,43 @@ -use aes::cipher::generic_array::GenericArray; -use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}; +use byteorder::{BigEndian, ByteOrder}; use bytes::{BufMut, BytesMut}; +use crypto::{ + HmacAlgorithm, Mac, RTCCrypto, SecretVec, StreamCipher, StreamCipherAlgorithm, constant_time_eq, +}; use rtcp::header::{HEADER_LENGTH, SSRC_LENGTH}; use shared::marshal::*; -use subtle::ConstantTimeEq; -use super::{Cipher, CipherInner}; -use crate::cipher::Kdf; +use super::{Cipher, Kdf, crypto_error}; use crate::key_derivation::*; -use crate::protection_profile::ProtectionProfile; +use crate::protection_profile::*; use shared::error::{Error, Result}; -type Aes128Ctr = ctr::Ctr128BE; -type Aes256Ctr = ctr::Ctr128BE; +pub const CIPHER_AES_CM_HMAC_SHA1AUTH_TAG_LEN: usize = 10; pub(crate) struct CipherAesCmHmacSha1 { - inner: CipherInner, - srtp_session_key: Vec, - srtcp_session_key: Vec, + profile: ProtectionProfile, + srtp_session_salt: Vec, + /// Pre-keyed HMAC-SHA1, built once here rather than per packet. Deriving the key schedule on + /// every packet measured ~3x slower on the SRTP path; see `benches/README.md`. + srtp_session_auth: Box, + srtcp_session_salt: Vec, + srtcp_session_auth: Box, + srtp_cipher: Box, + srtcp_cipher: Box, } impl CipherAesCmHmacSha1 { - pub fn new(profile: ProtectionProfile, master_key: &[u8], master_salt: &[u8]) -> Result { - let kdf: Kdf = match profile { + pub fn new( + profile: ProtectionProfile, + master_key: &[u8], + master_salt: &[u8], + crypto: &dyn RTCCrypto, + ) -> Result { + let (kdf, algorithm): (Kdf, StreamCipherAlgorithm) = match profile { ProtectionProfile::Aes128CmHmacSha1_32 | ProtectionProfile::Aes128CmHmacSha1_80 => { - aes_cm_key_derivation + (aes_cm_key_derivation, StreamCipherAlgorithm::Aes128Ctr) } ProtectionProfile::Aes256CmHmacSha1_80 | ProtectionProfile::Aes256CmHmacSha1_32 => { - aes_256_cm_key_derivation + (aes_256_cm_key_derivation, StreamCipherAlgorithm::Aes256Ctr) } _ => { return Err(Error::Other(String::from( @@ -35,49 +45,125 @@ impl CipherAesCmHmacSha1 { ))); } }; - let inner = CipherInner::new(profile, kdf, master_key, master_salt)?; - - let srtp_session_key = kdf( + let srtp_session_key = SecretVec::new(kdf( + crypto, LABEL_SRTP_ENCRYPTION, master_key, master_salt, 0, master_key.len(), - )?; - let srtcp_session_key = kdf( + )?); + let srtcp_session_key = SecretVec::new(kdf( + crypto, LABEL_SRTCP_ENCRYPTION, master_key, master_salt, 0, master_key.len(), + )?); + + let srtp_cipher = crypto + .new_stream_cipher(algorithm, srtp_session_key.as_ref()) + .map_err(crypto_error)?; + let srtcp_cipher = crypto + .new_stream_cipher(algorithm, srtcp_session_key.as_ref()) + .map_err(crypto_error)?; + let srtp_session_salt = kdf( + crypto, + LABEL_SRTP_SALT, + master_key, + master_salt, + 0, + master_salt.len(), )?; - - Ok(CipherAesCmHmacSha1 { - inner, - srtp_session_key, - srtcp_session_key, + let srtcp_session_salt = kdf( + crypto, + LABEL_SRTCP_SALT, + master_key, + master_salt, + 0, + master_salt.len(), + )?; + let auth_key_len = profile.auth_key_len(); + let srtp_session_auth = SecretVec::new(kdf( + crypto, + LABEL_SRTP_AUTHENTICATION_TAG, + master_key, + master_salt, + 0, + auth_key_len, + )?); + let srtcp_session_auth = SecretVec::new(kdf( + crypto, + LABEL_SRTCP_AUTHENTICATION_TAG, + master_key, + master_salt, + 0, + auth_key_len, + )?); + + // Key the MACs once per context. Everything above is per-context setup; the auth tag on + // each packet then costs only the message pass. + let srtp_session_auth = crypto + .new_hmac(HmacAlgorithm::Sha1, srtp_session_auth.as_ref()) + .map_err(crypto_error)?; + let srtcp_session_auth = crypto + .new_hmac(HmacAlgorithm::Sha1, srtcp_session_auth.as_ref()) + .map_err(crypto_error)?; + + Ok(Self { + profile, + srtp_session_salt, + srtp_session_auth, + srtcp_session_salt, + srtcp_session_auth, + srtp_cipher, + srtcp_cipher, }) } + + /// Generate the SRTP HMAC-SHA1 authentication tag described by RFC 3711, section 4.2. + /// + /// Takes `&mut self` so the pre-keyed [`Mac`] can be used directly; the key schedule was + /// derived once in [`new`](Self::new). + fn generate_srtp_auth_tag(&mut self, buf: &[u8], roc: u32) -> Result<[u8; 20]> { + let mut tag = [0; 20]; + self.srtp_session_auth + .sign(&[buf, &roc.to_be_bytes()], &mut tag) + .map_err(crypto_error)?; + Ok(tag) + } + + /// Generate the SRTCP HMAC-SHA1 authentication tag described by RFC 3711, section 4.2. + fn generate_srtcp_auth_tag(&mut self, buf: &[u8]) -> Result<[u8; 20]> { + let mut tag = [0; 20]; + self.srtcp_session_auth + .sign(&[buf], &mut tag) + .map_err(crypto_error)?; + Ok(tag) + } } impl Cipher for CipherAesCmHmacSha1 { /// Get RTP authenticated tag length. fn rtp_auth_tag_len(&self) -> usize { - self.inner.profile.rtp_auth_tag_len() + self.profile.rtp_auth_tag_len() } /// Get RTCP authenticated tag length. fn rtcp_auth_tag_len(&self) -> usize { - self.inner.profile.rtcp_auth_tag_len() + self.profile.rtcp_auth_tag_len() } /// Get AEAD auth key length of the cipher. fn aead_auth_tag_len(&self) -> usize { - self.inner.profile.aead_auth_tag_len() + self.profile.aead_auth_tag_len() } fn get_rtcp_index(&self, input: &[u8]) -> usize { - self.inner.get_rtcp_index(input) + let tail_offset = input.len() - (self.profile.rtcp_auth_tag_len() + SRTCP_INDEX_SIZE); + (BigEndian::read_u32(&input[tail_offset..tail_offset + SRTCP_INDEX_SIZE]) & !(1 << 31)) + as usize } fn encrypt_rtp( @@ -96,23 +182,16 @@ impl Cipher for CipherAesCmHmacSha1 { header.sequence_number, roc, header.ssrc, - &self.inner.srtp_session_salt, + &self.srtp_session_salt, ); - if self.inner.profile.key_len() == 16 { - let key = GenericArray::from_slice(&self.srtp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes128Ctr::new(key, nonce); - stream.apply_keystream(&mut writer[header.marshal_size()..]); - } else { - let key = GenericArray::from_slice(&self.srtp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes256Ctr::new(key, nonce); - stream.apply_keystream(&mut writer[header.marshal_size()..]); - } + self.srtp_cipher + .apply_keystream(&counter, &mut writer[header.marshal_size()..]) + .map_err(crypto_error)?; // Generate the auth tag. - let auth_tag = &self.inner.generate_srtp_auth_tag(&writer, roc)[..self.rtp_auth_tag_len()]; + let full_auth_tag = self.generate_srtp_auth_tag(&writer, roc)?; + let auth_tag = &full_auth_tag[..self.rtp_auth_tag_len()]; writer.extend_from_slice(auth_tag); Ok(writer) @@ -136,12 +215,12 @@ impl Cipher for CipherAesCmHmacSha1 { let cipher_text = &encrypted[..encrypted_len - self.rtp_auth_tag_len()]; // Generate the auth tag we expect to see from the ciphertext. - let expected_tag = - &self.inner.generate_srtp_auth_tag(cipher_text, roc)[..self.rtp_auth_tag_len()]; + let full_expected_tag = self.generate_srtp_auth_tag(cipher_text, roc)?; + let expected_tag = &full_expected_tag[..self.rtp_auth_tag_len()]; // See if the auth tag actually matches. // We use a constant time comparison to prevent timing attacks. - if actual_tag.ct_eq(expected_tag).unwrap_u8() != 1 { + if !constant_time_eq(actual_tag, expected_tag) { return Err(Error::RtpFailedToVerifyAuthTag); } @@ -153,22 +232,12 @@ impl Cipher for CipherAesCmHmacSha1 { header.sequence_number, roc, header.ssrc, - &self.inner.srtp_session_salt, + &self.srtp_session_salt, ); - if self.inner.profile.key_len() == 16 { - let key = GenericArray::from_slice(&self.srtp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes128Ctr::new(key, nonce); - stream.seek(0); - stream.apply_keystream(&mut writer[header.marshal_size()..]); - } else { - let key = GenericArray::from_slice(&self.srtp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes256Ctr::new(key, nonce); - stream.seek(0); - stream.apply_keystream(&mut writer[header.marshal_size()..]); - } + self.srtp_cipher + .apply_keystream(&counter, &mut writer[header.marshal_size()..]) + .map_err(crypto_error)?; Ok(writer) } @@ -190,26 +259,19 @@ impl Cipher for CipherAesCmHmacSha1 { (srtcp_index & 0xFFFF) as u16, (srtcp_index >> 16) as u32, ssrc, - &self.inner.srtcp_session_salt, + &self.srtcp_session_salt, ); - if self.inner.profile.key_len() == 16 { - let key = GenericArray::from_slice(&self.srtcp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes128Ctr::new(key, nonce); - stream.apply_keystream(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]); - } else { - let key = GenericArray::from_slice(&self.srtcp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes256Ctr::new(key, nonce); - stream.apply_keystream(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]); - } + self.srtcp_cipher + .apply_keystream(&counter, &mut writer[HEADER_LENGTH + SSRC_LENGTH..]) + .map_err(crypto_error)?; // Add SRTCP index and set Encryption bit writer.put_u32(srtcp_index as u32 | (1u32 << 31)); // Generate the auth tag. - let auth_tag = &self.inner.generate_srtcp_auth_tag(&writer)[..self.rtcp_auth_tag_len()]; + let full_auth_tag = self.generate_srtcp_auth_tag(&writer)?; + let auth_tag = &full_auth_tag[..self.rtcp_auth_tag_len()]; writer.extend_from_slice(auth_tag); Ok(writer) @@ -255,12 +317,12 @@ impl Cipher for CipherAesCmHmacSha1 { let cipher_text = &encrypted[..encrypted_len - self.rtcp_auth_tag_len()]; // Generate the auth tag we expect to see from the ciphertext. - let expected_tag = - &self.inner.generate_srtcp_auth_tag(cipher_text)[..self.rtcp_auth_tag_len()]; + let full_expected_tag = self.generate_srtcp_auth_tag(cipher_text)?; + let expected_tag = &full_expected_tag[..self.rtcp_auth_tag_len()]; // See if the auth tag actually matches. // We use a constant time comparison to prevent timing attacks. - if actual_tag.ct_eq(expected_tag).unwrap_u8() != 1 { + if !constant_time_eq(actual_tag, expected_tag) { return Err(Error::RtcpFailedToVerifyAuthTag); } @@ -268,22 +330,12 @@ impl Cipher for CipherAesCmHmacSha1 { (srtcp_index & 0xFFFF) as u16, (srtcp_index >> 16) as u32, ssrc, - &self.inner.srtcp_session_salt, + &self.srtcp_session_salt, ); - if self.inner.profile.key_len() == 16 { - let key = GenericArray::from_slice(&self.srtcp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes128Ctr::new(key, nonce); - stream.seek(0); - stream.apply_keystream(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]); - } else { - let key = GenericArray::from_slice(&self.srtcp_session_key); - let nonce = GenericArray::from_slice(&counter); - let mut stream = Aes256Ctr::new(key, nonce); - stream.seek(0); - stream.apply_keystream(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]); - } + self.srtcp_cipher + .apply_keystream(&counter, &mut writer[HEADER_LENGTH + SSRC_LENGTH..]) + .map_err(crypto_error)?; Ok(writer) } diff --git a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/mod.rs b/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/mod.rs deleted file mode 100644 index 40376b17..00000000 --- a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/mod.rs +++ /dev/null @@ -1,135 +0,0 @@ -use byteorder::{BigEndian, ByteOrder}; -use hmac::{Hmac, Mac}; -use sha1::Sha1; - -use super::{Cipher, Kdf}; -use crate::key_derivation::*; -use crate::protection_profile::*; -use shared::error::{Error, Result}; - -#[cfg(not(feature = "openssl"))] -mod ctrcipher; - -#[cfg(feature = "openssl")] -mod opensslcipher; - -#[cfg(not(feature = "openssl"))] -pub(crate) use ctrcipher::CipherAesCmHmacSha1; - -#[cfg(feature = "openssl")] -pub(crate) use opensslcipher::CipherAesCmHmacSha1; - -type HmacSha1 = Hmac; - -pub const CIPHER_AES_CM_HMAC_SHA1AUTH_TAG_LEN: usize = 10; - -pub(crate) struct CipherInner { - profile: ProtectionProfile, - srtp_session_salt: Vec, - srtp_session_auth: HmacSha1, - srtcp_session_salt: Vec, - srtcp_session_auth: HmacSha1, -} - -impl CipherInner { - pub fn new( - profile: ProtectionProfile, - kdf: Kdf, - master_key: &[u8], - master_salt: &[u8], - ) -> Result { - let srtp_session_salt = kdf( - LABEL_SRTP_SALT, - master_key, - master_salt, - 0, - master_salt.len(), - )?; - let srtcp_session_salt = kdf( - LABEL_SRTCP_SALT, - master_key, - master_salt, - 0, - master_salt.len(), - )?; - - let auth_key_len = profile.auth_key_len(); - let srtp_session_auth_tag = kdf( - LABEL_SRTP_AUTHENTICATION_TAG, - master_key, - master_salt, - 0, - auth_key_len, - )?; - let srtcp_session_auth_tag = kdf( - LABEL_SRTCP_AUTHENTICATION_TAG, - master_key, - master_salt, - 0, - auth_key_len, - )?; - - let srtp_session_auth = HmacSha1::new_from_slice(&srtp_session_auth_tag) - .map_err(|e| Error::Other(e.to_string()))?; - let srtcp_session_auth = HmacSha1::new_from_slice(&srtcp_session_auth_tag) - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(Self { - profile, - srtp_session_salt, - srtp_session_auth, - srtcp_session_salt, - srtcp_session_auth, - }) - } - - /// https://tools.ietf.org/html/rfc3711#section-4.2 - /// In the case of SRTP, M SHALL consist of the Authenticated - /// Portion of the packet (as specified in Figure 1) concatenated with - /// the roc, M = Authenticated Portion || roc; - /// - /// The pre-defined authentication transform for SRTP is HMAC-SHA1 - /// [RFC2104]. With HMAC-SHA1, the SRTP_PREFIX_LENGTH (Figure 3) SHALL - /// be 0. For SRTP (respectively SRTCP), the HMAC SHALL be applied to - /// the session authentication key and M as specified above, i.e., - /// HMAC(k_a, M). The HMAC output SHALL then be truncated to the n_tag - /// left-most bits. - /// - Authenticated portion of the packet is everything BEFORE MKI - /// - k_a is the session message authentication key - /// - n_tag is the bit-length of the output authentication tag - fn generate_srtp_auth_tag(&self, buf: &[u8], roc: u32) -> [u8; 20] { - let mut signer = self.srtp_session_auth.clone(); - - signer.update(buf); - - // For SRTP only, we need to hash the rollover counter as well. - signer.update(&roc.to_be_bytes()); - - signer.finalize().into_bytes().into() - } - - /// https://tools.ietf.org/html/rfc3711#section-4.2 - /// - /// The pre-defined authentication transform for SRTP is HMAC-SHA1 - /// [RFC2104]. With HMAC-SHA1, the SRTP_PREFIX_LENGTH (Figure 3) SHALL - /// be 0. For SRTP (respectively SRTCP), the HMAC SHALL be applied to - /// the session authentication key and M as specified above, i.e., - /// HMAC(k_a, M). The HMAC output SHALL then be truncated to the n_tag - /// left-most bits. - /// - Authenticated portion of the packet is everything BEFORE MKI - /// - k_a is the session message authentication key - /// - n_tag is the bit-length of the output authentication tag - fn generate_srtcp_auth_tag(&self, buf: &[u8]) -> [u8; 20] { - let mut signer = self.srtcp_session_auth.clone(); - - signer.update(buf); - - signer.finalize().into_bytes().into() - } - - fn get_rtcp_index(&self, input: &[u8]) -> usize { - let tail_offset = input.len() - (self.profile.rtcp_auth_tag_len() + SRTCP_INDEX_SIZE); - (BigEndian::read_u32(&input[tail_offset..tail_offset + SRTCP_INDEX_SIZE]) & !(1 << 31)) - as usize - } -} diff --git a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/opensslcipher.rs b/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/opensslcipher.rs deleted file mode 100644 index f767f109..00000000 --- a/rtc-srtp/src/cipher/cipher_aes_cm_hmac_sha1/opensslcipher.rs +++ /dev/null @@ -1,308 +0,0 @@ -use super::{Cipher, CipherInner}; -use crate::cipher::Kdf; -use crate::key_derivation::*; -use crate::protection_profile::ProtectionProfile; -use bytes::{BufMut, BytesMut}; -use openssl::cipher_ctx::CipherCtx; -use rtcp::header::{HEADER_LENGTH, SSRC_LENGTH}; -use shared::error::{Error, Result}; -use shared::marshal::*; -use subtle::ConstantTimeEq; - -pub(crate) struct CipherAesCmHmacSha1 { - inner: CipherInner, - rtp_ctx: CipherCtx, - rtcp_ctx: CipherCtx, -} - -impl CipherAesCmHmacSha1 { - pub fn new(profile: ProtectionProfile, master_key: &[u8], master_salt: &[u8]) -> Result { - let kdf: Kdf = match profile { - ProtectionProfile::Aes128CmHmacSha1_32 | ProtectionProfile::Aes128CmHmacSha1_80 => { - aes_cm_key_derivation - } - ProtectionProfile::Aes256CmHmacSha1_80 | ProtectionProfile::Aes256CmHmacSha1_32 => { - aes_256_cm_key_derivation - } - _ => { - return Err(Error::Other(String::from( - "no AES protection profile passed to CipherAesCmHmacSha1", - ))); - } - }; - - let inner = CipherInner::new(profile, kdf, master_key, master_salt)?; - - let srtp_session_key = kdf( - LABEL_SRTP_ENCRYPTION, - master_key, - master_salt, - 0, - master_key.len(), - )?; - let srtcp_session_key = kdf( - LABEL_SRTCP_ENCRYPTION, - master_key, - master_salt, - 0, - master_key.len(), - )?; - - let t = if profile.key_len() == 16 { - openssl::cipher::Cipher::aes_128_ctr() - } else { - openssl::cipher::Cipher::aes_256_ctr() - }; - let mut rtp_ctx = CipherCtx::new().map_err(|e| Error::Other(e.to_string()))?; - rtp_ctx - .encrypt_init(Some(t), Some(&srtp_session_key[..]), None) - .map_err(|e| Error::Other(e.to_string()))?; - - let t = if profile.key_len() == 16 { - openssl::cipher::Cipher::aes_128_ctr() - } else { - openssl::cipher::Cipher::aes_256_ctr() - }; - let mut rtcp_ctx = CipherCtx::new().map_err(|e| Error::Other(e.to_string()))?; - rtcp_ctx - .encrypt_init(Some(t), Some(&srtcp_session_key[..]), None) - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(Self { - inner, - rtp_ctx, - rtcp_ctx, - }) - } -} - -impl Cipher for CipherAesCmHmacSha1 { - /// Get RTP authenticated tag length. - fn rtp_auth_tag_len(&self) -> usize { - self.inner.profile.rtp_auth_tag_len() - } - - /// Get RTCP authenticated tag length. - fn rtcp_auth_tag_len(&self) -> usize { - self.inner.profile.rtcp_auth_tag_len() - } - - /// Get AEAD auth key length of the cipher. - fn aead_auth_tag_len(&self) -> usize { - self.inner.profile.aead_auth_tag_len() - } - - fn get_rtcp_index(&self, input: &[u8]) -> usize { - self.inner.get_rtcp_index(input) - } - - fn encrypt_rtp( - &mut self, - plaintext: &[u8], - header: &rtp::Header, - roc: u32, - ) -> Result { - let header_len = header.marshal_size(); - let mut writer = BytesMut::with_capacity(plaintext.len() + self.rtp_auth_tag_len()); - - // Copy the header unencrypted. - writer.extend_from_slice(&plaintext[..header_len]); - - // Encrypt the payload - let nonce = generate_counter( - header.sequence_number, - roc, - header.ssrc, - &self.inner.srtp_session_salt, - ); - writer.resize(plaintext.len(), 0); - self.rtp_ctx.encrypt_init(None, None, Some(&nonce)).unwrap(); - let count = self - .rtp_ctx - .cipher_update(&plaintext[header_len..], Some(&mut writer[header_len..])) - .unwrap(); - self.rtp_ctx - .cipher_final(&mut writer[header_len + count..]) - .unwrap(); - - // Generate and write the auth tag. - let auth_tag = &self.inner.generate_srtp_auth_tag(&writer, roc)[..self.rtp_auth_tag_len()]; - writer.extend_from_slice(auth_tag); - - Ok(writer) - } - - fn decrypt_rtp( - &mut self, - encrypted: &[u8], - header: &rtp::Header, - roc: u32, - ) -> Result { - let encrypted_len = encrypted.len(); - if encrypted_len < self.rtp_auth_tag_len() { - return Err(Error::SrtpTooSmall(encrypted_len, self.rtp_auth_tag_len())); - } - let header_len = header.marshal_size(); - - let mut writer = BytesMut::with_capacity(encrypted_len - self.rtp_auth_tag_len()); - - // Split the auth tag and the cipher text into two parts. - let actual_tag = &encrypted[encrypted_len - self.rtp_auth_tag_len()..]; - let cipher_text = &encrypted[..encrypted_len - self.rtp_auth_tag_len()]; - - // Generate the auth tag we expect to see from the ciphertext. - let expected_tag = - &self.inner.generate_srtp_auth_tag(cipher_text, roc)[..self.rtp_auth_tag_len()]; - - // See if the auth tag actually matches. - // We use a constant time comparison to prevent timing attacks. - if actual_tag.ct_eq(expected_tag).unwrap_u8() != 1 { - return Err(Error::RtpFailedToVerifyAuthTag); - } - - // Write cipher_text to the destination buffer. - writer.extend_from_slice(&cipher_text[..header_len]); - - // Decrypt the ciphertext for the payload. - let nonce = generate_counter( - header.sequence_number, - roc, - header.ssrc, - &self.inner.srtp_session_salt, - ); - - writer.resize(encrypted_len - self.rtp_auth_tag_len(), 0); - self.rtp_ctx.decrypt_init(None, None, Some(&nonce)).unwrap(); - let count = self - .rtp_ctx - .cipher_update(&cipher_text[header_len..], Some(&mut writer[header_len..])) - .unwrap(); - self.rtp_ctx - .cipher_final(&mut writer[header_len + count..]) - .unwrap(); - - Ok(writer) - } - - fn encrypt_rtcp( - &mut self, - decrypted: &[u8], - srtcp_index: usize, - ssrc: u32, - ) -> Result { - let decrypted_len = decrypted.len(); - - let mut writer = - BytesMut::with_capacity(decrypted_len + SRTCP_INDEX_SIZE + self.rtcp_auth_tag_len()); - - // Write the decrypted to the destination buffer. - writer.extend_from_slice(&decrypted[..HEADER_LENGTH + SSRC_LENGTH]); - - // Encrypt everything after header - let nonce = generate_counter( - (srtcp_index & 0xFFFF) as u16, - (srtcp_index >> 16) as u32, - ssrc, - &self.inner.srtcp_session_salt, - ); - - writer.resize(decrypted_len, 0); - self.rtcp_ctx - .encrypt_init(None, None, Some(&nonce)) - .unwrap(); - let count = self - .rtcp_ctx - .cipher_update( - &decrypted[HEADER_LENGTH + SSRC_LENGTH..], - Some(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]), - ) - .unwrap(); - self.rtcp_ctx - .cipher_final(&mut writer[HEADER_LENGTH + SSRC_LENGTH + count..]) - .unwrap(); - - // Add SRTCP index and set Encryption bit - writer.put_u32(srtcp_index as u32 | (1u32 << 31)); - - // Generate the auth tag. - let auth_tag = &self.inner.generate_srtcp_auth_tag(&writer)[..self.rtcp_auth_tag_len()]; - writer.extend_from_slice(auth_tag); - - Ok(writer) - } - - fn decrypt_rtcp( - &mut self, - encrypted: &[u8], - srtcp_index: usize, - ssrc: u32, - ) -> Result { - let encrypted_len = encrypted.len(); - - if encrypted_len < self.rtcp_auth_tag_len() + SRTCP_INDEX_SIZE { - return Err(Error::SrtcpTooSmall( - encrypted_len, - self.rtcp_auth_tag_len() + SRTCP_INDEX_SIZE, - )); - } - - let tail_offset = encrypted_len - (self.rtcp_auth_tag_len() + SRTCP_INDEX_SIZE); - if tail_offset < 8 { - return Err(Error::ErrTooShortRtcp); - } - - let mut writer = BytesMut::with_capacity(tail_offset); - - writer.extend_from_slice(&encrypted[..HEADER_LENGTH + SSRC_LENGTH]); - - let is_encrypted = encrypted[tail_offset] >> 7; - if is_encrypted == 0 { - return Ok(writer); - } - - // Split the auth tag and the cipher text into two parts. - let actual_tag = &encrypted[encrypted_len - self.rtcp_auth_tag_len()..]; - if actual_tag.len() != self.rtcp_auth_tag_len() { - return Err(Error::RtcpInvalidLengthAuthTag( - actual_tag.len(), - self.rtcp_auth_tag_len(), - )); - } - - let cipher_text = &encrypted[..encrypted_len - self.rtcp_auth_tag_len()]; - - // Generate the auth tag we expect to see from the ciphertext. - let expected_tag = - &self.inner.generate_srtcp_auth_tag(cipher_text)[..self.rtcp_auth_tag_len()]; - - // See if the auth tag actually matches. - // We use a constant time comparison to prevent timing attacks. - if actual_tag.ct_eq(expected_tag).unwrap_u8() != 1 { - return Err(Error::RtcpFailedToVerifyAuthTag); - } - - let nonce = generate_counter( - (srtcp_index & 0xFFFF) as u16, - (srtcp_index >> 16) as u32, - ssrc, - &self.inner.srtcp_session_salt, - ); - - writer.resize(tail_offset, 0); - self.rtcp_ctx - .decrypt_init(None, None, Some(&nonce)) - .unwrap(); - let count = self - .rtcp_ctx - .cipher_update( - &encrypted[HEADER_LENGTH + SSRC_LENGTH..tail_offset], - Some(&mut writer[HEADER_LENGTH + SSRC_LENGTH..]), - ) - .unwrap(); - self.rtcp_ctx - .cipher_final(&mut writer[HEADER_LENGTH + SSRC_LENGTH + count..]) - .unwrap(); - - Ok(writer) - } -} diff --git a/rtc-srtp/src/cipher/mod.rs b/rtc-srtp/src/cipher/mod.rs index ed21ff48..49629315 100644 --- a/rtc-srtp/src/cipher/mod.rs +++ b/rtc-srtp/src/cipher/mod.rs @@ -2,10 +2,15 @@ pub mod cipher_aead_aes_gcm; pub mod cipher_aes_cm_hmac_sha1; use bytes::BytesMut; +use crypto::{CryptoError, RTCCrypto}; -use shared::error::Result; +use shared::error::{Error, Result}; -type Kdf = fn(u8, &[u8], &[u8], usize, usize) -> Result>; +type Kdf = fn(&dyn RTCCrypto, u8, &[u8], &[u8], usize, usize) -> Result>; + +pub(crate) fn crypto_error(error: CryptoError) -> Error { + Error::Crypto(error.to_string()) +} ///NOTE: Auth tag and AEAD auth tag are placed at the different position in SRTCP /// @@ -32,7 +37,7 @@ type Kdf = fn(u8, &[u8], &[u8], usize, usize) -> Result>; /// /// Cipher represents a implementation of one /// of the SRTP Specific ciphers. -pub(crate) trait Cipher: Send + Sync { +pub(crate) trait Cipher: Send { /// Get RTP authenticated tag length. fn rtp_auth_tag_len(&self) -> usize; diff --git a/rtc-srtp/src/config.rs b/rtc-srtp/src/config.rs index 2084c6ed..a4c5c7d1 100644 --- a/rtc-srtp/src/config.rs +++ b/rtc-srtp/src/config.rs @@ -1,54 +1,77 @@ use crate::{option::*, protection_profile::*}; -use shared::{crypto::KeyingMaterialExporter, error::Result}; +use shared::error::{Error, Result}; -const LABEL_EXTRACTOR_DTLS_SRTP: &str = "EXTRACTOR-dtls_srtp"; +/// RFC 5764 exporter label used to derive SRTP master keys and salts from DTLS. +pub const LABEL_EXTRACTOR_DTLS_SRTP: &str = "EXTRACTOR-dtls_srtp"; /// SessionKeys bundles the keys required to setup an SRTP session #[derive(Default, Debug, Clone)] pub struct SessionKeys { + /// The master key used to protect outbound packets. pub local_master_key: Vec, + /// The master salt used to protect outbound packets. pub local_master_salt: Vec, + /// The master key used to unprotect inbound packets. pub remote_master_key: Vec, + /// The master salt used to unprotect inbound packets. pub remote_master_salt: Vec, } /// Config is used to configure a session. -/// You can provide either a KeyingMaterialExporter to export keys -/// or directly pass the keys themselves. +/// The top-level integration exports keying material from DTLS and installs it here, or callers +/// can directly pass the keys themselves. /// After a Config is passed to a session it must not be modified. #[derive(Default)] pub struct Config { + /// The master keys and salts for both directions. pub keys: SessionKeys, + /// The negotiated protection profile, which fixes the cipher and tag lengths. pub profile: ProtectionProfile, //LoggerFactory: logging.LoggerFactory /// List of local/remote context options. /// ReplayProtection is enabled on remote context by default. /// Default replay protection window size is 64. pub local_rtp_options: Option, + /// Replay-protection options for the inbound RTP context. Enabled by default. pub remote_rtp_options: Option, + /// Replay-protection options for the outbound RTCP context. pub local_rtcp_options: Option, + /// Replay-protection options for the inbound RTCP context. Enabled by default. pub remote_rtcp_options: Option, } impl Config { - /// ExtractSessionKeysFromDTLS allows setting the Config SessionKeys by - /// extracting them from DTLS. This behavior is defined in RFC5764: - /// - pub fn extract_session_keys_from_dtls( + /// Returns the exact number of DTLS exporter bytes required by this profile. + #[must_use] + pub fn keying_material_len(&self) -> usize { + let key_len = self.profile.key_len(); + let salt_len = self.profile.salt_len(); + (key_len * 2) + (salt_len * 2) + } + + /// Splits DTLS-SRTP exporter output into local and remote master keys and salts according to + /// RFC 5764. + /// + /// # Errors + /// + /// Returns an error unless `keying_material` has exactly [`Self::keying_material_len`] bytes. + pub fn set_session_keys_from_keying_material( &mut self, - exporter: &impl KeyingMaterialExporter, + keying_material: &[u8], is_client: bool, ) -> Result<()> { + let expected = self.keying_material_len(); + if keying_material.len() != expected { + return Err(Error::Other(format!( + "invalid DTLS-SRTP keying material length: expected {expected}, got {}", + keying_material.len() + ))); + } + let key_len = self.profile.key_len(); let salt_len = self.profile.salt_len(); - let keying_material = exporter.export_keying_material( - LABEL_EXTRACTOR_DTLS_SRTP, - &[], - (key_len * 2) + (salt_len * 2), - )?; - let mut offset = 0; let client_write_key = keying_material[offset..offset + key_len].to_vec(); offset += key_len; @@ -76,3 +99,60 @@ impl Config { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn material(len: usize) -> Vec { + (0..len).map(|value| value as u8).collect() + } + + #[test] + fn rejects_keying_material_with_the_wrong_length() { + let mut config = Config { + profile: ProtectionProfile::Aes128CmHmacSha1_80, + ..Default::default() + }; + let expected = config.keying_material_len(); + + for actual in [expected - 1, expected + 1] { + let error = config + .set_session_keys_from_keying_material(&material(actual), true) + .unwrap_err(); + assert!( + error + .to_string() + .contains(&format!("expected {expected}, got {actual}")) + ); + } + } + + #[test] + fn assigns_client_and_server_material_by_role() -> Result<()> { + let mut client = Config { + profile: ProtectionProfile::Aes128CmHmacSha1_80, + ..Default::default() + }; + let bytes = material(client.keying_material_len()); + client.set_session_keys_from_keying_material(&bytes, true)?; + + let mut server = Config { + profile: client.profile, + ..Default::default() + }; + server.set_session_keys_from_keying_material(&bytes, false)?; + + assert_eq!(client.keys.local_master_key, server.keys.remote_master_key); + assert_eq!( + client.keys.local_master_salt, + server.keys.remote_master_salt + ); + assert_eq!(client.keys.remote_master_key, server.keys.local_master_key); + assert_eq!( + client.keys.remote_master_salt, + server.keys.local_master_salt + ); + Ok(()) + } +} diff --git a/rtc-srtp/src/context/context_test.rs b/rtc-srtp/src/context/context_test.rs index 8e2ef7ca..481d1f69 100644 --- a/rtc-srtp/src/context/context_test.rs +++ b/rtc-srtp/src/context/context_test.rs @@ -1,6 +1,12 @@ use super::*; use crate::key_derivation::*; +/// The built-in provider, for tests only. Library code never resolves a default: every public +/// constructor takes the provider from its caller. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + use bytes::Bytes; use lazy_static::lazy_static; @@ -18,6 +24,7 @@ fn test_context_roc() -> Result<()> { CIPHER_CONTEXT_ALGO, None, None, + test_crypto_provider().crypto(), )?; let roc = c.get_roc(123); @@ -45,6 +52,7 @@ fn test_context_index() -> Result<()> { CIPHER_CONTEXT_ALGO, None, None, + test_crypto_provider().crypto(), )?; let index = c.get_index(123); @@ -66,10 +74,24 @@ fn test_key_len() -> Result<()> { let key_len = CIPHER_CONTEXT_ALGO.key_len(); let salt_len = CIPHER_CONTEXT_ALGO.salt_len(); - let result = Context::new(&[], &vec![0; salt_len], CIPHER_CONTEXT_ALGO, None, None); + let result = Context::new( + &[], + &vec![0; salt_len], + CIPHER_CONTEXT_ALGO, + None, + None, + test_crypto_provider().crypto(), + ); assert!(result.is_err(), "CreateContext accepted a 0 length key"); - let result = Context::new(&vec![0; key_len], &[], CIPHER_CONTEXT_ALGO, None, None); + let result = Context::new( + &vec![0; key_len], + &[], + CIPHER_CONTEXT_ALGO, + None, + None, + test_crypto_provider().crypto(), + ); assert!(result.is_err(), "CreateContext accepted a 0 length salt"); let result = Context::new( @@ -78,6 +100,7 @@ fn test_key_len() -> Result<()> { CIPHER_CONTEXT_ALGO, None, None, + test_crypto_provider().crypto(), ); assert!( result.is_ok(), @@ -98,6 +121,9 @@ fn test_valid_packet_counter() -> Result<()> { ]; let srtp_session_salt = aes_cm_key_derivation( + crypto::default_provider() + .map_err(|error| Error::Crypto(error.to_string()))? + .crypto(), LABEL_SRTP_SALT, &master_key, &master_salt, @@ -340,6 +366,7 @@ fn test_encrypt_aead_aes_128_gcm_rtp() { ProtectionProfile::AeadAes128Gcm, None, None, + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -361,6 +388,7 @@ fn test_decrypt_aead_aes_128_gcm_rtp() { ProtectionProfile::AeadAes128Gcm, None, None, + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -379,6 +407,7 @@ fn test_encrypt_aead_aes_128_gcm_rtcp() { ProtectionProfile::AeadAes128Gcm, None, None, + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -400,6 +429,7 @@ fn test_decrypt_aead_aes_128_gcm_rtcp() { ProtectionProfile::AeadAes128Gcm, None, None, + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -418,6 +448,7 @@ fn test_encrypt_aes_256_cm_rtp() { ProtectionProfile::Aes256CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -439,6 +470,7 @@ fn test_decrypt_aes_256_cm_rtp() { ProtectionProfile::Aes256CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -457,6 +489,7 @@ fn test_encrypt_aes_256_cm_rtcp() { ProtectionProfile::Aes256CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); @@ -478,6 +511,7 @@ fn test_decrypt_aes_256_cm_rtcp() { ProtectionProfile::Aes256CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), ) .expect("Error creating srtp context"); diff --git a/rtc-srtp/src/context/mod.rs b/rtc-srtp/src/context/mod.rs index 78036fbf..d1df6acf 100644 --- a/rtc-srtp/src/context/mod.rs +++ b/rtc-srtp/src/context/mod.rs @@ -7,6 +7,7 @@ mod srtp_test; use std::collections::HashMap; +use crypto::RTCCrypto; use shared::replay_detector::*; use crate::cipher::cipher_aead_aes_gcm::*; @@ -16,7 +17,9 @@ use crate::option::*; use crate::protection_profile::*; use shared::error::{Error, Result}; +/// SRTCP protection and unprotection. pub mod srtcp; +/// SRTP protection and unprotection. pub mod srtp; const MAX_ROC: u32 = u32::MAX; @@ -101,13 +104,18 @@ pub struct Context { } impl Context { - /// CreateContext creates a new SRTP Context + /// Creates an SRTP context. + /// + /// The crypto implementation is supplied by the caller; this crate never resolves a default. + /// Borrowed, not owned: it is only needed to build the keyed cipher/MAC objects below, which + /// are what the context retains. pub fn new( master_key: &[u8], master_salt: &[u8], profile: ProtectionProfile, srtp_ctx_opt: Option, srtcp_ctx_opt: Option, + crypto: &dyn RTCCrypto, ) -> Result { let key_len = profile.key_len(); let salt_len = profile.salt_len(); @@ -117,19 +125,28 @@ impl Context { } else if master_salt.len() != salt_len { return Err(Error::SrtpSaltLength(salt_len, master_salt.len())); } + profile.ensure_crypto_supported(crypto)?; let cipher: Box = match profile { ProtectionProfile::Aes128CmHmacSha1_32 | ProtectionProfile::Aes128CmHmacSha1_80 | ProtectionProfile::Aes256CmHmacSha1_80 - | ProtectionProfile::Aes256CmHmacSha1_32 => { - Box::new(CipherAesCmHmacSha1::new(profile, master_key, master_salt)?) - } + | ProtectionProfile::Aes256CmHmacSha1_32 => Box::new(CipherAesCmHmacSha1::new( + profile, + master_key, + master_salt, + crypto, + )?), ProtectionProfile::AeadAes128Gcm | ProtectionProfile::AeadAes256Gcm => { // `CipherAeadAesGcm::new` selects AES-128 vs AES-256 from the // profile itself, so both GCM profiles share one arm. - Box::new(CipherAeadAesGcm::new(profile, master_key, master_salt)?) + Box::new(CipherAeadAesGcm::new( + profile, + master_key, + master_salt, + crypto, + )?) } }; diff --git a/rtc-srtp/src/context/srtcp_test.rs b/rtc-srtp/src/context/srtcp_test.rs index 08dd00d5..cee31d3a 100644 --- a/rtc-srtp/src/context/srtcp_test.rs +++ b/rtc-srtp/src/context/srtcp_test.rs @@ -1,6 +1,12 @@ use super::*; use crate::key_derivation::*; +/// The built-in provider, for tests only. Library code never resolves a default: every public +/// constructor takes the provider from its caller. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + use bytes::{Buf, Bytes, BytesMut}; use lazy_static::lazy_static; @@ -101,6 +107,7 @@ fn test_rtcp_lifecycle() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), )?; let mut decrypt_context = Context::new( &RTCP_TEST_MASTER_KEY, @@ -108,6 +115,7 @@ fn test_rtcp_lifecycle() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), )?; for test_case in &*RTCP_TEST_CASES { @@ -138,6 +146,7 @@ fn test_rtcp_invalid_auth_tag() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), )?; let decrypt_result = decrypt_context.decrypt_rtcp(&RTCP_TEST_CASES[0].encrypted)?; @@ -169,6 +178,7 @@ fn test_rtcp_replay_detector_separation() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, Some(srtcp_replay_protection(10)), + test_crypto_provider().crypto(), )?; let rtcp_packet1 = RTCP_TEST_CASES[0].encrypted.clone(); @@ -215,6 +225,7 @@ fn test_encrypt_rtcp_separation() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), )?; let auth_tag_len = ProtectionProfile::Aes128CmHmacSha1_80.rtcp_auth_tag_len(); @@ -225,6 +236,7 @@ fn test_encrypt_rtcp_separation() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, Some(srtcp_replay_protection(10)), + test_crypto_provider().crypto(), )?; let inputs = vec![ @@ -277,7 +289,14 @@ fn test_rtcp_short_packet_errors() -> Result<()> { ]; for (profile, salt) in cases { - let mut ctx = Context::new(&RTCP_TEST_MASTER_KEY, salt, profile, None, None)?; + let mut ctx = Context::new( + &RTCP_TEST_MASTER_KEY, + salt, + profile, + None, + None, + test_crypto_provider().crypto(), + )?; // Slices of a real packet (its first 4 bytes are a valid RTCP header, so // the header parse succeeds and pre-fix execution reached the diff --git a/rtc-srtp/src/context/srtp.rs b/rtc-srtp/src/context/srtp.rs index efe1b208..80f4c391 100644 --- a/rtc-srtp/src/context/srtp.rs +++ b/rtc-srtp/src/context/srtp.rs @@ -7,6 +7,15 @@ use shared::{ use bytes::BytesMut; impl Context { + /// Decrypts an SRTP packet whose header has already been parsed. + /// + /// Saves re-parsing when the caller needed the header to route the packet. The header must + /// be the one belonging to `encrypted`. + /// + /// # Errors + /// + /// Fails if authentication fails, if the packet is a replay, or if it is too short to hold + /// the profile's auth tag. pub fn decrypt_rtp_with_header( &mut self, encrypted: &[u8], @@ -47,6 +56,14 @@ impl Context { self.decrypt_rtp_with_header(encrypted, &header) } + /// Encrypts an RTP payload, using an already-parsed header. + /// + /// Saves re-parsing when the caller has just built the header. Returns the full protected + /// packet, header included. + /// + /// # Errors + /// + /// Fails if the SRTP context has no key for this SSRC or the cipher rejects the input. pub fn encrypt_rtp_with_header( &mut self, plaintext: &[u8], diff --git a/rtc-srtp/src/context/srtp_test.rs b/rtc-srtp/src/context/srtp_test.rs index e6e7ef6f..18fb130f 100644 --- a/rtc-srtp/src/context/srtp_test.rs +++ b/rtc-srtp/src/context/srtp_test.rs @@ -1,6 +1,12 @@ use super::*; use shared::marshal::*; +/// The built-in provider, for tests only. Library code never resolves a default: every public +/// constructor takes the provider from its caller. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + use bytes::Bytes; use lazy_static::lazy_static; @@ -72,6 +78,7 @@ fn build_test_context() -> Result { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), ) } @@ -92,6 +99,7 @@ fn test_rtp_invalid_auth() -> Result<()> { ProtectionProfile::Aes128CmHmacSha1_80, None, None, + test_crypto_provider().crypto(), )?; for test_case in &*RTP_TEST_CASES { diff --git a/rtc-srtp/src/key_derivation.rs b/rtc-srtp/src/key_derivation.rs index dbe36280..ac012570 100644 --- a/rtc-srtp/src/key_derivation.rs +++ b/rtc-srtp/src/key_derivation.rs @@ -1,8 +1,6 @@ -use aes::Aes256; -use aes::cipher::BlockEncrypt; -use aes::cipher::KeyInit; -use aes::{Aes128, cipher::generic_array::GenericArray}; +use crypto::{BlockCipherAlgorithm, RTCCrypto}; +use crate::cipher::crypto_error; use shared::error::{Error, Result}; pub const LABEL_SRTP_ENCRYPTION: u8 = 0x00; @@ -15,6 +13,7 @@ pub const LABEL_SRTCP_SALT: u8 = 0x05; pub(crate) const SRTCP_INDEX_SIZE: usize = 4; pub(crate) fn aes_cm_key_derivation( + crypto: &dyn RTCCrypto, label: u8, master_key: &[u8], master_salt: &[u8], @@ -40,9 +39,6 @@ pub(crate) fn aes_cm_key_derivation( prf_in[7] ^= label; //The resulting value is then AES encrypted using the master key to get the cipher key. - let key = GenericArray::from_slice(master_key); - let block = Aes128::new(key); - let mut out = vec![0u8; ((out_len + n_master_key) / n_master_key) * n_master_key]; for (i, n) in (0..out_len).step_by(n_master_key).enumerate() { //BigEndian.PutUint16(prfIn[nMasterKey-2:], i) @@ -50,8 +46,13 @@ pub(crate) fn aes_cm_key_derivation( prf_in[n_master_key - 1] = (i & 0xFF) as u8; out[n..n + n_master_key].copy_from_slice(&prf_in); - let out_key = GenericArray::from_mut_slice(&mut out[n..n + 16]); - block.encrypt_block(out_key); + crypto + .block_encrypt( + BlockCipherAlgorithm::Aes128, + master_key, + &mut out[n..n + 16], + ) + .map_err(crypto_error)?; } Ok(out[..out_len].to_vec()) @@ -61,6 +62,7 @@ pub(crate) fn aes_cm_key_derivation( // The key derivation rate is zero as per https://datatracker.ietf.org/doc/html/rfc5764 hence index_over-kdr is 0 const AES_256_BS: usize = 16; pub(crate) fn aes_256_cm_key_derivation( + crypto: &dyn RTCCrypto, label: u8, master_key: &[u8], master_salt: &[u8], @@ -93,16 +95,18 @@ pub(crate) fn aes_256_cm_key_derivation( } //The resulting value is then AES encrypted using the master key to get the cipher key. - let key = GenericArray::from_slice(master_key); - let block = Aes256::new(key); - let mut out = vec![0u8; ((out_len + AES_256_BS) / AES_256_BS) * AES_256_BS]; for (i, n) in (0..out_len).step_by(AES_256_BS).enumerate() { prf_in[AES_256_BS - 2..].copy_from_slice(&((i as u16).to_be_bytes())); out[n..n + AES_256_BS].copy_from_slice(&prf_in); - let out_key = GenericArray::from_mut_slice(&mut out[n..n + 16]); - block.encrypt_block(out_key); + crypto + .block_encrypt( + BlockCipherAlgorithm::Aes256, + master_key, + &mut out[n..n + AES_256_BS], + ) + .map_err(crypto_error)?; } Ok(out[..out_len].to_vec()) @@ -114,9 +118,9 @@ pub(crate) fn aes_256_cm_key_derivation( /// ROC = a 32-bit unsigned rollover counter (roc), which records how many /// times the 16-bit RTP sequence number has been reset to zero after /// passing through 65,535 -/// ```nobuild -/// i = 2^16 * roc + SEQ -/// IV = (salt*2 ^ 16) | (ssrc*2 ^ 64) | (i*2 ^ 16) +/// ```text +/// i = 2^16 * roc + SEQ +/// IV = (salt * 2^16) | (ssrc * 2^64) | (i * 2^16) /// ``` pub(crate) fn generate_counter( sequence_number: u16, @@ -150,6 +154,7 @@ mod test { #[test] fn test_valid_session_keys() -> Result<()> { + let provider = crypto::default_provider().map_err(crypto_error)?; // Key Derivation Test Vectors from https://tools.ietf.org/html/rfc3711#appendix-B.3 let master_key = vec![ 0xE1, 0xF9, 0x7A, 0x0D, 0x3E, 0x01, 0x8B, 0xE0, 0xD6, 0x4F, 0xA3, 0x2C, 0x06, 0xDE, @@ -172,6 +177,7 @@ mod test { ]; let session_key = aes_cm_key_derivation( + provider.crypto(), LABEL_SRTP_ENCRYPTION, &master_key, &master_salt, @@ -184,6 +190,7 @@ mod test { ); let session_salt = aes_cm_key_derivation( + provider.crypto(), LABEL_SRTP_SALT, &master_key, &master_salt, @@ -198,6 +205,7 @@ mod test { let auth_key_len = ProtectionProfile::Aes128CmHmacSha1_80.auth_key_len(); let session_auth_tag = aes_cm_key_derivation( + provider.crypto(), LABEL_SRTP_AUTHENTICATION_TAG, &master_key, &master_salt, @@ -216,7 +224,15 @@ mod test { // Currently this isn't supported, but the API makes sure we can add this in the future #[test] fn test_index_over_kdr() -> Result<()> { - let result = aes_cm_key_derivation(LABEL_SRTP_AUTHENTICATION_TAG, &[], &[], 1, 0); + let provider = crypto::default_provider().map_err(crypto_error)?; + let result = aes_cm_key_derivation( + provider.crypto(), + LABEL_SRTP_AUTHENTICATION_TAG, + &[], + &[], + 1, + 0, + ); assert!(result.is_err()); Ok(()) @@ -224,6 +240,7 @@ mod test { #[test] fn test_aes_256_cm_key_derivation() -> Result<()> { + let provider = crypto::default_provider().map_err(crypto_error)?; // Key Derivation Test Vectors from https://datatracker.ietf.org/doc/html/rfc6188#section-7.2 let master_key = vec![ 0xF0, 0xF0, 0x49, 0x14, 0xB5, 0x13, 0xF2, 0x76, 0x3A, 0x1B, 0x1F, 0xA1, 0x30, 0xF1, @@ -248,6 +265,7 @@ mod test { ]; let session_key = aes_256_cm_key_derivation( + provider.crypto(), LABEL_SRTP_ENCRYPTION, &master_key, &master_salt, @@ -260,6 +278,7 @@ mod test { ); let session_salt = aes_256_cm_key_derivation( + provider.crypto(), LABEL_SRTP_SALT, &master_key, &master_salt, @@ -274,6 +293,7 @@ mod test { let auth_key_len = ProtectionProfile::Aes128CmHmacSha1_80.auth_key_len(); let session_auth_tag = aes_256_cm_key_derivation( + provider.crypto(), LABEL_SRTP_AUTHENTICATION_TAG, &master_key, &master_salt, diff --git a/rtc-srtp/src/lib.rs b/rtc-srtp/src/lib.rs index 2b1ee642..5c0d6eb0 100644 --- a/rtc-srtp/src/lib.rs +++ b/rtc-srtp/src/lib.rs @@ -1,16 +1,64 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! SRTP and SRTCP for the Sans-I/O WebRTC stack. +//! +//! The Secure Real-time Transport Protocol ([RFC 3711]) as WebRTC keys it: protection +//! profiles negotiated through DTLS-SRTP ([RFC 5764]), with keying material exported from +//! the DTLS handshake rather than signalled. +//! +//! # Structure +//! +//! * [`context`] — [`Context`](context::Context), the encrypt/decrypt state for one +//! direction: `encrypt_rtp`/`decrypt_rtp` and the RTCP equivalents, plus the replay +//! protection and rollover-counter tracking the RFC requires. +//! * [`protection_profile`] — the negotiable profiles (AES-128-CM-SHA1-80, +//! AEAD-AES-128-GCM, and friends) and their key/salt lengths. +//! * [`config`], [`option`] — how a context is built, including replay-window sizing. +//! +//! # Example +//! +//! A profile is negotiated through DTLS-SRTP, and it fixes the key, salt and tag sizes the +//! context will use: +//! +//! ``` +//! use rtc_srtp::protection_profile::ProtectionProfile; +//! +//! let profile = ProtectionProfile::Aes128CmHmacSha1_80; +//! assert_eq!(profile.key_len(), 16); // AES-128 +//! assert_eq!(profile.salt_len(), 14); +//! assert_eq!(profile.rtp_auth_tag_len(), 10); // 80-bit tag +//! +//! // The AEAD profiles authenticate inside the cipher, so they carry no HMAC key. +//! assert_eq!(ProtectionProfile::AeadAes128Gcm.auth_key_len(), 0); +//! ``` +//! +//! Most applications do not depend on this crate directly — the +//! [`rtc`](https://docs.rs/rtc) crate creates the contexts from the DTLS handshake and +//! applies them to media as one layer of the peer-connection pipeline. +//! Applications constructing contexts directly can select cryptography explicitly with +//! [`context::Context::new`]; [`context::Context::new`] retains default-provider +//! compatibility. +//! +//! [RFC 3711]: https://datatracker.ietf.org/doc/html/rfc3711 +//! [RFC 5764]: https://datatracker.ietf.org/doc/html/rfc5764 + mod cipher; + +/// The crypto provider API. +/// +/// Re-exported because this crate's public constructors take an +/// [`Arc`](crypto::RTCCryptoProvider), which a caller must be able to name +/// without adding — and version-matching — a direct `rtc-crypto` dependency. +pub use crypto; + +/// Session configuration: keys, protection profile, and replay-protection options. pub mod config; +/// The encrypt/decrypt state for one SRTP/SRTCP session. pub mod context; mod key_derivation; +/// Per-context options, currently the replay-detector factory. pub mod option; +/// The DTLS-SRTP protection profiles and their key, salt and tag lengths. pub mod protection_profile; - -#[cfg(all(feature = "aws-lc-rs", feature = "ring"))] -compile_error!("At most one of the features \"aws-lc-rs\" and \"ring\" can be enabled."); -#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))] -compile_error!("At least one of the features \"aws-lc-rs\" and \"ring\" must be enabled."); -#[cfg(feature = "aws-lc-rs")] -extern crate aws_lc_rs as ring; diff --git a/rtc-srtp/src/option.rs b/rtc-srtp/src/option.rs index 6b5a79a1..3e2b6889 100644 --- a/rtc-srtp/src/option.rs +++ b/rtc-srtp/src/option.rs @@ -1,5 +1,11 @@ use shared::replay_detector::*; +/// A factory for the [`ReplayDetector`] a context +/// should use. +/// +/// A factory rather than a value because each SSRC in a session needs its own detector +/// state. Remote contexts default to a 64-packet sliding window; pass a different factory to +/// widen it or to disable replay protection. pub type ContextOption = Box Box + Send + Sync>; pub(crate) const MAX_SEQUENCE_NUMBER: u16 = 65535; diff --git a/rtc-srtp/src/protection_profile.rs b/rtc-srtp/src/protection_profile.rs index 268237c7..de3ffbf2 100644 --- a/rtc-srtp/src/protection_profile.rs +++ b/rtc-srtp/src/protection_profile.rs @@ -1,17 +1,77 @@ +use crypto::{ + AeadAlgorithm, BlockCipherAlgorithm, CryptoAlgorithm, HmacAlgorithm, RTCCrypto, + StreamCipherAlgorithm, +}; +use shared::error::{Error, Result}; + +const AES_128_CM_REQUIREMENTS: &[CryptoAlgorithm] = &[ + CryptoAlgorithm::BlockCipher(BlockCipherAlgorithm::Aes128), + CryptoAlgorithm::StreamCipher(StreamCipherAlgorithm::Aes128Ctr), + CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1), +]; +const AES_256_CM_REQUIREMENTS: &[CryptoAlgorithm] = &[ + CryptoAlgorithm::BlockCipher(BlockCipherAlgorithm::Aes256), + CryptoAlgorithm::StreamCipher(StreamCipherAlgorithm::Aes256Ctr), + CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1), +]; +const AEAD_AES_128_GCM_REQUIREMENTS: &[CryptoAlgorithm] = &[ + CryptoAlgorithm::BlockCipher(BlockCipherAlgorithm::Aes128), + CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Gcm), +]; +const AEAD_AES_256_GCM_REQUIREMENTS: &[CryptoAlgorithm] = &[ + CryptoAlgorithm::BlockCipher(BlockCipherAlgorithm::Aes256), + CryptoAlgorithm::Aead(AeadAlgorithm::Aes256Gcm), +]; + /// ProtectionProfile specifies Cipher and AuthTag details, similar to TLS cipher suite #[derive(Default, Debug, Clone, Copy)] #[repr(u8)] +#[non_exhaustive] pub enum ProtectionProfile { #[default] + /// `SRTP_AES128_CM_HMAC_SHA1_80`: AES-128 counter mode with an 80-bit HMAC-SHA1 tag. + /// + /// The profile every WebRTC implementation supports. Aes128CmHmacSha1_80 = 0x0001, + /// `SRTP_AES128_CM_HMAC_SHA1_32`: as above with a truncated 32-bit tag, trading + /// authentication strength for 6 bytes per packet. Aes128CmHmacSha1_32 = 0x0002, + /// `SRTP_AES256_CM_HMAC_SHA1_80`: AES-256 counter mode with an 80-bit HMAC-SHA1 tag. Aes256CmHmacSha1_80 = 0x0003, + /// `SRTP_AES256_CM_HMAC_SHA1_32`: AES-256 counter mode with a 32-bit HMAC-SHA1 tag. Aes256CmHmacSha1_32 = 0x0004, + /// `SRTP_AEAD_AES_128_GCM`: AES-128 in GCM, which authenticates as part of encryption + /// rather than with a separate HMAC. AeadAes128Gcm = 0x0007, + /// `SRTP_AEAD_AES_256_GCM`: AES-256 in GCM. AeadAes256Gcm = 0x0008, } impl ProtectionProfile { + /// Returns the provider operations required to construct this protection profile. + #[must_use] + pub const fn required_crypto_algorithms(self) -> &'static [CryptoAlgorithm] { + match self { + Self::Aes128CmHmacSha1_32 | Self::Aes128CmHmacSha1_80 => AES_128_CM_REQUIREMENTS, + Self::Aes256CmHmacSha1_32 | Self::Aes256CmHmacSha1_80 => AES_256_CM_REQUIREMENTS, + Self::AeadAes128Gcm => AEAD_AES_128_GCM_REQUIREMENTS, + Self::AeadAes256Gcm => AEAD_AES_256_GCM_REQUIREMENTS, + } + } + + /// Validates that `crypto` implements every operation required by this profile. + pub fn ensure_crypto_supported(self, crypto: &dyn RTCCrypto) -> Result<()> { + for algorithm in self.required_crypto_algorithms() { + if !crypto.supports(*algorithm) { + return Err(Error::Crypto(format!( + "SRTP protection profile {self:?} requires unsupported algorithm {algorithm:?}" + ))); + } + } + Ok(()) + } + + /// The master key length in bytes for this profile. pub fn key_len(&self) -> usize { match *self { ProtectionProfile::Aes128CmHmacSha1_32 @@ -22,6 +82,7 @@ impl ProtectionProfile { } } + /// The master salt length in bytes for this profile. pub fn salt_len(&self) -> usize { match *self { ProtectionProfile::Aes128CmHmacSha1_32 @@ -32,6 +93,7 @@ impl ProtectionProfile { } } + /// The authentication tag length appended to each SRTP packet, in bytes. pub fn rtp_auth_tag_len(&self) -> usize { match *self { ProtectionProfile::Aes128CmHmacSha1_80 | ProtectionProfile::Aes256CmHmacSha1_80 => 10, @@ -40,6 +102,7 @@ impl ProtectionProfile { } } + /// The authentication tag length appended to each SRTCP packet, in bytes. pub fn rtcp_auth_tag_len(&self) -> usize { match *self { ProtectionProfile::Aes128CmHmacSha1_80 @@ -50,6 +113,7 @@ impl ProtectionProfile { } } + /// The AEAD tag length in bytes, for the GCM profiles; `0` for the HMAC-SHA1 ones. pub fn aead_auth_tag_len(&self) -> usize { match *self { ProtectionProfile::Aes128CmHmacSha1_80 @@ -60,6 +124,8 @@ impl ProtectionProfile { } } + /// The HMAC authentication key length in bytes; `0` for the AEAD profiles, which derive + /// authentication from the cipher itself. pub fn auth_key_len(&self) -> usize { match *self { ProtectionProfile::Aes128CmHmacSha1_80 @@ -70,3 +136,50 @@ impl ProtectionProfile { } } } + +#[cfg(test)] +mod tests { + use super::*; + + struct CapabilityCrypto { + missing: Option, + } + + impl RTCCrypto for CapabilityCrypto { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + self.missing != Some(algorithm) + } + } + + const PROFILES: [ProtectionProfile; 6] = [ + ProtectionProfile::Aes128CmHmacSha1_80, + ProtectionProfile::Aes128CmHmacSha1_32, + ProtectionProfile::Aes256CmHmacSha1_80, + ProtectionProfile::Aes256CmHmacSha1_32, + ProtectionProfile::AeadAes128Gcm, + ProtectionProfile::AeadAes256Gcm, + ]; + + #[test] + fn complete_provider_supports_every_profile() { + let crypto = CapabilityCrypto { missing: None }; + for profile in PROFILES { + profile.ensure_crypto_supported(&crypto).unwrap(); + } + } + + #[test] + fn every_required_capability_is_enforced() { + for profile in PROFILES { + for algorithm in profile.required_crypto_algorithms() { + let crypto = CapabilityCrypto { + missing: Some(*algorithm), + }; + let error = profile.ensure_crypto_supported(&crypto).unwrap_err(); + let message = error.to_string(); + assert!(message.contains(&format!("{profile:?}"))); + assert!(message.contains(&format!("{algorithm:?}"))); + } + } + } +} diff --git a/rtc-srtp/tests/provider_profiles.rs b/rtc-srtp/tests/provider_profiles.rs new file mode 100644 index 00000000..1e7921d0 --- /dev/null +++ b/rtc-srtp/tests/provider_profiles.rs @@ -0,0 +1,417 @@ +use std::sync::Arc; +#[cfg(feature = "crypto-ring")] +use std::sync::atomic::{AtomicUsize, Ordering}; + +#[cfg(feature = "crypto-ring")] +use crypto::{ + AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, HmacAlgorithm, StreamCipher, + StreamCipherAlgorithm, +}; +use crypto::{CryptoAlgorithm, CryptoError, RTCCrypto, RTCCryptoProvider, RTCRandom}; +use rtc_srtp::context::Context; +use rtc_srtp::option::{srtcp_replay_protection, srtp_replay_protection}; +use rtc_srtp::protection_profile::ProtectionProfile; +use shared::error::Result; +use shared::marshal::Marshal; + +const PROFILES: [ProtectionProfile; 6] = [ + ProtectionProfile::Aes128CmHmacSha1_80, + ProtectionProfile::Aes128CmHmacSha1_32, + ProtectionProfile::Aes256CmHmacSha1_80, + ProtectionProfile::Aes256CmHmacSha1_32, + ProtectionProfile::AeadAes128Gcm, + ProtectionProfile::AeadAes256Gcm, +]; + +fn providers() -> Vec> { + #[cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] + return vec![ + Arc::new(crypto::providers::RingProvider::new()), + Arc::new(crypto::providers::AwsLcRsProvider::new()), + ]; + #[cfg(all(feature = "crypto-ring", not(feature = "crypto-aws-lc-rs")))] + return vec![Arc::new(crypto::providers::RingProvider::new())]; + #[cfg(all(not(feature = "crypto-ring"), feature = "crypto-aws-lc-rs"))] + return vec![Arc::new(crypto::providers::AwsLcRsProvider::new())]; + #[cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] + Vec::new() +} + +struct IncompleteCrypto; + +impl RTCCrypto for IncompleteCrypto { + fn supports(&self, _algorithm: CryptoAlgorithm) -> bool { + false + } +} + +struct IncompleteRandom; + +impl RTCRandom for IncompleteRandom { + fn fill(&self, _output: &mut [u8]) -> std::result::Result<(), CryptoError> { + Err(CryptoError::RandomnessFailed) + } +} + +struct IncompleteProvider { + crypto: IncompleteCrypto, + random: IncompleteRandom, +} + +impl RTCCryptoProvider for IncompleteProvider { + fn name(&self) -> &'static str { + "incomplete" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + &self.random + } +} + +#[test] +fn explicit_incomplete_provider_returns_actionable_capability_error() { + let profile = ProtectionProfile::Aes128CmHmacSha1_80; + let (key, salt) = key_material(profile); + let provider = IncompleteProvider { + crypto: IncompleteCrypto, + random: IncompleteRandom, + }; + let error = Context::new(&key, &salt, profile, None, None, provider.crypto()) + .err() + .expect("an incomplete provider must be rejected"); + let message = error.to_string(); + assert!(message.contains("Aes128CmHmacSha1_80")); + assert!(message.contains("BlockCipher(Aes128)")); +} + +fn key_material(profile: ProtectionProfile) -> (Vec, Vec) { + ( + (0..profile.key_len()).map(|index| index as u8).collect(), + (0..profile.salt_len()) + .map(|index| 0x80 | index as u8) + .collect(), + ) +} + +fn context( + profile: ProtectionProfile, + provider: Arc, + replay: bool, +) -> Result { + let (key, salt) = key_material(profile); + Context::new( + &key, + &salt, + profile, + replay.then(|| srtp_replay_protection(64)), + replay.then(|| srtcp_replay_protection(64)), + provider.crypto(), + ) +} + +fn rtp_packet(sequence_number: u16) -> Result> { + Ok(rtp::Packet { + header: rtp::Header { + version: 2, + sequence_number, + timestamp: 0x1234_5678, + ssrc: 0x1122_3344, + ..Default::default() + }, + payload: vec![0x41; 48].into(), + } + .marshal()? + .to_vec()) +} + +fn rtcp_packet() -> [u8; 8] { + [0x80, 200, 0, 1, 0x11, 0x22, 0x33, 0x44] +} + +#[test] +fn every_profile_round_trips_with_every_enabled_provider() -> Result<()> { + for provider in providers() { + for profile in PROFILES { + let mut sender = context(profile, provider.clone(), false)?; + let mut receiver = context(profile, provider.clone(), false)?; + + let rtp = rtp_packet(7)?; + let protected_rtp = sender.encrypt_rtp(&rtp)?; + assert_eq!(receiver.decrypt_rtp(&protected_rtp)?.as_ref(), rtp); + + if profile.aead_auth_tag_len() == 0 { + let mut wrong_tag = protected_rtp.to_vec(); + let last = wrong_tag.len() - 1; + wrong_tag[last] ^= 1; + assert!( + context(profile, provider.clone(), false)? + .decrypt_rtp(&wrong_tag) + .is_err() + ); + } + + let rtcp = rtcp_packet(); + let protected_rtcp = sender.encrypt_rtcp(&rtcp)?; + assert_eq!(receiver.decrypt_rtcp(&protected_rtcp)?.as_ref(), rtcp); + if profile.aead_auth_tag_len() == 0 { + let mut wrong_tag = protected_rtcp.to_vec(); + let last = wrong_tag.len() - 1; + wrong_tag[last] ^= 1; + assert!( + context(profile, provider.clone(), false)? + .decrypt_rtcp(&wrong_tag) + .is_err() + ); + } + } + } + Ok(()) +} + +#[test] +fn aead_profiles_reject_wrong_aad_tag_rollover_and_replay() -> Result<()> { + for provider in providers() { + for profile in [ + ProtectionProfile::AeadAes128Gcm, + ProtectionProfile::AeadAes256Gcm, + ] { + let mut sender = context(profile, provider.clone(), false)?; + let first = rtp_packet(u16::MAX)?; + let first_protected = sender.encrypt_rtp(&first)?; + let wrapped = rtp_packet(0)?; + let wrapped_protected = sender.encrypt_rtp(&wrapped)?; + + let mut receiver = context(profile, provider.clone(), true)?; + assert_eq!(receiver.decrypt_rtp(&first_protected)?.as_ref(), first); + assert_eq!(receiver.decrypt_rtp(&wrapped_protected)?.as_ref(), wrapped); + assert!(receiver.decrypt_rtp(&wrapped_protected).is_err()); + + let mut wrong_rollover = context(profile, provider.clone(), false)?; + assert!(wrong_rollover.decrypt_rtp(&wrapped_protected).is_err()); + + let mut wrong_aad = first_protected.to_vec(); + wrong_aad[8] ^= 1; + assert!( + context(profile, provider.clone(), false)? + .decrypt_rtp(&wrong_aad) + .is_err() + ); + + let mut wrong_tag = first_protected.to_vec(); + let last = wrong_tag.len() - 1; + wrong_tag[last] ^= 1; + assert!( + context(profile, provider.clone(), false)? + .decrypt_rtp(&wrong_tag) + .is_err() + ); + + let rtcp = rtcp_packet(); + let mut rtcp_sender = context(profile, provider.clone(), false)?; + let protected_rtcp = rtcp_sender.encrypt_rtcp(&rtcp)?; + let mut rtcp_receiver = context(profile, provider.clone(), true)?; + assert_eq!(rtcp_receiver.decrypt_rtcp(&protected_rtcp)?.as_ref(), rtcp); + assert!(rtcp_receiver.decrypt_rtcp(&protected_rtcp).is_err()); + + let mut wrong_rtcp_aad = protected_rtcp.to_vec(); + wrong_rtcp_aad[4] ^= 1; + assert!( + context(profile, provider.clone(), false)? + .decrypt_rtcp(&wrong_rtcp_aad) + .is_err() + ); + + let mut wrong_rtcp_tag = protected_rtcp.to_vec(); + let tag_byte = wrong_rtcp_tag.len() - 5; + wrong_rtcp_tag[tag_byte] ^= 1; + assert!( + context(profile, provider.clone(), false)? + .decrypt_rtcp(&wrong_rtcp_tag) + .is_err() + ); + } + } + Ok(()) +} + +#[cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] +#[test] +fn providers_produce_identical_packets_and_interoperate() -> Result<()> { + let ring: Arc = Arc::new(crypto::providers::RingProvider::new()); + let aws: Arc = Arc::new(crypto::providers::AwsLcRsProvider::new()); + + for profile in PROFILES { + let rtp = rtp_packet(42)?; + let mut ring_sender = context(profile, ring.clone(), false)?; + let mut aws_sender = context(profile, aws.clone(), false)?; + let ring_packet = ring_sender.encrypt_rtp(&rtp)?; + let aws_packet = aws_sender.encrypt_rtp(&rtp)?; + assert_eq!(ring_packet, aws_packet); + + let mut ring_receiver = context(profile, ring.clone(), false)?; + let mut aws_receiver = context(profile, aws.clone(), false)?; + assert_eq!(ring_receiver.decrypt_rtp(&aws_packet)?.as_ref(), rtp); + assert_eq!(aws_receiver.decrypt_rtp(&ring_packet)?.as_ref(), rtp); + } + Ok(()) +} + +#[cfg(feature = "crypto-ring")] +struct CountingCrypto { + inner: crypto::providers::RingCrypto, + stream_constructions: AtomicUsize, + aead_constructions: AtomicUsize, + mac_constructions: AtomicUsize, +} + +#[cfg(feature = "crypto-ring")] +impl RTCCrypto for CountingCrypto { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + self.inner.supports(algorithm) + } + + fn block_encrypt( + &self, + algorithm: BlockCipherAlgorithm, + key: &[u8], + block: &mut [u8], + ) -> std::result::Result<(), CryptoError> { + self.inner.block_encrypt(algorithm, key, block) + } + + fn new_stream_cipher( + &self, + algorithm: StreamCipherAlgorithm, + key: &[u8], + ) -> std::result::Result, CryptoError> { + self.stream_constructions.fetch_add(1, Ordering::Relaxed); + self.inner.new_stream_cipher(algorithm, key) + } + + fn new_aead( + &self, + algorithm: AeadAlgorithm, + key: &[u8], + ) -> std::result::Result, CryptoError> { + self.aead_constructions.fetch_add(1, Ordering::Relaxed); + self.inner.new_aead(algorithm, key) + } + + fn new_hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + ) -> std::result::Result, CryptoError> { + self.mac_constructions.fetch_add(1, Ordering::Relaxed); + self.inner.new_hmac(algorithm, key) + } +} + +#[cfg(feature = "crypto-ring")] +struct CountingProvider { + crypto: CountingCrypto, + random: crypto::providers::RingRandom, +} + +#[cfg(feature = "crypto-ring")] +impl CountingProvider { + fn new() -> Self { + Self { + crypto: CountingCrypto { + inner: crypto::providers::RingCrypto, + stream_constructions: AtomicUsize::new(0), + aead_constructions: AtomicUsize::new(0), + mac_constructions: AtomicUsize::new(0), + }, + random: crypto::providers::RingRandom, + } + } +} + +#[cfg(feature = "crypto-ring")] +impl RTCCryptoProvider for CountingProvider { + fn name(&self) -> &'static str { + "counting-ring" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + &self.random + } +} + +#[cfg(feature = "crypto-ring")] +#[test] +fn keyed_ciphers_are_constructed_once_per_context_not_per_packet() -> Result<()> { + let stream_provider = Arc::new(CountingProvider::new()); + let mut stream_context = context( + ProtectionProfile::Aes128CmHmacSha1_80, + stream_provider.clone(), + false, + )?; + assert_eq!( + stream_provider + .crypto + .stream_constructions + .load(Ordering::Relaxed), + 2 + ); + // Two MACs (SRTP and SRTCP), keyed once alongside the ciphers. + assert_eq!( + stream_provider + .crypto + .mac_constructions + .load(Ordering::Relaxed), + 2 + ); + for sequence_number in 0..4 { + stream_context.encrypt_rtp(&rtp_packet(sequence_number)?)?; + } + assert_eq!( + stream_provider + .crypto + .stream_constructions + .load(Ordering::Relaxed), + 2 + ); + assert_eq!( + stream_provider + .crypto + .mac_constructions + .load(Ordering::Relaxed), + 2, + "MACs must be keyed once per context, not per packet" + ); + + let aead_provider = Arc::new(CountingProvider::new()); + let mut aead_context = context( + ProtectionProfile::AeadAes128Gcm, + aead_provider.clone(), + false, + )?; + assert_eq!( + aead_provider + .crypto + .aead_constructions + .load(Ordering::Relaxed), + 2 + ); + for sequence_number in 0..4 { + aead_context.encrypt_rtp(&rtp_packet(sequence_number)?)?; + } + assert_eq!( + aead_provider + .crypto + .aead_constructions + .load(Ordering::Relaxed), + 2 + ); + Ok(()) +} diff --git a/rtc-stun/Cargo.toml b/rtc-stun/Cargo.toml index 793cf64c..d6244bca 100644 --- a/rtc-stun/Cargo.toml +++ b/rtc-stun/Cargo.toml @@ -12,25 +12,22 @@ keywords.workspace = true categories.workspace = true [features] -default = ["ring"] +default = ["crypto-ring"] +crypto-ring = ["crypto/crypto-ring"] +crypto-aws-lc-rs = ["crypto/crypto-aws-lc-rs"] bench = [] -ring = ["dep:ring"] -aws-lc-rs = ["dep:aws-lc-rs"] [dependencies] shared = { workspace = true, default-features = false, features = [] } sansio.workspace = true +crypto.workspace = true bytes.workspace = true -lazy_static = "1.4.0" -url = "2.5.0" +lazy_static.workspace = true +url.workspace = true rand.workspace = true -base64 = "0.22.1" -subtle = "2.5.0" -crc = "3.0.1" -ring = { workspace = true, optional = true } -aws-lc-rs = { workspace = true, optional = true } -md-5 = "0.10" +base64.workspace = true +crc.workspace = true [dev-dependencies] clap.workspace = true diff --git a/rtc-stun/benches/README.md b/rtc-stun/benches/README.md index db8aaa5c..177d508d 100644 --- a/rtc-stun/benches/README.md +++ b/rtc-stun/benches/README.md @@ -784,4 +784,40 @@ BenchmarkXORMappedAddress_GetFrom Found 13 outliers among 100 measurements (13.00%) 2 (2.00%) high mild 11 (11.00%) high severe -``` \ No newline at end of file +``` + +--- + +## G3 crypto-provider migration: measured impact + +`MESSAGE-INTEGRITY` is the only cryptographic operation on the STUN path, so these two benchmarks +cover it. One machine (Apple M1 Max, macOS 26.5.2), identical criterion settings, comparing a +worktree at `b8bb313` (P1 — the last commit before STUN moved to `rtc-crypto`) against the current +tree. The historical results above were taken on a different machine and are not comparable. + +| Benchmark | Pre-migration | Current | Change | +|---|---|---|---| +| `BenchmarkMessageIntegrity_AddTo` | 934.5 ns | 937.5 ns | none | +| `BenchmarkMessageIntegrity_Check` | 944.5 ns | 956.6 ns | none (within noise) | + +**No regression.** This is the expected result, and it corroborates the diagnosis in +`rtc-srtp/benches/README.md`: STUN already computed HMAC-SHA1 through `ring::hmac` before the +migration, so routing it through `RTCCrypto` did not change the underlying implementation. SRTP +regressed because it moved from the RustCrypto `sha1` crate — which uses ARMv8 SHA-1 instructions +— onto `ring`'s software SHA-1. + +`MessageIntegrity` keys its MAC per message rather than holding a keyed `Mac`, because +`Setter::add_to` and `check` take `&self`. That is deliberate: a STUN message is authenticated +once, and ICE exchanges them at connectivity-check rates rather than per media packet. The figures +above show the cost is not material at that rate. The keyed-object pattern is used where it does +matter, on the SRTP and DTLS record paths. + +Reproduce: + +```bash +cargo bench --package rtc-stun --bench bench -- --warm-up-time 2 --measurement-time 4 MessageIntegrity +git worktree add /tmp/rtc-stun-base b8bb313 # same command in the worktree +``` + +The methodology, including why cross-machine numbers must not be compared, is in +`docs/benchmarking-crypto-migration.md`. diff --git a/rtc-stun/benches/bench.rs b/rtc-stun/benches/bench.rs index ba8ce4e2..1804f3c4 100644 --- a/rtc-stun/benches/bench.rs +++ b/rtc-stun/benches/bench.rs @@ -26,6 +26,12 @@ use base64::prelude::*; // sufficient to make function zero-alloc in most cases. // const AGENT_COLLECT_CAP: usize = 100; +/// The built-in provider used by these benchmarks. Provider selection is explicit since the +/// default-resolving constructors were removed before 1.0. +fn bench_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled") +} + fn benchmark_addr(c: &mut Criterion) { let mut m = Message::new(); @@ -232,9 +238,13 @@ fn benchmark_message_build_overhead(c: &mut Criterion) { } fn benchmark_message_integrity(c: &mut Criterion) { + let provider = bench_provider(); { let mut m = Message::new(); - let integrity = MessageIntegrity::new_short_term_integrity("password".to_owned()); + let integrity = MessageIntegrity::new_short_term_integrity_with_provider( + "password".to_owned(), + provider.crypto(), + ); m.write_header(); c.bench_function("BenchmarkMessageIntegrity_AddTo", |b| { b.iter(|| { @@ -250,19 +260,24 @@ fn benchmark_message_integrity(c: &mut Criterion) { m.raw = Vec::with_capacity(1024); let software = Software::new(ATTR_SOFTWARE, "software".to_owned()); let _ = software.add_to(&mut m); - let integrity = MessageIntegrity::new_short_term_integrity("password".to_owned()); + let integrity = MessageIntegrity::new_short_term_integrity_with_provider( + "password".to_owned(), + provider.crypto(), + ); m.write_header(); integrity.add_to(&mut m).unwrap(); m.write_header(); c.bench_function("BenchmarkMessageIntegrity_Check", |b| { b.iter(|| { - integrity.check(&mut m).unwrap(); + MessageIntegrity::<'_>::check(&mut m, "password".as_bytes(), provider.crypto()) + .unwrap(); }) }); } } fn benchmark_message(c: &mut Criterion) { + let provider = bench_provider(); { let mut m = Message::new(); c.bench_function("BenchmarkMessage_Write", |b| { @@ -449,11 +464,15 @@ fn benchmark_message(c: &mut Criterion) { Box::new(BINDING_REQUEST), Box::new(TransactionId([1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2])), Box::new(Software::new(ATTR_SOFTWARE, "webrtc-rs/stun".to_owned())), - Box::new(MessageIntegrity::new_long_term_integrity( - "username".to_owned(), - "realm".to_owned(), - "password".to_owned(), - )), + Box::new( + MessageIntegrity::new_long_term_integrity_with_provider( + "username".to_owned(), + "realm".to_owned(), + "password".to_owned(), + provider.crypto(), + ) + .unwrap(), + ), Box::new(FINGERPRINT), ]) .unwrap(); diff --git a/rtc-stun/src/addr.rs b/rtc-stun/src/addr.rs index 23b25f20..fe225f66 100644 --- a/rtc-stun/src/addr.rs +++ b/rtc-stun/src/addr.rs @@ -20,7 +20,9 @@ pub(crate) const IPV6LEN: usize = 16; /// /// RFC 5389 Section 15.1 pub struct MappedAddress { + /// The IP address. pub ip: IpAddr, + /// The port. pub port: u16, } diff --git a/rtc-stun/src/agent.rs b/rtc-stun/src/agent.rs index c6d0f0e6..12777433 100644 --- a/rtc-stun/src/agent.rs +++ b/rtc-stun/src/agent.rs @@ -1,3 +1,11 @@ +//! STUN transaction tracking. +//! +//! The agent remembers which requests are outstanding and when each should be considered lost. It +//! performs no I/O: the caller submits [`ClientAgent`](crate::agent::ClientAgent) commands — start a transaction, hand over +//! an inbound message, advance time, stop or close — and polls for the resulting [`Event`](crate::agent::Event)s. +//! +//! This split is what lets the retransmission schedule be tested without a network, and lets ICE +//! reuse the same transaction bookkeeping for its connectivity checks. #[cfg(test)] mod agent_test; @@ -27,15 +35,23 @@ pub struct Agent { /// Do not reuse outside Handler. #[derive(Debug)] //Clone pub struct Event { + /// The transaction this event belongs to. pub id: TransactionId, + /// What happened. pub evt: StunEvent, } #[derive(Debug)] //Clone +/// What became of a STUN transaction. +#[non_exhaustive] pub enum StunEvent { + /// The agent was closed, abandoning this transaction. AgentClosed, + /// The transaction was stopped by the caller. TransactionStopped, + /// The transaction timed out with no response. TransactionTimeOut, + /// A response arrived for this transaction. Message(Message), } @@ -53,11 +69,17 @@ const AGENT_COLLECT_CAP: usize = 100; /// ClientAgent is Agent implementation that is used by Client to /// process transactions. #[derive(Debug)] +#[non_exhaustive] pub enum ClientAgent { + /// Hand an inbound message to the agent for matching against a transaction. Process(Message), + /// Advance time so the agent can expire transactions. Collect(Instant), + /// Register a new transaction with its deadline. Start(TransactionId, Instant), + /// Abandon a transaction without waiting for its deadline. Stop(TransactionId), + /// Close the agent, abandoning every outstanding transaction. Close, } @@ -71,6 +93,11 @@ impl Agent { } } + /// Applies an agent command: start, stop, process a message, collect timeouts, or close. + /// + /// # Errors + /// + /// Fails if the agent is closed, or a transaction id is already in use. pub fn handle_event(&mut self, client_agent: ClientAgent) -> Result<()> { match client_agent { ClientAgent::Process(message) => self.process(message), @@ -81,6 +108,7 @@ impl Agent { } } + /// When the agent next needs [`ClientAgent::Collect`], or `None` with nothing outstanding. pub fn poll_timeout(&mut self) -> Option { let mut deadline = None; for transaction in self.transactions.values() { @@ -91,6 +119,7 @@ impl Agent { deadline } + /// The next transaction event, or `None` when there is nothing to report. pub fn poll_event(&mut self) -> Option { self.events_queue.pop_front() } diff --git a/rtc-stun/src/attributes.rs b/rtc-stun/src/attributes.rs index 843782a8..e4aaa430 100644 --- a/rtc-stun/src/attributes.rs +++ b/rtc-stun/src/attributes.rs @@ -1,3 +1,18 @@ +//! STUN attribute types. +//! +//! Every attribute is a type code, a length and a value ([`RawAttribute`](crate::attributes::RawAttribute)). This module defines +//! the code points — those from STUN itself ([RFC 5389]) plus the ones TURN, ICE and the NAT +//! behaviour discovery extensions add — while the typed accessors live in +//! [`textattrs`](crate::textattrs), [`xoraddr`](crate::xoraddr), +//! [`error_code`](crate::error_code) and friends. +//! +//! Codes below `0x8000` are *comprehension-required*: a receiver that does not understand one +//! must reject the message. Codes at or above `0x8000` are comprehension-optional and may be +//! ignored, which is how `FINGERPRINT` and the ICE attributes can be added without breaking +//! older peers. +//! +//! [RFC 5389]: https://datatracker.ietf.org/doc/html/rfc5389 + #[cfg(test)] mod attributes_test; @@ -91,66 +106,106 @@ impl AttrType { } /// Attributes from comprehension-required range (0x0000-0x7FFF). -pub const ATTR_MAPPED_ADDRESS: AttrType = AttrType(0x0001); // MAPPED-ADDRESS -pub const ATTR_USERNAME: AttrType = AttrType(0x0006); // USERNAME -pub const ATTR_MESSAGE_INTEGRITY: AttrType = AttrType(0x0008); // MESSAGE-INTEGRITY -pub const ATTR_ERROR_CODE: AttrType = AttrType(0x0009); // ERROR-CODE -pub const ATTR_UNKNOWN_ATTRIBUTES: AttrType = AttrType(0x000A); // UNKNOWN-ATTRIBUTES -pub const ATTR_REALM: AttrType = AttrType(0x0014); // REALM -pub const ATTR_NONCE: AttrType = AttrType(0x0015); // NONCE -pub const ATTR_XORMAPPED_ADDRESS: AttrType = AttrType(0x0020); // XOR-MAPPED-ADDRESS +/// MAPPED-ADDRESS. +pub const ATTR_MAPPED_ADDRESS: AttrType = AttrType(0x0001); +/// USERNAME. +pub const ATTR_USERNAME: AttrType = AttrType(0x0006); +/// MESSAGE-INTEGRITY. +pub const ATTR_MESSAGE_INTEGRITY: AttrType = AttrType(0x0008); +/// ERROR-CODE. +pub const ATTR_ERROR_CODE: AttrType = AttrType(0x0009); +/// UNKNOWN-ATTRIBUTES. +pub const ATTR_UNKNOWN_ATTRIBUTES: AttrType = AttrType(0x000A); +/// REALM. +pub const ATTR_REALM: AttrType = AttrType(0x0014); +/// NONCE. +pub const ATTR_NONCE: AttrType = AttrType(0x0015); +/// XOR-MAPPED-ADDRESS. +pub const ATTR_XORMAPPED_ADDRESS: AttrType = AttrType(0x0020); /// Attributes from comprehension-optional range (0x8000-0xFFFF). -pub const ATTR_SOFTWARE: AttrType = AttrType(0x8022); // SOFTWARE -pub const ATTR_ALTERNATE_SERVER: AttrType = AttrType(0x8023); // ALTERNATE-SERVER -pub const ATTR_FINGERPRINT: AttrType = AttrType(0x8028); // FINGERPRINT +/// SOFTWARE. +pub const ATTR_SOFTWARE: AttrType = AttrType(0x8022); +/// ALTERNATE-SERVER. +pub const ATTR_ALTERNATE_SERVER: AttrType = AttrType(0x8023); +/// FINGERPRINT. +pub const ATTR_FINGERPRINT: AttrType = AttrType(0x8028); /// Attributes from RFC 5245 ICE. -pub const ATTR_PRIORITY: AttrType = AttrType(0x0024); // PRIORITY -pub const ATTR_USE_CANDIDATE: AttrType = AttrType(0x0025); // USE-CANDIDATE -pub const ATTR_ICE_CONTROLLED: AttrType = AttrType(0x8029); // ICE-CONTROLLED -pub const ATTR_ICE_CONTROLLING: AttrType = AttrType(0x802A); // ICE-CONTROLLING -pub const ATTR_NETWORK_COST: AttrType = AttrType(0xC057); // NETWORK-COST +/// PRIORITY. +pub const ATTR_PRIORITY: AttrType = AttrType(0x0024); +/// USE-CANDIDATE. +pub const ATTR_USE_CANDIDATE: AttrType = AttrType(0x0025); +/// ICE-CONTROLLED. +pub const ATTR_ICE_CONTROLLED: AttrType = AttrType(0x8029); +/// ICE-CONTROLLING. +pub const ATTR_ICE_CONTROLLING: AttrType = AttrType(0x802A); +/// NETWORK-COST. +pub const ATTR_NETWORK_COST: AttrType = AttrType(0xC057); /// Attributes from RFC 5766 TURN. -pub const ATTR_CHANNEL_NUMBER: AttrType = AttrType(0x000C); // CHANNEL-NUMBER -pub const ATTR_LIFETIME: AttrType = AttrType(0x000D); // LIFETIME -pub const ATTR_XOR_PEER_ADDRESS: AttrType = AttrType(0x0012); // XOR-PEER-ADDRESS -pub const ATTR_DATA: AttrType = AttrType(0x0013); // DATA -pub const ATTR_XOR_RELAYED_ADDRESS: AttrType = AttrType(0x0016); // XOR-RELAYED-ADDRESS -pub const ATTR_EVEN_PORT: AttrType = AttrType(0x0018); // EVEN-PORT -pub const ATTR_REQUESTED_TRANSPORT: AttrType = AttrType(0x0019); // REQUESTED-TRANSPORT -pub const ATTR_DONT_FRAGMENT: AttrType = AttrType(0x001A); // DONT-FRAGMENT -pub const ATTR_RESERVATION_TOKEN: AttrType = AttrType(0x0022); // RESERVATION-TOKEN +/// CHANNEL-NUMBER. +pub const ATTR_CHANNEL_NUMBER: AttrType = AttrType(0x000C); +/// LIFETIME. +pub const ATTR_LIFETIME: AttrType = AttrType(0x000D); +/// XOR-PEER-ADDRESS. +pub const ATTR_XOR_PEER_ADDRESS: AttrType = AttrType(0x0012); +/// DATA. +pub const ATTR_DATA: AttrType = AttrType(0x0013); +/// XOR-RELAYED-ADDRESS. +pub const ATTR_XOR_RELAYED_ADDRESS: AttrType = AttrType(0x0016); +/// EVEN-PORT. +pub const ATTR_EVEN_PORT: AttrType = AttrType(0x0018); +/// REQUESTED-TRANSPORT. +pub const ATTR_REQUESTED_TRANSPORT: AttrType = AttrType(0x0019); +/// DONT-FRAGMENT. +pub const ATTR_DONT_FRAGMENT: AttrType = AttrType(0x001A); +/// RESERVATION-TOKEN. +pub const ATTR_RESERVATION_TOKEN: AttrType = AttrType(0x0022); /// Attributes from RFC 5780 NAT Behavior Discovery -pub const ATTR_CHANGE_REQUEST: AttrType = AttrType(0x0003); // CHANGE-REQUEST -pub const ATTR_PADDING: AttrType = AttrType(0x0026); // PADDING -pub const ATTR_RESPONSE_PORT: AttrType = AttrType(0x0027); // RESPONSE-PORT -pub const ATTR_CACHE_TIMEOUT: AttrType = AttrType(0x8027); // CACHE-TIMEOUT -pub const ATTR_RESPONSE_ORIGIN: AttrType = AttrType(0x802b); // RESPONSE-ORIGIN -pub const ATTR_OTHER_ADDRESS: AttrType = AttrType(0x802C); // OTHER-ADDRESS +/// CHANGE-REQUEST. +pub const ATTR_CHANGE_REQUEST: AttrType = AttrType(0x0003); +/// PADDING. +pub const ATTR_PADDING: AttrType = AttrType(0x0026); +/// RESPONSE-PORT. +pub const ATTR_RESPONSE_PORT: AttrType = AttrType(0x0027); +/// CACHE-TIMEOUT. +pub const ATTR_CACHE_TIMEOUT: AttrType = AttrType(0x8027); +/// RESPONSE-ORIGIN. +pub const ATTR_RESPONSE_ORIGIN: AttrType = AttrType(0x802b); +/// OTHER-ADDRESS. +pub const ATTR_OTHER_ADDRESS: AttrType = AttrType(0x802C); /// Attributes from RFC 3489, removed by RFC 5389, /// but still used by RFC5389-implementing software like Vovida.org, reTURNServer, etc. -pub const ATTR_SOURCE_ADDRESS: AttrType = AttrType(0x0004); // SOURCE-ADDRESS -pub const ATTR_CHANGED_ADDRESS: AttrType = AttrType(0x0005); // CHANGED-ADDRESS +/// SOURCE-ADDRESS. +pub const ATTR_SOURCE_ADDRESS: AttrType = AttrType(0x0004); +/// CHANGED-ADDRESS. +pub const ATTR_CHANGED_ADDRESS: AttrType = AttrType(0x0005); /// Attributes from RFC 6062 TURN Extensions for TCP Allocations. -pub const ATTR_CONNECTION_ID: AttrType = AttrType(0x002a); // CONNECTION-ID +/// CONNECTION-ID. +pub const ATTR_CONNECTION_ID: AttrType = AttrType(0x002a); /// Attributes from RFC 6156 TURN IPv6. -pub const ATTR_REQUESTED_ADDRESS_FAMILY: AttrType = AttrType(0x0017); // REQUESTED-ADDRESS-FAMILY +/// REQUESTED-ADDRESS-FAMILY. +pub const ATTR_REQUESTED_ADDRESS_FAMILY: AttrType = AttrType(0x0017); /// Attributes from An Origin Attribute for the STUN Protocol. pub const ATTR_ORIGIN: AttrType = AttrType(0x802F); /// Attributes from RFC 8489 STUN. -pub const ATTR_MESSAGE_INTEGRITY_SHA256: AttrType = AttrType(0x001C); // MESSAGE-INTEGRITY-SHA256 -pub const ATTR_PASSWORD_ALGORITHM: AttrType = AttrType(0x001D); // PASSWORD-ALGORITHM -pub const ATTR_USER_HASH: AttrType = AttrType(0x001E); // USER-HASH -pub const ATTR_PASSWORD_ALGORITHMS: AttrType = AttrType(0x8002); // PASSWORD-ALGORITHMS -pub const ATTR_ALTERNATE_DOMAIN: AttrType = AttrType(0x8003); // ALTERNATE-DOMAIN +/// MESSAGE-INTEGRITY-SHA256. +pub const ATTR_MESSAGE_INTEGRITY_SHA256: AttrType = AttrType(0x001C); +/// PASSWORD-ALGORITHM. +pub const ATTR_PASSWORD_ALGORITHM: AttrType = AttrType(0x001D); +/// USER-HASH. +pub const ATTR_USER_HASH: AttrType = AttrType(0x001E); +/// PASSWORD-ALGORITHMS. +pub const ATTR_PASSWORD_ALGORITHMS: AttrType = AttrType(0x8002); +/// ALTERNATE-DOMAIN. +pub const ATTR_ALTERNATE_DOMAIN: AttrType = AttrType(0x8003); /// RawAttribute is a Type-Length-Value (TLV) object that /// can be added to a STUN message. Attributes are divided into two @@ -161,8 +216,11 @@ pub const ATTR_ALTERNATE_DOMAIN: AttrType = AttrType(0x8003); // ALTERNATE-DOMAI /// understood. #[derive(Default, Debug, Clone, PartialEq, Eq)] pub struct RawAttribute { + /// Which attribute this is. pub typ: AttrType, + /// The value length in bytes; recomputed when encoding, so it is ignored there. pub length: u16, // ignored while encoding + /// The attribute's raw value. pub value: Vec, } diff --git a/rtc-stun/src/checks.rs b/rtc-stun/src/checks.rs index 1fc4dd9d..a7f35140 100644 --- a/rtc-stun/src/checks.rs +++ b/rtc-stun/src/checks.rs @@ -1,9 +1,7 @@ use crate::attributes::*; use shared::error::*; -use subtle::ConstantTimeEq; - -// check_size returns ErrAttrSizeInvalid if got is not equal to expected. +/// Check_size returns ErrAttrSizeInvalid if got is not equal to expected. pub fn check_size(_at: AttrType, got: usize, expected: usize) -> Result<()> { if got == expected { Ok(()) @@ -12,19 +10,11 @@ pub fn check_size(_at: AttrType, got: usize, expected: usize) -> Result<()> { } } -// is_attr_size_invalid returns true if error means that attribute size is invalid. +/// Is_attr_size_invalid returns true if error means that attribute size is invalid. pub fn is_attr_size_invalid(err: &Error) -> bool { Error::ErrAttributeSizeInvalid == *err } -pub(crate) fn check_hmac(got: &[u8], expected: &[u8]) -> Result<()> { - if got.ct_eq(expected).unwrap_u8() != 1 { - Err(Error::ErrIntegrityMismatch) - } else { - Ok(()) - } -} - pub(crate) fn check_fingerprint(got: u32, expected: u32) -> Result<()> { if got == expected { Ok(()) @@ -33,7 +23,7 @@ pub(crate) fn check_fingerprint(got: u32, expected: u32) -> Result<()> { } } -// check_overflow returns ErrAttributeSizeOverflow if got is bigger that max. +/// Check_overflow returns ErrAttributeSizeOverflow if got is bigger that max. pub fn check_overflow(_at: AttrType, got: usize, max: usize) -> Result<()> { if got <= max { Ok(()) @@ -42,7 +32,7 @@ pub fn check_overflow(_at: AttrType, got: usize, max: usize) -> Result<()> { } } -// is_attr_size_overflow returns true if error means that attribute size is too big. +/// Is_attr_size_overflow returns true if error means that attribute size is too big. pub fn is_attr_size_overflow(err: &Error) -> bool { Error::ErrAttributeSizeOverflow == *err } diff --git a/rtc-stun/src/client.rs b/rtc-stun/src/client.rs index e19f5017..aec31a29 100644 --- a/rtc-stun/src/client.rs +++ b/rtc-stun/src/client.rs @@ -1,3 +1,8 @@ +//! A Sans-I/O STUN client. +//! +//! Sends Binding requests and matches the responses, applying the retransmission schedule the RFC +//! specifies (an initial RTO, doubling per retry) so a lost request on a UDP path is retried +//! rather than lost. Build one with [`ClientBuilder`](crate::client::ClientBuilder); drive it with datagrams and time. use bytes::BytesMut; use shared::error::*; use std::collections::{HashMap, VecDeque}; @@ -55,6 +60,8 @@ impl Default for ClientSettings { } #[derive(Default)] +/// Builds a [`Client`] with a chosen transaction timeout, retransmission schedule and +/// handler. pub struct ClientBuilder { settings: ClientSettings, } @@ -90,12 +97,18 @@ impl ClientBuilder { self } + /// A builder with the RFC's default timings. pub fn new() -> Self { ClientBuilder { settings: ClientSettings::default(), } } + /// Builds the client for the given local and remote addresses. + /// + /// # Errors + /// + /// Fails if the configured timings are inconsistent. pub fn build( self, local: SocketAddr, @@ -135,10 +148,12 @@ impl Client { } } + /// The address this client sends from. pub fn local_addr(&self) -> SocketAddr { self.local } + /// The STUN server this client talks to. pub fn peer_addr(&self) -> SocketAddr { self.remote } diff --git a/rtc-stun/src/error_code.rs b/rtc-stun/src/error_code.rs index f847a430..6a79ecd1 100644 --- a/rtc-stun/src/error_code.rs +++ b/rtc-stun/src/error_code.rs @@ -1,3 +1,11 @@ +//! The `ERROR-CODE` attribute. +//! +//! An [`ErrorCodeAttribute`](crate::error_code::ErrorCodeAttribute) is a numeric [`ErrorCode`](crate::error_code::ErrorCode) plus a reason phrase. The codes here span +//! three specs — STUN's own (400, 401, 420, 500), ICE's role conflict (487), and TURN's +//! allocation failures (437, 441, 486, 508) — because all three share this attribute. +//! +//! [`ERROR_REASONS`](crate::error_code::ERROR_REASONS) maps each known code to the phrase it is normally sent with, so a responder +//! does not have to invent one. #[cfg(test)] mod error_code_test; @@ -13,8 +21,11 @@ use std::fmt; // // RFC 5389 Section 15.6 #[derive(Default)] +/// The `ERROR-CODE` attribute: a numeric code and a human-readable reason. pub struct ErrorCodeAttribute { + /// The numeric error code. pub code: ErrorCode, + /// The reason phrase, as UTF-8 bytes. pub reason: Vec, } @@ -81,6 +92,7 @@ impl Getter for ErrorCodeAttribute { // ErrorCode is code for ERROR-CODE attribute. #[derive(PartialEq, Eq, Hash, Copy, Clone, Default)] +/// A STUN error code, as carried in `ERROR-CODE`. pub struct ErrorCode(pub u16); impl Setter for ErrorCode { @@ -99,42 +111,59 @@ impl Setter for ErrorCode { } } -// Possible error codes. +/// Possible error codes. pub const CODE_TRY_ALTERNATE: ErrorCode = ErrorCode(300); +/// 400 Bad Request: the request was malformed. pub const CODE_BAD_REQUEST: ErrorCode = ErrorCode(400); +/// 401 Unauthorized: authentication is required, or the credentials were wrong. pub const CODE_UNAUTHORIZED: ErrorCode = ErrorCode(401); +/// 420 Unknown Attribute: the request carried a comprehension-required attribute the server +/// does not understand. pub const CODE_UNKNOWN_ATTRIBUTE: ErrorCode = ErrorCode(420); +/// 438 Stale Nonce: the nonce expired; retry with the one in this response. pub const CODE_STALE_NONCE: ErrorCode = ErrorCode(438); +/// 487 Role Conflict: both ICE agents claimed the same role. pub const CODE_ROLE_CONFLICT: ErrorCode = ErrorCode(487); +/// 500 Server Error: a temporary failure on the server. pub const CODE_SERVER_ERROR: ErrorCode = ErrorCode(500); -// DEPRECATED constants. -// DEPRECATED, use CODE_UNAUTHORIZED. +/// DEPRECATED constants. +/// DEPRECATED, use CODE_UNAUTHORIZED. pub const CODE_UNAUTHORISED: ErrorCode = CODE_UNAUTHORIZED; -// Error codes from RFC 5766. -// -// RFC 5766 Section 15 -pub const CODE_FORBIDDEN: ErrorCode = ErrorCode(403); // Forbidden -pub const CODE_ALLOC_MISMATCH: ErrorCode = ErrorCode(437); // Allocation Mismatch -pub const CODE_WRONG_CREDENTIALS: ErrorCode = ErrorCode(441); // Wrong Credentials -pub const CODE_UNSUPPORTED_TRANS_PROTO: ErrorCode = ErrorCode(442); // Unsupported Transport Protocol -pub const CODE_ALLOC_QUOTA_REACHED: ErrorCode = ErrorCode(486); // Allocation Quota Reached -pub const CODE_INSUFFICIENT_CAPACITY: ErrorCode = ErrorCode(508); // Insufficient Capacity - -// Error codes from RFC 6062. -// -// RFC 6062 Section 6.3 +/// Error codes from RFC 5766. +/// +/// RFC 5766 Section 15. +/// Forbidden. +pub const CODE_FORBIDDEN: ErrorCode = ErrorCode(403); +/// Allocation Mismatch. +pub const CODE_ALLOC_MISMATCH: ErrorCode = ErrorCode(437); +/// Wrong Credentials. +pub const CODE_WRONG_CREDENTIALS: ErrorCode = ErrorCode(441); +/// Unsupported Transport Protocol. +pub const CODE_UNSUPPORTED_TRANS_PROTO: ErrorCode = ErrorCode(442); +/// Allocation Quota Reached. +pub const CODE_ALLOC_QUOTA_REACHED: ErrorCode = ErrorCode(486); +/// Insufficient Capacity. +pub const CODE_INSUFFICIENT_CAPACITY: ErrorCode = ErrorCode(508); + +/// Error codes from RFC 6062. +/// +/// RFC 6062 Section 6.3. pub const CODE_CONN_ALREADY_EXISTS: ErrorCode = ErrorCode(446); +/// 447 Connection Timeout or Failure: the TURN TCP connection to the peer failed. pub const CODE_CONN_TIMEOUT_OR_FAILURE: ErrorCode = ErrorCode(447); -// Error codes from RFC 6156. -// -// RFC 6156 Section 10.2 -pub const CODE_ADDR_FAMILY_NOT_SUPPORTED: ErrorCode = ErrorCode(440); // Address Family not Supported -pub const CODE_PEER_ADDR_FAMILY_MISMATCH: ErrorCode = ErrorCode(443); // Peer Address Family Mismatch +/// Error codes from RFC 6156. +/// +/// RFC 6156 Section 10.2. +/// Address Family not Supported. +pub const CODE_ADDR_FAMILY_NOT_SUPPORTED: ErrorCode = ErrorCode(440); +/// Peer Address Family Mismatch. +pub const CODE_PEER_ADDR_FAMILY_MISMATCH: ErrorCode = ErrorCode(443); lazy_static! { + /// The reason phrase each known [`ErrorCode`] is sent with. pub static ref ERROR_REASONS:HashMap> = [ (CODE_TRY_ALTERNATE, b"Try Alternate".to_vec()), diff --git a/rtc-stun/src/fingerprint.rs b/rtc-stun/src/fingerprint.rs index 0ec73bb5..26e70fb6 100644 --- a/rtc-stun/src/fingerprint.rs +++ b/rtc-stun/src/fingerprint.rs @@ -8,20 +8,22 @@ use shared::error::*; use crc::{CRC_32_ISO_HDLC, Crc, Table}; -// FingerprintAttr represents FINGERPRINT attribute. -// -// RFC 5389 Section 15.5 +/// FINGERPRINT attribute. +/// +/// RFC 5389 Section 15.5. pub struct FingerprintAttr; -// FINGERPRINT is shorthand for FingerprintAttr. -// -// Example: -// -// m := New() -// FINGERPRINT.add_to(m) +/// Shorthand for FingerprintAttr. +/// +/// Example: +/// +/// m := New() +/// FINGERPRINT.add_to(m). pub const FINGERPRINT: FingerprintAttr = FingerprintAttr {}; +/// The value the CRC-32 is XORed with, `0x5354554e` — ASCII `STUN`. pub const FINGERPRINT_XOR_VALUE: u32 = 0x5354554e; +/// The attribute's value length in bytes. pub const FINGERPRINT_SIZE: usize = 4; // 32 bit // FingerprintValue returns CRC-32 of b XOR-ed by 0x5354554e. @@ -37,6 +39,7 @@ pub const FINGERPRINT_SIZE: usize = 4; // 32 bit /// messages (one fingerprint per ICE connectivity check / consent probe). static CRC_32: Crc> = Crc::>::new(&CRC_32_ISO_HDLC); +/// Computes the `FINGERPRINT` value over `b`: CRC-32 XORed with [`FINGERPRINT_XOR_VALUE`]. pub fn fingerprint_value(b: &[u8]) -> u32 { let checksum = CRC_32.checksum(b); checksum ^ FINGERPRINT_XOR_VALUE // XOR @@ -58,8 +61,8 @@ impl Setter for FingerprintAttr { } impl FingerprintAttr { - // Check reads fingerprint value from m and checks it, returning error if any. - // Can return *AttrLengthErr, ErrAttributeNotFound, and *CRCMismatch. + /// Check reads fingerprint value from m and checks it, returning error if any. + /// Can return *AttrLengthErr, ErrAttributeNotFound, and *CRCMismatch. pub fn check(&self, m: &Message) -> Result<()> { let b = m.get(ATTR_FINGERPRINT)?; check_size(ATTR_FINGERPRINT, b.len(), FINGERPRINT_SIZE)?; diff --git a/rtc-stun/src/integrity.rs b/rtc-stun/src/integrity.rs index 8ac340fd..4006d6f9 100644 --- a/rtc-stun/src/integrity.rs +++ b/rtc-stun/src/integrity.rs @@ -2,12 +2,9 @@ mod integrity_test; use crate::attributes::*; -use crate::checks::*; use crate::message::*; -use md5::{Digest, Md5}; +use crypto::{CryptoError, HashAlgorithm, HmacAlgorithm, RTCCrypto, SecretVec}; use shared::error::*; - -use ring::hmac; use std::fmt; // separator for credentials. @@ -19,21 +16,30 @@ pub(crate) const CREDENTIALS_SEP: &str = ":"; // newHMAC function and internal/hmac/pool.go. // // RFC 5389 Section 15.4 -#[derive(Default, Clone)] -pub struct MessageIntegrity(pub Vec); +#[derive(Clone)] +/// The `MESSAGE-INTEGRITY` key: an HMAC-SHA1 is computed over the message with it. +/// +/// Built from a short-term password, or from a long-term username/realm/password triple. +pub struct MessageIntegrity<'a> { + key: SecretVec, + crypto: &'a dyn RTCCrypto, +} -fn new_hmac(key: &[u8], message: &[u8]) -> Vec { - let mac = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, key); - hmac::sign(&mac, message).as_ref().to_vec() +fn crypto_error(error: CryptoError) -> Error { + Error::Crypto(error.to_string()) } -impl fmt::Display for MessageIntegrity { +impl<'a> fmt::Display for MessageIntegrity<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "KEY: 0x{:x?}", self.0) + write!( + f, + "MESSAGE-INTEGRITY key: [REDACTED; {} bytes]", + self.key.len() + ) } } -impl Setter for MessageIntegrity { +impl<'a> Setter for MessageIntegrity<'a> { // add_to adds MESSAGE-INTEGRITY attribute to message. // // CPU costly, see BenchmarkMessageIntegrity_AddTo. @@ -52,10 +58,18 @@ impl Setter for MessageIntegrity { // Adjusting m.Length to contain MESSAGE-INTEGRITY TLV. m.length += (MESSAGE_INTEGRITY_SIZE + ATTRIBUTE_HEADER_SIZE) as u32; m.write_length(); // writing length to m.Raw - let v = new_hmac(&self.0, &m.raw); // calculating HMAC for adjusted m.Raw + let mut value = [0_u8; MESSAGE_INTEGRITY_SIZE]; + // A STUN message is authenticated once, so the MAC is keyed here rather than held. On a + // per-packet path the keyed object belongs in the surrounding state instead. + let result = self + .crypto + .new_hmac(HmacAlgorithm::Sha1, self.key.as_ref()) + .and_then(|mut mac| mac.sign(&[&m.raw], &mut value)); m.length = length; // changing m.Length back + m.write_length(); + result.map_err(crypto_error)?; - m.add(ATTR_MESSAGE_INTEGRITY, &v); + m.add(ATTR_MESSAGE_INTEGRITY, &value); Ok(()) } @@ -63,28 +77,63 @@ impl Setter for MessageIntegrity { pub(crate) const MESSAGE_INTEGRITY_SIZE: usize = 20; -impl MessageIntegrity { - // new_long_term_integrity returns new MessageIntegrity with key for long-term - // credentials. Password, username, and realm must be SASL-prepared. - pub fn new_long_term_integrity(username: String, realm: String, password: String) -> Self { - let s = [username, realm, password].join(CREDENTIALS_SEP); +impl<'a> MessageIntegrity<'a> { + /// Creates a raw-key integrity attribute with an explicit crypto provider. + #[must_use] + pub fn new_raw_integrity_with_provider( + key: impl Into>, + crypto: &'a dyn RTCCrypto, + ) -> Self { + Self { + key: SecretVec::new(key.into()), + crypto, + } + } - let mut h = Md5::new(); - h.update(s.as_bytes()); + /// Creates a short-term integrity attribute with an explicit crypto provider. + #[must_use] + pub fn new_short_term_integrity_with_provider( + password: String, + crypto: &'a dyn RTCCrypto, + ) -> Self { + Self::new_raw_integrity_with_provider(password.into_bytes(), crypto) + } - MessageIntegrity(h.finalize().as_slice().to_vec()) + /// Creates a long-term integrity attribute with an explicit crypto provider. + pub fn new_long_term_integrity_with_provider( + username: String, + realm: String, + password: String, + crypto: &'a dyn RTCCrypto, + ) -> Result { + let key = MessageIntegrity::long_term_integrity_key(username, realm, password, crypto)?; + Ok(Self::new_raw_integrity_with_provider(key, crypto)) } - // new_short_term_integrity returns new MessageIntegrity with key for short-term - // credentials. Password must be SASL-prepared. - pub fn new_short_term_integrity(password: String) -> Self { - MessageIntegrity(password.as_bytes().to_vec()) + /// Creates a long-term integrity key with an explicit crypto provider. + pub fn long_term_integrity_key( + username: String, + realm: String, + password: String, + crypto: &'a dyn RTCCrypto, + ) -> Result> { + let credentials = [username, realm, password].join(CREDENTIALS_SEP); + let key = crypto + .hash(HashAlgorithm::Md5, credentials.as_bytes()) + .map_err(crypto_error)?; + if key.len() != 16 { + return Err(Error::Crypto(format!( + "provider returned an invalid MD5 digest length: {}", + key.len() + ))); + } + Ok(key) } - // Check checks MESSAGE-INTEGRITY attribute. - // - // CPU costly, see BenchmarkMessageIntegrity_Check. - pub fn check(&self, m: &mut Message) -> Result<()> { + /// Check checks MESSAGE-INTEGRITY attribute. + /// + /// CPU costly, see BenchmarkMessageIntegrity_Check. + pub fn check(m: &mut Message, key: &[u8], crypto: &dyn RTCCrypto) -> Result<()> { let v = m.get(ATTR_MESSAGE_INTEGRITY)?; // Adjusting length in header to match m.Raw that was @@ -109,9 +158,17 @@ impl MessageIntegrity { let start_of_hmac = MESSAGE_HEADER_SIZE + m.length as usize - (ATTRIBUTE_HEADER_SIZE + MESSAGE_INTEGRITY_SIZE); let b = &m.raw[..start_of_hmac]; // data before integrity attribute - let expected = new_hmac(&self.0, b); + let result = crypto + .new_hmac(HmacAlgorithm::Sha1, key) + .and_then(|mut mac| mac.verify(&[b], &v)); m.length = length as u32; m.write_length(); // writing length back - check_hmac(&v, &expected) + match result { + Ok(()) => Ok(()), + Err(CryptoError::AuthenticationFailed | CryptoError::InvalidTagLength { .. }) => { + Err(Error::ErrIntegrityMismatch) + } + Err(error) => Err(crypto_error(error)), + } } } diff --git a/rtc-stun/src/integrity/integrity_test.rs b/rtc-stun/src/integrity/integrity_test.rs index a3992e69..1e8833a7 100644 --- a/rtc-stun/src/integrity/integrity_test.rs +++ b/rtc-stun/src/integrity/integrity_test.rs @@ -3,31 +3,204 @@ use crate::attributes::ATTR_SOFTWARE; use crate::fingerprint::FINGERPRINT; use crate::message::TransactionId; use crate::textattrs::TextAttribute; +use crypto::{ + CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, RTCCrypto, RTCCryptoProvider, + RTCRandom, constant_time_eq, +}; +use std::sync::Arc; + +struct TestProvider { + crypto: TestCrypto, + random: TestRandom, +} + +struct TestCrypto; + +struct TestRandom; + +impl RTCCryptoProvider for TestProvider { + fn name(&self) -> &'static str { + "test" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + &self.random + } +} + +impl RTCRandom for TestRandom { + fn fill(&self, output: &mut [u8]) -> std::result::Result<(), CryptoError> { + output.fill(0x42); + Ok(()) + } +} + +impl RTCCrypto for TestCrypto { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + matches!( + algorithm, + CryptoAlgorithm::Hash(HashAlgorithm::Md5) | CryptoAlgorithm::Hmac(HmacAlgorithm::Sha1) + ) + } + + fn hash( + &self, + algorithm: HashAlgorithm, + data: &[u8], + ) -> std::result::Result, CryptoError> { + if algorithm != HashAlgorithm::Md5 { + return Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hash( + algorithm, + ))); + } + let mut output = vec![0_u8; 16]; + let output_len = output.len(); + for (index, byte) in data.iter().enumerate() { + output[index % output_len] ^= byte; + } + Ok(output) + } + + fn new_hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + ) -> std::result::Result, CryptoError> { + if algorithm != HmacAlgorithm::Sha1 { + return Err(CryptoError::UnsupportedAlgorithm(CryptoAlgorithm::Hmac( + algorithm, + ))); + } + Ok(Box::new(TestMac { + key: key.to_vec(), + output_len: algorithm.output_len(), + })) + } +} + +/// A deliberately fake MAC: an XOR fold, not HMAC-SHA1. It exists to prove the custom-provider +/// path is honoured, so tests asserting RFC 5389 vectors must use `builtin_provider()` instead. +struct TestMac { + key: Vec, + output_len: usize, +} + +impl crypto::Mac for TestMac { + fn output_len(&self) -> usize { + self.output_len + } + + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> std::result::Result<(), CryptoError> { + if output.len() != self.output_len { + return Err(CryptoError::InvalidTagLength { + expected: self.output_len, + actual: output.len(), + }); + } + output.fill(0); + for (index, byte) in self + .key + .iter() + .chain(input.iter().flat_map(|part| part.iter())) + .enumerate() + { + output[index % self.output_len] ^= byte; + } + Ok(()) + } + + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> std::result::Result<(), CryptoError> { + if expected.len() != self.output_len { + return Err(CryptoError::InvalidTagLength { + expected: self.output_len, + actual: expected.len(), + }); + } + let mut actual = vec![0_u8; self.output_len]; + self.sign(input, &mut actual)?; + if constant_time_eq(&actual, expected) { + Ok(()) + } else { + Err(CryptoError::AuthenticationFailed) + } + } +} + +fn test_provider() -> Arc { + Arc::new(TestProvider { + crypto: TestCrypto, + random: TestRandom, + }) +} + +/// A real built-in provider. Tests asserting RFC 5389 key/tag vectors need genuine MD5 and +/// HMAC-SHA1, not the `TestProvider` stand-in above. +fn builtin_provider() -> Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + +#[test] +fn explicit_custom_provider_round_trip_and_truncated_tag_rejection() -> Result<()> { + let provider = test_provider(); + let integrity = MessageIntegrity::new_long_term_integrity_with_provider( + "user".to_owned(), + "realm".to_owned(), + "password".to_owned(), + provider.crypto(), + )?; + let mut message = Message::new(); + message.write_header(); + integrity.add_to(&mut message)?; + MessageIntegrity::check(&mut message, integrity.key.as_ref(), provider.crypto())?; + + let attribute = message + .attributes + .0 + .iter_mut() + .find(|attribute| attribute.typ == ATTR_MESSAGE_INTEGRITY) + .expect("MESSAGE-INTEGRITY attribute"); + attribute.value.pop(); + attribute.length -= 1; + assert_eq!( + MessageIntegrity::check(&mut message, integrity.key.as_ref(), provider.crypto()), + Err(Error::ErrIntegrityMismatch) + ); + + Ok(()) +} #[test] fn test_message_integrity_add_to_simple() -> Result<()> { { - let i = MessageIntegrity::new_long_term_integrity( + let provider = builtin_provider(); + let i = MessageIntegrity::new_long_term_integrity_with_provider( "user".to_owned(), "realm".to_owned(), "passsss".to_owned(), - ); + provider.crypto(), + )?; let expected = vec![ 104, 228, 91, 113, 61, 154, 222, 34, 101, 61, 181, 146, 177, 90, 4, 29, ]; - assert_eq!(i.0, expected, "{}", Error::ErrIntegrityMismatch); + assert_eq!(i.key.as_ref(), expected, "{}", Error::ErrIntegrityMismatch); } - let i = MessageIntegrity::new_long_term_integrity( + let provider = builtin_provider(); + let i = MessageIntegrity::new_long_term_integrity_with_provider( "user".to_owned(), "realm".to_owned(), "pass".to_owned(), - ); + provider.crypto(), + )?; let expected = vec![ 0x84, 0x93, 0xfb, 0xc5, 0x3b, 0xa5, 0x82, 0xfb, 0x4c, 0x04, 0x4c, 0x45, 0x6b, 0xdc, 0x40, 0xeb, ]; - assert_eq!(i.0, expected, "{}", Error::ErrIntegrityMismatch); + assert_eq!(i.key.as_ref(), expected, "{}", Error::ErrIntegrityMismatch); //"Check" { @@ -44,11 +217,11 @@ fn test_message_integrity_add_to_simple() -> Result<()> { let mut d_m = Message::new(); d_m.raw = m.raw.clone(); d_m.decode()?; - i.check(&mut d_m)?; + MessageIntegrity::check(&mut d_m, i.key.as_ref(), provider.crypto())?; d_m.raw[24] += 12; // HMAC now invalid d_m.decode()?; - let result = i.check(&mut d_m); + let result = MessageIntegrity::check(&mut d_m, i.key.as_ref(), provider.crypto()); assert!(result.is_err(), "should be invalid"); } @@ -66,17 +239,25 @@ fn test_message_integrity_with_fingerprint() -> Result<()> { }; a.add_to(&mut m)?; - let i = MessageIntegrity::new_short_term_integrity("pwd".to_owned()); - assert_eq!(i.to_string(), "KEY: 0x[70, 77, 64]", "bad string {i}"); - let result = i.check(&mut m); + let provider = builtin_provider(); + let i = MessageIntegrity::new_short_term_integrity_with_provider( + "pwd".to_owned(), + provider.crypto(), + ); + assert_eq!( + i.to_string(), + "MESSAGE-INTEGRITY key: [REDACTED; 3 bytes]", + "bad string {i}" + ); + let result = MessageIntegrity::check(&mut m, i.key.as_ref(), provider.crypto()); assert!(result.is_err(), "should error"); i.add_to(&mut m)?; FINGERPRINT.add_to(&mut m)?; - i.check(&mut m)?; + MessageIntegrity::check(&mut m, i.key.as_ref(), provider.crypto())?; m.raw[24] = 33; m.decode()?; - let result = i.check(&mut m); + let result = MessageIntegrity::check(&mut m, i.key.as_ref(), provider.crypto()); assert!(result.is_err(), "mismatch expected"); Ok(()) @@ -85,7 +266,11 @@ fn test_message_integrity_with_fingerprint() -> Result<()> { #[test] fn test_message_integrity() -> Result<()> { let mut m = Message::new(); - let i = MessageIntegrity::new_short_term_integrity("password".to_owned()); + let provider = builtin_provider(); + let i = MessageIntegrity::new_short_term_integrity_with_provider( + "password".to_owned(), + provider.crypto(), + ); m.write_header(); i.add_to(&mut m)?; m.get(ATTR_MESSAGE_INTEGRITY)?; @@ -97,7 +282,11 @@ fn test_message_integrity_before_fingerprint() -> Result<()> { let mut m = Message::new(); m.write_header(); FINGERPRINT.add_to(&mut m)?; - let i = MessageIntegrity::new_short_term_integrity("password".to_owned()); + let provider = builtin_provider(); + let i = MessageIntegrity::new_short_term_integrity_with_provider( + "password".to_owned(), + provider.crypto(), + ); let result = i.add_to(&mut m); assert!(result.is_err(), "should error"); diff --git a/rtc-stun/src/lib.rs b/rtc-stun/src/lib.rs index ccaf4b32..57769974 100644 --- a/rtc-stun/src/lib.rs +++ b/rtc-stun/src/lib.rs @@ -1,30 +1,101 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! STUN for the Sans-I/O WebRTC stack. +//! +//! Session Traversal Utilities for NAT ([RFC 5389], superseding [RFC 3489]), plus the +//! attributes ICE ([RFC 8445]) and TURN ([RFC 5766]) layer on top. In WebRTC, STUN does +//! double duty: it discovers a peer's server-reflexive address, and its binding +//! request/response exchange *is* the ICE connectivity check. +//! +//! # Structure +//! +//! * [`message`] — [`Message`](message::Message), the STUN message itself: build one from +//! attributes, marshal it, unmarshal one off the wire. +//! * [`attributes`], [`textattrs`], [`uattrs`], [`xoraddr`], [`error_code`] — the attribute +//! types, including `XOR-MAPPED-ADDRESS`, `USERNAME`, `REALM` and `ERROR-CODE`. +//! * [`integrity`], [`fingerprint`] — `MESSAGE-INTEGRITY` (HMAC-SHA1) and `FINGERPRINT` +//! (CRC-32), the two attributes whose values depend on the encoded message. +//! * [`agent`], [`client`] — transaction tracking and a Sans-I/O client for talking to a +//! STUN server. +//! * [`uri`] — parsing `stun:`/`stuns:` URLs. +//! * [`checks`] — validation helpers for received messages. +//! +//! # Example +//! +//! ``` +//! use rtc_stun::attributes::ATTR_SOFTWARE; +//! use rtc_stun::message::{BINDING_REQUEST, Message, TransactionId}; +//! use rtc_stun::textattrs::TextAttribute; +//! +//! # fn example() -> Result<(), Box> { +//! let mut msg = Message::new(); +//! msg.build(&[ +//! Box::new(TransactionId::new()), +//! Box::new(BINDING_REQUEST), +//! Box::new(TextAttribute::new(ATTR_SOFTWARE, "webrtc-rs".to_owned())), +//! ])?; +//! +//! // `build` encodes as it goes, so `raw` is ready to send. +//! assert!(!msg.raw.is_empty()); +//! +//! let mut decoded = Message::new(); +//! decoded.raw = msg.raw.clone(); +//! decoded.decode()?; +//! assert_eq!(decoded.typ, BINDING_REQUEST); +//! # Ok(()) +//! # } +//! ``` +//! +//! Most applications do not depend on this crate directly — [`rtc-ice`] and +//! [`rtc-turn`] build on it, and the [`rtc`](https://docs.rs/rtc) crate drives those. +//! +//! [RFC 5389]: https://datatracker.ietf.org/doc/html/rfc5389 +//! [RFC 3489]: https://datatracker.ietf.org/doc/html/rfc3489 +//! [RFC 8445]: https://datatracker.ietf.org/doc/html/rfc8445 +//! [RFC 5766]: https://datatracker.ietf.org/doc/html/rfc5766 +//! [`rtc-ice`]: https://docs.rs/rtc-ice +//! [`rtc-turn`]: https://docs.rs/rtc-turn + #[macro_use] extern crate lazy_static; +/// The crypto provider API. +/// +/// Re-exported because this crate's public constructors take an +/// [`Arc`](crypto::RTCCryptoProvider), which a caller must be able to name +/// without adding — and version-matching — a direct `rtc-crypto` dependency. +pub use crypto; + +/// Socket-address helpers shared by the address attributes. pub mod addr; +/// Transaction tracking: which requests are outstanding and when they time out. pub mod agent; +/// The STUN attribute types and the raw attribute representation. pub mod attributes; +/// Validation helpers for received messages and attributes. pub mod checks; +/// A Sans-I/O STUN client for talking to a STUN server. pub mod client; +/// The `ERROR-CODE` attribute and the codes defined by STUN, TURN and ICE. pub mod error_code; +/// The `FINGERPRINT` attribute, a CRC-32 over the message. pub mod fingerprint; +/// The `MESSAGE-INTEGRITY` attribute, an HMAC-SHA1 over the message. pub mod integrity; +/// The STUN message itself: header, attributes, and encoding. pub mod message; +/// Text-valued attributes such as `USERNAME`, `REALM` and `SOFTWARE`. pub mod textattrs; +/// The `UNKNOWN-ATTRIBUTES` attribute, listing attributes a server could not process. pub mod uattrs; +/// Parsing `stun:` and `stuns:` URIs. pub mod uri; +/// The `XOR-MAPPED-ADDRESS` attribute, whose value is masked with the magic cookie. pub mod xoraddr; -// IANA assigned ports for "stun" protocol. +/// IANA assigned ports for "stun" protocol. pub const DEFAULT_PORT: u16 = 3478; +/// The default port for `stuns:` (STUN over TLS/DTLS). pub const DEFAULT_TLS_PORT: u16 = 5349; - -#[cfg(all(feature = "aws-lc-rs", feature = "ring"))] -compile_error!("At most one of the features \"aws-lc-rs\" and \"ring\" can be enabled."); -#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))] -compile_error!("At least one of the features \"aws-lc-rs\" and \"ring\" must be enabled."); -#[cfg(feature = "aws-lc-rs")] -extern crate aws_lc_rs as ring; diff --git a/rtc-stun/src/message.rs b/rtc-stun/src/message.rs index bf15477e..02d1efc1 100644 --- a/rtc-stun/src/message.rs +++ b/rtc-stun/src/message.rs @@ -1,3 +1,14 @@ +//! The STUN message: header, attributes, and encoding. +//! +//! A [`Message`](crate::message::Message) is a class and method ([`MessageType`](crate::message::MessageType)), a 96-bit [`TransactionId`](crate::message::TransactionId), and a +//! list of attributes. Build one by applying [`Setter`](crate::message::Setter)s, read one back with [`Getter`](crate::message::Getter)s, and +//! move it across the wire with `marshal`/`unmarshal`. +//! +//! Two attributes are special, because their value covers the *encoded* message: `FINGERPRINT` +//! and `MESSAGE-INTEGRITY` must be appended last and are validated through [`Checker`](crate::message::Checker). That is +//! why [`Message::raw`](crate::message::Message::raw) is kept alongside the parsed attributes — those checks are computed over +//! it rather than over a re-encoding. + #[cfg(test)] mod message_test; @@ -9,27 +20,33 @@ use rand::RngExt; use std::fmt; use std::io::{Read, Write}; -// MAGIC_COOKIE is fixed value that aids in distinguishing STUN packets -// from packets of other protocols when STUN is multiplexed with those -// other protocols on the same Port. -// -// The magic cookie field MUST contain the fixed value 0x2112A442 in -// network byte order. -// -// Defined in "STUN Message Structure", section 6. +/// Fixed value that aids in distinguishing STUN packets +/// from packets of other protocols when STUN is multiplexed with those +/// other protocols on the same Port. +/// +/// The magic cookie field MUST contain the fixed value 0x2112A442 in +/// network byte order. +/// +/// Defined in "STUN Message Structure", section 6. pub const MAGIC_COOKIE: u32 = 0x2112A442; +/// Bytes of type and length preceding each attribute value. pub const ATTRIBUTE_HEADER_SIZE: usize = 4; +/// Bytes in the STUN message header. pub const MESSAGE_HEADER_SIZE: usize = 20; -// TRANSACTION_ID_SIZE is length of transaction id array (in bytes). -pub const TRANSACTION_ID_SIZE: usize = 12; // 96 bit +/// Length of transaction id array (in bytes). +/// 96 bit. +pub const TRANSACTION_ID_SIZE: usize = 12; #[derive(PartialEq, Eq, Hash, Copy, Clone, Default, Debug)] +/// A 96-bit transaction id, which pairs a response with its request. pub struct TransactionId(pub [u8; TRANSACTION_ID_SIZE]); impl TransactionId { - /// new returns new random transaction ID using crypto/rand - /// as source. + /// Creates a random transaction ID with `rand`'s cryptographically secure thread RNG. + /// + /// Transaction IDs remain independent of [`crypto::RTCCryptoProvider`] because ordinary STUN + /// message construction does not otherwise own a provider. pub fn new() -> Self { let mut b = TransactionId([0u8; TRANSACTION_ID_SIZE]); rand::rng().fill(&mut b.0); @@ -45,26 +62,44 @@ impl Setter for TransactionId { } } -// Interfaces that are implemented by message attributes, shorthands for them, -// or helpers for message fields as type or transaction id. +/// Interfaces that are implemented by message attributes, shorthands for them, +/// or helpers for message fields as type or transaction id. pub trait Setter { // Setter sets *Message attribute. + /// Encodes this value into `m` as an attribute. + /// + /// # Errors + /// + /// Fails if the value cannot be encoded, or the message has no room for it. fn add_to(&self, m: &mut Message) -> Result<()>; } -// Getter parses attribute from *Message. +/// Getter parses attribute from *Message. pub trait Getter { + /// Decodes this value from the corresponding attribute of `m`. + /// + /// # Errors + /// + /// Fails if the attribute is absent or malformed. fn get_from(&mut self, m: &Message) -> Result<()>; } -// Checker checks *Message attribute. +/// Checker checks *Message attribute. pub trait Checker { + /// Validates this value against `m`. + /// + /// Used by attributes whose value depends on the encoded message, such as + /// `MESSAGE-INTEGRITY` and `FINGERPRINT`. + /// + /// # Errors + /// + /// Fails if the check does not hold. fn check(&self, m: &Message) -> Result<()>; } -// is_stun_message returns true if b looks like STUN message. -// Useful for multiplexing. is_stun_message does not guarantee -// that decoding will be successful. +/// Is_stun_message returns true if b looks like STUN message. +/// Useful for multiplexing. is_stun_message does not guarantee +/// that decoding will be successful. pub fn is_stun_message(b: &[u8]) -> bool { b.len() >= MESSAGE_HEADER_SIZE && u32::from_be_bytes([b[4], b[5], b[6], b[7]]) == MAGIC_COOKIE } @@ -75,11 +110,17 @@ pub fn is_stun_message(b: &[u8]) -> bool { // Message, its fields, results of m.Get or any attribute a.GetFrom // are valid only until Message.Raw is not modified. #[derive(Default, Debug, Clone)] +/// A STUN message: type, transaction id, and attributes, plus the encoded bytes. pub struct Message { + /// The message class and method — request, response or indication, and which method. pub typ: MessageType, + /// The attribute section's length in bytes, header excluded. pub length: u32, // len(Raw) not including header + /// The transaction id, echoed by the responder. pub transaction_id: TransactionId, + /// The message's attributes. pub attributes: Attributes, + /// The encoded message. Attributes whose value covers the message are computed over this. pub raw: Vec, } @@ -131,7 +172,7 @@ impl Setter for Message { } impl Message { - // New returns *Message with pre-allocated Raw. + /// New returns *Message with pre-allocated Raw. pub fn new() -> Self { Message { raw: { @@ -143,14 +184,14 @@ impl Message { } } - // marshal_binary implements the encoding.BinaryMarshaler interface. + /// Marshal_binary implements the encoding.BinaryMarshaler interface. pub fn marshal_binary(&self) -> Result> { // We can't return m.Raw, allocation is expected by implicit interface // contract induced by other implementations. Ok(self.raw.clone()) } - // unmarshal_binary implements the encoding.BinaryUnmarshaler interface. + /// Unmarshal_binary implements the encoding.BinaryUnmarshaler interface. pub fn unmarshal_binary(&mut self, data: &[u8]) -> Result<()> { // We can't retain data, copy is expected by interface contract. self.raw.clear(); @@ -158,15 +199,17 @@ impl Message { self.decode() } - // NewTransactionID sets m.TransactionID to random value from crypto/rand - // and returns error if any. + /// Replaces the transaction ID using `rand`'s cryptographically secure thread RNG. + /// + /// Transaction IDs remain independent of [`crypto::RTCCryptoProvider`] because a general + /// [`Message`] does not otherwise own a provider. pub fn new_transaction_id(&mut self) -> Result<()> { rand::rng().fill(&mut self.transaction_id.0); self.write_transaction_id(); Ok(()) } - // Reset resets Message, attributes and underlying buffer length. + /// Reset resets Message, attributes and underlying buffer length. pub fn reset(&mut self) { self.raw.clear(); self.length = 0; @@ -184,10 +227,10 @@ impl Message { self.raw.extend_from_slice(&vec![0; n - self.raw.len()]); } - // Add appends new attribute to message. Not goroutine-safe. - // - // Value of attribute is copied to internal buffer so - // it is safe to reuse v. + /// Add appends new attribute to message. Not goroutine-safe. + /// + /// Value of attribute is copied to internal buffer so + /// it is safe to reuse v. pub fn add(&mut self, t: AttrType, v: &[u8]) { // Allocating buffer for TLV (type-length-value). // T = t, L = len(v), V = v. @@ -237,13 +280,13 @@ impl Message { self.write_length(); } - // WriteLength writes m.Length to m.Raw. + /// WriteLength writes m.Length to m.Raw. pub fn write_length(&mut self) { self.grow(4, false); self.raw[2..4].copy_from_slice(&(self.length as u16).to_be_bytes()); } - // WriteHeader writes header to underlying buffer. Not goroutine-safe. + /// WriteHeader writes header to underlying buffer. Not goroutine-safe. pub fn write_header(&mut self) { self.grow(MESSAGE_HEADER_SIZE, false); @@ -254,13 +297,13 @@ impl Message { // transaction ID } - // WriteTransactionID writes m.TransactionID to m.Raw. + /// WriteTransactionID writes m.TransactionID to m.Raw. pub fn write_transaction_id(&mut self) { self.raw[8..MESSAGE_HEADER_SIZE].copy_from_slice(&self.transaction_id.0); // transaction ID } - // WriteAttributes encodes all m.Attributes to m. + /// WriteAttributes encodes all m.Attributes to m. pub fn write_attributes(&mut self) { let attributes: Vec = self.attributes.0.drain(..).collect(); for a in &attributes { @@ -269,19 +312,19 @@ impl Message { self.attributes = Attributes(attributes); } - // WriteType writes m.Type to m.Raw. + /// WriteType writes m.Type to m.Raw. pub fn write_type(&mut self) { self.grow(2, false); self.raw[..2].copy_from_slice(&self.typ.value().to_be_bytes()); // message type } - // SetType sets m.Type and writes it to m.Raw. + /// SetType sets m.Type and writes it to m.Raw. pub fn set_type(&mut self, t: MessageType) { self.typ = t; self.write_type(); } - // Encode re-encodes message into m.Raw. + /// Encode re-encodes message into m.Raw. pub fn encode(&mut self) { self.raw.clear(); self.write_header(); @@ -289,7 +332,7 @@ impl Message { self.write_attributes(); } - // Decode decodes m.Raw into m. + /// Decode decodes m.Raw into m. pub fn decode(&mut self) -> Result<()> { // decoding message header let buf = &self.raw; @@ -365,18 +408,18 @@ impl Message { Ok(()) } - // WriteTo implements WriterTo via calling Write(m.Raw) on w and returning - // call result. + /// WriteTo implements WriterTo via calling Write(m.Raw) on w and returning + /// call result. pub fn write_to(&self, writer: &mut W) -> Result { let n = writer.write(&self.raw)?; Ok(n) } - // ReadFrom implements ReaderFrom. Reads message from r into m.Raw, - // Decodes it and return error if any. If m.Raw is too small, will return - // ErrUnexpectedEOF, ErrUnexpectedHeaderEOF or *DecodeErr. - // - // Can return *DecodeErr while decoding too. + /// ReadFrom implements ReaderFrom. Reads message from r into m.Raw, + /// Decodes it and return error if any. If m.Raw is too small, will return + /// ErrUnexpectedEOF, ErrUnexpectedHeaderEOF or *DecodeErr. + /// + /// Can return *DecodeErr while decoding too. pub fn read_from(&mut self, reader: &mut R) -> Result { let mut t_buf = vec![0; DEFAULT_RAW_CAPACITY]; let n = reader.read(&mut t_buf)?; @@ -385,9 +428,9 @@ impl Message { Ok(n) } - // Write decodes message and return error if any. - // - // Any error is unrecoverable, but message could be partially decoded. + /// Write decodes message and return error if any. + /// + /// Any error is unrecoverable, but message could be partially decoded. pub fn write(&mut self, t_buf: &[u8]) -> Result { self.raw.clear(); self.raw.extend_from_slice(t_buf); @@ -395,14 +438,14 @@ impl Message { Ok(t_buf.len()) } - // CloneTo clones m to b securing any further m mutations. + /// CloneTo clones m to b securing any further m mutations. pub fn clone_to(&self, b: &mut Message) -> Result<()> { b.raw.clear(); b.raw.extend_from_slice(&self.raw); b.decode() } - // Contains return true if message contain t attribute. + /// Contains return true if message contain t attribute. pub fn contains(&self, t: AttrType) -> bool { for a in &self.attributes.0 { if a.typ == t { @@ -412,9 +455,9 @@ impl Message { false } - // get returns byte slice that represents attribute value, - // if there is no attribute with such type, - // ErrAttributeNotFound is returned. + /// Get returns byte slice that represents attribute value, + /// if there is no attribute with such type, + /// ErrAttributeNotFound is returned. pub fn get(&self, t: AttrType) -> Result> { let (v, ok) = self.attributes.get(t); if ok { @@ -424,22 +467,33 @@ impl Message { } } - // Build resets message and applies setters to it in batch, returning on - // first error. To prevent allocations, pass pointers to values. - // - // Example: - // var ( - // t = BindingRequest - // username = NewUsername("username") - // nonce = NewNonce("nonce") - // realm = NewRealm("example.org") - // ) - // m := new(Message) - // m.Build(t, username, nonce, realm) // 4 allocations - // m.Build(&t, &username, &nonce, &realm) // 0 allocations - // - // See BenchmarkBuildOverhead. - pub fn build(&mut self, setters: &[Box]) -> Result<()> { + /// Resets the message and applies `setters` to it in order, returning on the first + /// error. + /// + /// Each setter writes its attribute into the message as it is applied, so the encoded + /// [`raw`](Self::raw) bytes are complete once this returns. Order matters for setters + /// that cover the attributes before them — a + /// [`MessageIntegrity`](crate::integrity::MessageIntegrity) or + /// [`FINGERPRINT`](crate::fingerprint::FINGERPRINT) therefore goes last. + /// + /// ``` + /// use rtc_stun::attributes::ATTR_SOFTWARE; + /// use rtc_stun::message::{BINDING_REQUEST, Message, TransactionId}; + /// use rtc_stun::textattrs::TextAttribute; + /// + /// # fn example() -> Result<(), Box> { + /// let mut m = Message::new(); + /// m.build(&[ + /// Box::new(BINDING_REQUEST), + /// Box::new(TransactionId::new()), + /// Box::new(TextAttribute::new(ATTR_SOFTWARE, "webrtc-rs".to_owned())), + /// ])?; + /// + /// assert!(!m.raw.is_empty()); + /// # Ok(()) + /// # } + /// ``` + pub fn build(&mut self, setters: &[Box]) -> Result<()> { self.reset(); self.write_header(); for s in setters { @@ -448,7 +502,7 @@ impl Message { Ok(()) } - // Check applies checkers to message in batch, returning on first error. + /// Check applies checkers to message in batch, returning on first error. pub fn check(&self, checkers: &[C]) -> Result<()> { for c in checkers { c.check(self)?; @@ -456,7 +510,7 @@ impl Message { Ok(()) } - // Parse applies getters to message in batch, returning on first error. + /// Parse applies getters to message in batch, returning on first error. pub fn parse(&self, getters: &mut [G]) -> Result<()> { for c in getters { c.get_from(self)?; @@ -467,13 +521,18 @@ impl Message { // MessageClass is 8-bit representation of 2-bit class of STUN Message Class. #[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +/// A STUN message class: request, indication, success response or error response. pub struct MessageClass(u8); -// Possible values for message class in STUN Message Type. -pub const CLASS_REQUEST: MessageClass = MessageClass(0x00); // 0b00 -pub const CLASS_INDICATION: MessageClass = MessageClass(0x01); // 0b01 -pub const CLASS_SUCCESS_RESPONSE: MessageClass = MessageClass(0x02); // 0b10 -pub const CLASS_ERROR_RESPONSE: MessageClass = MessageClass(0x03); // 0b11 +/// Possible values for message class in STUN Message Type. +/// 0b00. +pub const CLASS_REQUEST: MessageClass = MessageClass(0x00); +/// 0b01. +pub const CLASS_INDICATION: MessageClass = MessageClass(0x01); +/// 0b10. +pub const CLASS_SUCCESS_RESPONSE: MessageClass = MessageClass(0x02); +/// 0b11. +pub const CLASS_ERROR_RESPONSE: MessageClass = MessageClass(0x03); impl fmt::Display for MessageClass { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -491,20 +550,29 @@ impl fmt::Display for MessageClass { // Method is uint16 representation of 12-bit STUN method. #[derive(Default, PartialEq, Eq, Debug, Copy, Clone)] +/// A STUN method, such as Binding or one of TURN's. pub struct Method(u16); -// Possible methods for STUN Message. +/// Possible methods for STUN Message. pub const METHOD_BINDING: Method = Method(0x001); +/// TURN Allocate: asks a relay for a public address. pub const METHOD_ALLOCATE: Method = Method(0x003); +/// TURN Refresh: extends or releases an allocation. pub const METHOD_REFRESH: Method = Method(0x004); +/// TURN Send: an indication carrying data to a peer through the relay. pub const METHOD_SEND: Method = Method(0x006); +/// TURN Data: an indication carrying data from a peer through the relay. pub const METHOD_DATA: Method = Method(0x007); +/// TURN CreatePermission: authorizes traffic to and from a peer address. pub const METHOD_CREATE_PERMISSION: Method = Method(0x008); +/// TURN ChannelBind: binds a channel number to a peer for compact framing. pub const METHOD_CHANNEL_BIND: Method = Method(0x009); -// Methods from RFC 6062. +/// Methods from RFC 6062. pub const METHOD_CONNECT: Method = Method(0x000a); +/// TURN-TCP ConnectionBind: associates a new TCP connection with an allocation. pub const METHOD_CONNECTION_BIND: Method = Method(0x000b); +/// TURN-TCP ConnectionAttempt: notifies the client of an inbound TCP connection. pub const METHOD_CONNECTION_ATTEMPT: Method = Method(0x000c); impl fmt::Display for Method { @@ -533,23 +601,26 @@ impl fmt::Display for Method { // MessageType is STUN Message Type Field. #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] +/// A message's class and method together, as encoded in the first two header bytes. pub struct MessageType { - pub method: Method, // e.g. binding + /// The method, such as Binding or Allocate. + pub method: Method, // e.g. binding + /// The class: request, indication, success response, or error response. pub class: MessageClass, // e.g. request } -// Common STUN message types. -// Binding request message type. +/// Common STUN message types. +/// Binding request message type. pub const BINDING_REQUEST: MessageType = MessageType { method: METHOD_BINDING, class: CLASS_REQUEST, }; -// Binding success response message type +/// Binding success response message type. pub const BINDING_SUCCESS: MessageType = MessageType { method: METHOD_BINDING, class: CLASS_SUCCESS_RESPONSE, }; -// Binding error response message type. +/// Binding error response message type. pub const BINDING_ERROR: MessageType = MessageType { method: METHOD_BINDING, class: CLASS_ERROR_RESPONSE, @@ -586,12 +657,12 @@ impl Setter for MessageType { } impl MessageType { - // NewType returns new message type with provided method and class. + /// NewType returns new message type with provided method and class. pub fn new(method: Method, class: MessageClass) -> Self { MessageType { method, class } } - // Value returns bit representation of messageType. + /// Value returns bit representation of messageType. pub fn value(&self) -> u16 { // 0 1 // 2 3 4 5 6 7 8 9 0 1 2 3 4 5 @@ -625,7 +696,7 @@ impl MessageType { method + class } - // ReadValue decodes uint16 into MessageType. + /// ReadValue decodes uint16 into MessageType. pub fn read_value(&mut self, value: u16) { // Decoding class. // We are taking first bit from v >> 4 and second from v >> 7. diff --git a/rtc-stun/src/message/message_test.rs b/rtc-stun/src/message/message_test.rs index c119f4e1..000260df 100644 --- a/rtc-stun/src/message/message_test.rs +++ b/rtc-stun/src/message/message_test.rs @@ -623,11 +623,14 @@ fn test_message_full_size() -> Result<()> { ATTR_SOFTWARE, "webrtc-rs/stun".to_owned(), )), - Box::new(MessageIntegrity::new_long_term_integrity( + Box::new(MessageIntegrity::new_long_term_integrity_with_provider( "username".to_owned(), "realm".to_owned(), "password".to_owned(), - )), + crypto::default_provider() + .expect("a built-in provider is enabled for tests") + .crypto(), + )?), Box::new(FINGERPRINT), ])?; let l = m.raw.len(); @@ -652,11 +655,14 @@ fn test_message_clone_to() -> Result<()> { ATTR_SOFTWARE, "webrtc-rs/stun".to_owned(), )), - Box::new(MessageIntegrity::new_long_term_integrity( + Box::new(MessageIntegrity::new_long_term_integrity_with_provider( "username".to_owned(), "realm".to_owned(), "password".to_owned(), - )), + crypto::default_provider() + .expect("a built-in provider is enabled for tests") + .crypto(), + )?), Box::new(FINGERPRINT), ])?; m.encode(); diff --git a/rtc-stun/src/textattrs.rs b/rtc-stun/src/textattrs.rs index 887e325e..3016a2ab 100644 --- a/rtc-stun/src/textattrs.rs +++ b/rtc-stun/src/textattrs.rs @@ -13,30 +13,33 @@ const MAX_REALM_B: usize = 763; const MAX_SOFTWARE_B: usize = 763; const MAX_NONCE_B: usize = 763; -// Username represents USERNAME attribute. -// -// RFC 5389 Section 15.3 +/// USERNAME attribute. +/// +/// RFC 5389 Section 15.3. pub type Username = TextAttribute; -// Realm represents REALM attribute. -// -// RFC 5389 Section 15.7 +/// REALM attribute. +/// +/// RFC 5389 Section 15.7. pub type Realm = TextAttribute; -// Nonce represents NONCE attribute. -// -// RFC 5389 Section 15.8 +/// NONCE attribute. +/// +/// RFC 5389 Section 15.8. pub type Nonce = TextAttribute; -// Software is SOFTWARE attribute. -// -// RFC 5389 Section 15.10 +/// SOFTWARE attribute. +/// +/// RFC 5389 Section 15.10. pub type Software = TextAttribute; // TextAttribute is helper for adding and getting text attributes. #[derive(Clone, Default)] +/// A text-valued attribute such as `USERNAME`, `REALM`, `NONCE` or `SOFTWARE`. pub struct TextAttribute { + /// Which attribute this text belongs to. pub attr: AttrType, + /// The text value. pub text: String, } @@ -74,11 +77,12 @@ impl Getter for TextAttribute { } impl TextAttribute { + /// A text attribute of type `attr` holding `text`. pub fn new(attr: AttrType, text: String) -> Self { TextAttribute { attr, text } } - // get_from_as gets t attribute from m and appends its value to reset v. + /// Get_from_as gets t attribute from m and appends its value to reset v. pub fn get_from_as(m: &Message, attr: AttrType) -> Result { match attr { ATTR_USERNAME => {} diff --git a/rtc-stun/src/uattrs.rs b/rtc-stun/src/uattrs.rs index 4148297a..2fca17e2 100644 --- a/rtc-stun/src/uattrs.rs +++ b/rtc-stun/src/uattrs.rs @@ -7,9 +7,9 @@ use shared::error::*; use std::fmt; -// UnknownAttributes represents UNKNOWN-ATTRIBUTES attribute. -// -// RFC 5389 Section 15.9 +/// UNKNOWN-ATTRIBUTES attribute. +/// +/// RFC 5389 Section 15.9. pub struct UnknownAttributes(pub Vec); impl fmt::Display for UnknownAttributes { diff --git a/rtc-stun/src/uri.rs b/rtc-stun/src/uri.rs index b829c523..e347d05c 100644 --- a/rtc-stun/src/uri.rs +++ b/rtc-stun/src/uri.rs @@ -7,14 +7,20 @@ use std::fmt; // SCHEME definitions from RFC 7064 Section 3.2. +/// The `stun:` URI scheme. pub const SCHEME: &str = "stun"; +/// The `stuns:` URI scheme, for STUN over TLS or DTLS. pub const SCHEME_SECURE: &str = "stuns"; // URI as defined in RFC 7064. #[derive(PartialEq, Eq, Debug)] +/// A parsed `stun:` or `stuns:` URI. pub struct Uri { + /// The scheme, `stun` or `stuns`. pub scheme: String, + /// The server host name or address. pub host: String, + /// The port, if the URI specified one. pub port: Option, } @@ -35,7 +41,7 @@ impl fmt::Display for Uri { } impl Uri { - // parse_uri parses URI from string. + /// Parse_uri parses URI from string. pub fn parse_uri(raw: &str) -> Result { // work around for url crate if raw.contains("//") { diff --git a/rtc-stun/src/xoraddr.rs b/rtc-stun/src/xoraddr.rs index f212a150..b328e0f4 100644 --- a/rtc-stun/src/xoraddr.rs +++ b/rtc-stun/src/xoraddr.rs @@ -67,7 +67,9 @@ pub fn xor_bytes(dst: &mut [u8], a: &[u8], b: &[u8]) -> usize { /// /// RFC 5389 Section 15.2 pub struct XorMappedAddress { + /// The IP address. pub ip: IpAddr, + /// The port. pub port: u16, } diff --git a/rtc-turn/Cargo.toml b/rtc-turn/Cargo.toml index e496ea36..68ffbd65 100644 --- a/rtc-turn/Cargo.toml +++ b/rtc-turn/Cargo.toml @@ -11,10 +11,17 @@ repository.workspace = true keywords.workspace = true categories.workspace = true +[features] +default = ["crypto-ring"] +crypto-ring = ["crypto/crypto-ring", "stun/crypto-ring"] +crypto-aws-lc-rs = ["crypto/crypto-aws-lc-rs", "stun/crypto-aws-lc-rs"] +metrics = [] + [dependencies] shared = { workspace = true, default-features = false, features = [] } stun.workspace = true sansio.workspace = true +crypto.workspace = true bytes.workspace = true log.workspace = true @@ -22,18 +29,12 @@ log.workspace = true [dev-dependencies] env_logger.workspace = true chrono.workspace = true -hex = "0.4.3" +hex.workspace = true clap.workspace = true criterion.workspace = true -crossbeam-channel = "0.5" +crossbeam-channel.workspace = true ctrlc.workspace = true -[features] -default = ["ring"] -metrics = [] -ring = ["stun/ring"] -aws-lc-rs = ["stun/aws-lc-rs"] - [[bench]] name = "bench" harness = false diff --git a/rtc-turn/examples/turn_client_udp.rs b/rtc-turn/examples/turn_client_udp.rs index f5f7cfeb..0d66d01b 100644 --- a/rtc-turn/examples/turn_client_udp.rs +++ b/rtc-turn/examples/turn_client_udp.rs @@ -94,7 +94,9 @@ fn main() -> Result<()> { rto_in_ms: 0, }; - let mut client = Client::new(cfg)?; + // An application selects the crypto provider; library code never resolves a default. + let provider = crypto::default_provider().map_err(|e| Error::Other(e.to_string()))?; + let mut client = Client::new(cfg, provider)?; // Allocate a relay socket on the TURN server. let allocate_tid = client.allocate()?; @@ -176,6 +178,7 @@ fn main() -> Result<()> { client.relay(relay_addr)?.send_to(&data[..], from)?; } } + _ => {} } } diff --git a/rtc-turn/src/client/client_test.rs b/rtc-turn/src/client/client_test.rs index dff3e730..4a03149b 100644 --- a/rtc-turn/src/client/client_test.rs +++ b/rtc-turn/src/client/client_test.rs @@ -3,20 +3,28 @@ use sansio::Protocol; use std::collections::HashSet; use std::net::UdpSocket; +/// Tests may resolve the built-in provider; library code never does. +fn test_crypto_provider() -> std::sync::Arc { + crypto::default_provider().expect("a built-in crypto provider must be enabled for tests") +} + fn create_listening_test_client(rto_in_ms: u64) -> Result<(UdpSocket, Client)> { let udp_socket = UdpSocket::bind("0.0.0.0:0")?; - let client = Client::new(ClientConfig { - stun_serv_addr: String::new(), - turn_serv_addr: String::new(), - local_addr: udp_socket.local_addr()?, - transport_protocol: TransportProtocol::UDP, - username: String::new(), - password: String::new(), - realm: String::new(), - software: "TEST SOFTWARE".to_owned(), - rto_in_ms, - })?; + let client = Client::new( + ClientConfig { + stun_serv_addr: String::new(), + turn_serv_addr: String::new(), + local_addr: udp_socket.local_addr()?, + transport_protocol: TransportProtocol::UDP, + username: String::new(), + password: String::new(), + realm: String::new(), + software: "TEST SOFTWARE".to_owned(), + rto_in_ms, + }, + test_crypto_provider(), + )?; Ok((udp_socket, client)) } @@ -24,17 +32,20 @@ fn create_listening_test_client(rto_in_ms: u64) -> Result<(UdpSocket, Client)> { fn create_listening_test_client_with_stun_serv() -> Result<(UdpSocket, Client)> { let udp_socket = UdpSocket::bind("0.0.0.0:0")?; - let client = Client::new(ClientConfig { - stun_serv_addr: "stun1.l.google.com:19302".to_owned(), - turn_serv_addr: String::new(), - local_addr: udp_socket.local_addr()?, - transport_protocol: TransportProtocol::UDP, - username: String::new(), - password: String::new(), - realm: String::new(), - software: "TEST SOFTWARE".to_owned(), - rto_in_ms: 0, - })?; + let client = Client::new( + ClientConfig { + stun_serv_addr: "stun1.l.google.com:19302".to_owned(), + turn_serv_addr: String::new(), + local_addr: udp_socket.local_addr()?, + transport_protocol: TransportProtocol::UDP, + username: String::new(), + password: String::new(), + realm: String::new(), + software: "TEST SOFTWARE".to_owned(), + rto_in_ms: 0, + }, + test_crypto_provider(), + )?; Ok((udp_socket, client)) } diff --git a/rtc-turn/src/client/mod.rs b/rtc-turn/src/client/mod.rs index 5655f322..dd3fa883 100644 --- a/rtc-turn/src/client/mod.rs +++ b/rtc-turn/src/client/mod.rs @@ -1,17 +1,32 @@ +//! The Sans-I/O TURN client. +//! +//! Using a relay takes three steps: Allocate to obtain a public address, CreatePermission for +//! each peer you intend to exchange data with, and then Send/Data indications (or a bound channel) +//! to move bytes. Each step is a STUN transaction, so every [`Event`](crate::client::Event) carries the +//! transaction id of the request it answers. +//! +//! The three address kinds are easy to confuse: [`RelayedAddr`](crate::client::RelayedAddr) is what peers send to, +//! [`ReflexiveAddr`](crate::client::ReflexiveAddr) is how the server sees this client, and [`PeerAddr`](crate::client::PeerAddr) is the far end. #[cfg(test)] mod client_test; +/// Channel bindings, which replace the 36-byte Data indication header with a 4-byte one. pub mod binding; +/// Per-peer send permissions, which a relay requires before it will forward to an address. pub mod permission; mod proto; +/// A live allocation on the server, and sending or receiving through it. pub mod relay; +/// Outstanding request tracking, with the RFC's retransmission schedule. pub mod transaction; use bytes::BytesMut; +use crypto::RTCCryptoProvider; use log::{debug, trace}; use std::collections::{HashMap, VecDeque}; use std::net::SocketAddr; -use std::time::Instant; +use std::sync::Arc; +use std::time::{Duration, Instant}; use stun::attributes::*; use stun::integrity::*; @@ -41,23 +56,44 @@ const DEFAULT_RTO_IN_MS: u64 = 200; const MAX_DATA_BUFFER_SIZE: usize = u16::MAX as usize; // message size limit for Chromium const MAX_READ_QUEUE_SIZE: usize = 1024; +/// The public address the TURN server allocated on this client's behalf. +/// +/// Peers send here; the server forwards to the client. pub type RelayedAddr = SocketAddr; +/// The client's own address as seen by the server — its server-reflexive address. pub type ReflexiveAddr = SocketAddr; +/// The address of a remote peer the client exchanges data with through the relay. pub type PeerAddr = SocketAddr; #[derive(Debug)] +/// What the client produces in response to inbound datagrams and elapsed time. +/// +/// Every variant carries the [`TransactionId`] of the request it answers, so a caller can +/// match responses to the requests it issued. +#[non_exhaustive] pub enum Event { + /// A request exhausted its retransmissions without a response. TransactionTimeout(TransactionId), + /// A STUN Binding succeeded, reporting this client's server-reflexive address. BindingResponse(TransactionId, ReflexiveAddr), + /// A STUN Binding failed. BindingError(TransactionId, Error), + /// An Allocate succeeded; the relayed address is now usable. AllocateResponse(TransactionId, RelayedAddr), + /// An Allocate failed — commonly authentication, or the server being out of ports. AllocateError(TransactionId, Error), + /// A CreatePermission succeeded; the relay will now forward to and from this peer. CreatePermissionResponse(TransactionId, PeerAddr), + /// A CreatePermission failed. CreatePermissionError(TransactionId, Error), + /// Data arrived from a peer through the relay. + /// + /// The channel number is `Some` when it came as ChannelData and `None` when it came as a + /// Data indication. DataIndicationOrChannelData(Option, PeerAddr, BytesMut), } @@ -78,19 +114,29 @@ enum AllocateState { /// ClientConfig is a bag of config parameters for Client. pub struct ClientConfig { + /// The STUN server to use for Binding requests, as `host:port`. May be empty. pub stun_serv_addr: String, // STUN server address (e.g. "stun.abc.com:3478") + /// The TURN server to allocate from, as `host:port`. pub turn_serv_addr: String, // TURN server address (e.g. "turn.abc.com:3478") + /// The local address the client sends from. pub local_addr: SocketAddr, + /// Whether to reach the server over UDP or TCP. pub transport_protocol: TransportProtocol, + /// The long-term credential username for the TURN server. pub username: String, + /// The long-term credential password. pub password: String, + /// The authentication realm, used in the `MESSAGE-INTEGRITY` computation. pub realm: String, + /// An optional `SOFTWARE` attribute value, sent for diagnostics. pub software: String, + /// The initial retransmission timeout in milliseconds; each retry doubles it. pub rto_in_ms: u64, } /// Client is a STUN client pub struct Client { + crypto_provider: Arc, stun_serv_addr: Option, turn_serv_addr: Option, local_addr: SocketAddr, @@ -98,7 +144,6 @@ pub struct Client { username: Username, password: String, realm: Realm, - integrity: MessageIntegrity, software: Software, tr_map: TransactionMap, binding_mgr: BindingManager, @@ -110,8 +155,10 @@ pub struct Client { } impl Client { - /// new returns a new Client instance. listeningAddress is the address and port to listen on, default "0.0.0.0:0" - pub fn new(config: ClientConfig) -> Result { + /// Returns a new TURN client. + /// + /// The crypto provider is supplied by the caller; this crate never resolves a default. + pub fn new(config: ClientConfig, crypto_provider: Arc) -> Result { let stun_serv_addr = if config.stun_serv_addr.is_empty() { None } else { @@ -131,6 +178,7 @@ impl Client { }; Ok(Client { + crypto_provider: crypto_provider.clone(), stun_serv_addr, turn_serv_addr, local_addr: config.local_addr, @@ -146,8 +194,6 @@ impl Client { } else { DEFAULT_RTO_IN_MS }, - integrity: MessageIntegrity::new_short_term_integrity(String::new()), - relays: HashMap::new(), transmits: VecDeque::new(), events: VecDeque::new(), @@ -333,6 +379,12 @@ impl Client { Ok(()) } + /// Borrows the allocation for `relayed_addr` so data can be sent or permissions created. + /// + /// # Errors + /// + /// Fails if this client has no allocation for that address — it was never allocated, or has + /// already been closed. pub fn relay(&mut self, relayed_addr: SocketAddr) -> Result> { if !self.relays.contains_key(&relayed_addr) { Err(Error::ErrStreamNotExisted) @@ -348,7 +400,7 @@ impl Client { /// return key to find out corresponding Event either BindingResponse or BindingRequestTimeout pub fn send_binding_request_to(&mut self, to: SocketAddr) -> Result { let msg = { - let attrs: Vec> = if !self.software.text.is_empty() { + let attrs: Vec> = if !self.software.text.is_empty() { vec![ Box::new(TransactionId::new()), Box::new(BINDING_REQUEST), @@ -429,6 +481,62 @@ impl Client { | XOR-MAPPED-ADDRESS=192.0.2.1:7000 | | | MESSAGE-INTEGRITY-SHA256=... | | | */ + /// Replaces the long-term credential used to sign subsequent requests, **keeping any + /// existing allocation**. + /// + /// A TURN allocation is a property of the 5-tuple, not of the credential that created + /// it: [RFC 5766 §6.2] identifies an allocation by 5-tuple, and a server's + /// `Refresh` handling looks it up the same way. So when credentials are rotated on the + /// same server there is no need to give up the allocation and re-`Allocate` — which + /// would in fact be rejected with **437 (Allocation Mismatch)**, since the server still + /// holds the previous allocation for that 5-tuple. Re-signing the existing allocation is + /// both correct and seamless: permissions and channel bindings survive. + /// + /// The realm is *not* re-negotiated. It was learned from the server's 401 during the + /// first `Allocate`, and a credential rotation keeps the same server, so it still + /// applies. Follow this with [`Self::refresh_allocations`] so the server sees the new + /// credential before the allocation would otherwise expire. + /// + /// [RFC 5766 §6.2]: https://datatracker.ietf.org/doc/html/rfc5766#section-6.2 + pub fn update_credentials(&mut self, username: String, password: String) -> Result<()> { + let username = Username::new(ATTR_USERNAME, username); + let long_term_integrity_key = MessageIntegrity::long_term_integrity_key( + username.text.clone(), + self.realm.text.clone(), + password.clone(), + self.crypto_provider.crypto(), + )?; + self.username = username; + self.password = password; + + // Each allocation carries the integrity it will sign its own Refresh / + // CreatePermission / ChannelBind with, so they have to be re-signed too — otherwise + // the next refresh would still present the retired credential. + for relay in self.relays.values_mut() { + relay.long_term_integrity_key = long_term_integrity_key.clone(); + } + + Ok(()) + } + + /// Refreshes every live allocation, re-signing each with the current credential. + /// + /// Each allocation is refreshed with its own current lifetime, so this extends rather + /// than changes it. Intended to follow [`update_credentials`](Self::update_credentials). + pub fn refresh_allocations(&mut self) -> Result<()> { + let relays: Vec<(RelayedAddr, Duration)> = self + .relays + .iter() + .map(|(addr, relay)| (*addr, relay.lifetime)) + .collect(); + + for (relayed_addr, lifetime) in relays { + self.relay(relayed_addr)?.refresh_allocation(lifetime)?; + } + + Ok(()) + } + /// Allocate sends a TURN allocation request to the given transport address pub fn allocate(&mut self) -> Result { let mut msg = Message::new(); @@ -480,11 +588,12 @@ impl Client { } }; - self.integrity = MessageIntegrity::new_long_term_integrity( + let integrity = MessageIntegrity::new_long_term_integrity_with_provider( self.username.text.clone(), self.realm.text.clone(), self.password.clone(), - ); + self.crypto_provider.crypto(), + )?; let mut msg = Message::new(); @@ -507,7 +616,7 @@ impl Client { Box::new(self.username.clone()), Box::new(self.realm.clone()), Box::new(nonce.clone()), - Box::new(self.integrity.clone()), + Box::new(integrity), Box::new(FINGERPRINT), ])?; @@ -542,7 +651,17 @@ impl Client { self.relays.insert( relayed_addr, - RelayState::new(relayed_addr, self.integrity.clone(), nonce, lifetime.0), + RelayState::new( + relayed_addr, + MessageIntegrity::long_term_integrity_key( + self.username.text.clone(), + self.realm.text.clone(), + self.password.clone(), + self.crypto_provider.crypto(), + )?, + nonce, + lifetime.0, + ), ); self.events.push_back(Event::AllocateResponse( response.transaction_id, diff --git a/rtc-turn/src/client/relay.rs b/rtc-turn/src/client/relay.rs index f9ef8174..22064fa9 100644 --- a/rtc-turn/src/client/relay.rs +++ b/rtc-turn/src/client/relay.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use std::net::SocketAddr; use std::ops::Add; use std::time::{Duration, Instant}; - use stun::attributes::*; use stun::error_code::*; use stun::fingerprint::*; @@ -28,7 +27,7 @@ const MAX_RETRY_ATTEMPTS: u16 = 3; // RelayState is a set of params use by Relay pub(crate) struct RelayState { pub(crate) relayed_addr: RelayedAddr, - pub(crate) integrity: MessageIntegrity, + pub(crate) long_term_integrity_key: Vec, pub(crate) nonce: Nonce, pub(crate) lifetime: Duration, perm_map: HashMap, @@ -39,7 +38,7 @@ pub(crate) struct RelayState { impl RelayState { pub(super) fn new( relayed_addr: RelayedAddr, - integrity: MessageIntegrity, + long_term_integrity_key: Vec, nonce: Nonce, lifetime: Duration, ) -> Self { @@ -47,7 +46,7 @@ impl RelayState { Self { relayed_addr, - integrity, + long_term_integrity_key, nonce, lifetime, perm_map: HashMap::new(), @@ -68,13 +67,25 @@ impl RelayState { } } -// Relay is the implementation of the Conn interfaces for UDP Relayed network connections. +/// A borrowed handle to one live allocation on the TURN server. +/// +/// Obtained from [`Client::relay`](crate::client::Client::relay). Sending to a peer requires +/// a permission for it first — see [`Self::create_permission`]. pub struct Relay<'a> { pub(crate) relayed_addr: RelayedAddr, pub(crate) client: &'a mut Client, } impl Relay<'_> { + /// Asks the server to permit traffic to and from `peer_addr`. + /// + /// The relay silently drops data for peers with no permission, and permissions expire after + /// five minutes unless refreshed. Returns the transaction id to match against the resulting + /// [`Event`], or `None` if a permission is already in place. + /// + /// # Errors + /// + /// Fails if the allocation no longer exists or the request cannot be encoded. pub fn create_permission(&mut self, peer_addr: SocketAddr) -> Result> { if let Some(relay) = self.client.relays.get_mut(&self.relayed_addr) { relay @@ -137,6 +148,13 @@ impl Relay<'_> { } } + /// Sends `p` to `peer_addr` through the relay. + /// + /// Uses ChannelData framing if a channel is bound for that peer, otherwise a Data indication. + /// + /// # Errors + /// + /// Fails if the allocation is gone, or if no permission exists for `peer_addr`. pub fn send_to(&mut self, p: &[u8], peer_addr: SocketAddr) -> Result<()> { // check if we have a permission for the destination IP addr let result = if let Some(relay) = self.client.relays.get_mut(&self.relayed_addr) { @@ -144,7 +162,7 @@ impl Relay<'_> { if perm.state() != PermState::Permitted { Err(Error::ErrNoPermission) } else { - Ok((relay.integrity.clone(), relay.nonce.clone())) + Ok((relay.long_term_integrity_key.clone(), relay.nonce.clone())) } } else { Err(Error::ErrNoPermission) @@ -153,16 +171,16 @@ impl Relay<'_> { Err(Error::ErrConnClosed) }; - let (integrity, nonce) = result?; + let (long_term_integrity_key, nonce) = result?; - self.send(p, peer_addr, integrity, nonce) + self.send(p, peer_addr, long_term_integrity_key, nonce) } fn send( &mut self, p: &[u8], peer_addr: SocketAddr, - integrity: MessageIntegrity, + long_term_integrity_key: Vec, nonce: Nonce, ) -> Result<()> { let channel_number = { @@ -189,7 +207,13 @@ impl Relay<'_> { if let Some(b) = self.client.binding_mgr.get_by_addr(&bind_addr) { b.set_state(BindingState::Request); } - self.channel_bind(self.relayed_addr, bind_addr, bind_number, nonce, integrity)?; + self.channel_bind( + self.relayed_addr, + bind_addr, + bind_number, + nonce, + long_term_integrity_key, + )?; } // send data using SendIndication @@ -222,7 +246,13 @@ impl Relay<'_> { if let Some(b) = self.client.binding_mgr.get_by_addr(&bind_addr) { b.set_state(BindingState::Refresh); } - self.channel_bind(self.relayed_addr, bind_addr, bind_number, nonce, integrity)?; + self.channel_bind( + self.relayed_addr, + bind_addr, + bind_number, + nonce, + long_term_integrity_key, + )?; } bind_number @@ -234,6 +264,11 @@ impl Relay<'_> { // Close closes the connection. // Any blocked ReadFrom or write_to operations will be unblocked and return errors. + /// Releases the allocation by refreshing it with a zero lifetime. + /// + /// # Errors + /// + /// Fails if the refresh request cannot be sent. pub fn close(&mut self) -> Result<()> { self.refresh_allocation(Duration::from_secs(0)) } @@ -246,7 +281,7 @@ impl Relay<'_> { let (username, realm) = (self.client.username(), self.client.realm()); if let Some(relay) = self.client.relays.get_mut(&self.relayed_addr) { let msg = { - let mut setters: Vec> = vec![ + let mut setters: Vec> = vec![ Box::new(TransactionId::new()), Box::new(MessageType::new(METHOD_CREATE_PERMISSION, CLASS_REQUEST)), ]; @@ -261,7 +296,10 @@ impl Relay<'_> { setters.push(Box::new(username)); setters.push(Box::new(realm)); setters.push(Box::new(relay.nonce.clone())); - setters.push(Box::new(relay.integrity.clone())); + setters.push(Box::new(MessageIntegrity::new_raw_integrity_with_provider( + relay.long_term_integrity_key.clone(), + self.client.crypto_provider.crypto(), + ))); setters.push(Box::new(FINGERPRINT)); let mut msg = Message::new(); @@ -320,7 +358,7 @@ impl Relay<'_> { } } - fn refresh_allocation(&mut self, lifetime: Duration) -> Result<()> { + pub(super) fn refresh_allocation(&mut self, lifetime: Duration) -> Result<()> { let (username, realm) = (self.client.username(), self.client.realm()); if let Some(relay) = self.client.relays.get_mut(&self.relayed_addr) { let mut msg = Message::new(); @@ -331,7 +369,10 @@ impl Relay<'_> { Box::new(username), Box::new(realm), Box::new(relay.nonce.clone()), - Box::new(relay.integrity.clone()), + Box::new(MessageIntegrity::new_raw_integrity_with_provider( + relay.long_term_integrity_key.clone(), + self.client.crypto_provider.crypto(), + )), Box::new(FINGERPRINT), ])?; @@ -376,7 +417,7 @@ impl Relay<'_> { } } - fn refresh_permissions(&mut self) -> Result<()> { + pub(super) fn refresh_permissions(&mut self) -> Result<()> { if let Some(relay) = self.client.relays.get_mut(&self.relayed_addr) { #[allow(clippy::map_clone)] let addrs: Vec = relay.perm_map.keys().map(|addr| *addr).collect(); @@ -397,10 +438,10 @@ impl Relay<'_> { bind_addr: SocketAddr, bind_number: u16, nonce: Nonce, - integrity: MessageIntegrity, + long_term_integrity_key: Vec, ) -> Result<()> { let (msg, turn_server_addr) = { - let setters: Vec> = vec![ + let setters: Vec> = vec![ Box::new(TransactionId::new()), Box::new(MessageType::new(METHOD_CHANNEL_BIND, CLASS_REQUEST)), Box::new(proto::peeraddr::PeerAddress { @@ -411,7 +452,12 @@ impl Relay<'_> { Box::new(self.client.username()), Box::new(self.client.realm()), Box::new(nonce), - Box::new(integrity), + // Built here rather than by the caller: it borrows `crypto_provider`, and that + // borrow must end with `setters` so the `&mut self` transaction below is free. + Box::new(MessageIntegrity::new_raw_integrity_with_provider( + long_term_integrity_key, + self.client.crypto_provider.crypto(), + )), Box::new(FINGERPRINT), ]; diff --git a/rtc-turn/src/lib.rs b/rtc-turn/src/lib.rs index f8e44019..fb87d7cc 100644 --- a/rtc-turn/src/lib.rs +++ b/rtc-turn/src/lib.rs @@ -1,5 +1,61 @@ #![warn(rust_2018_idioms)] +#![warn(missing_docs)] #![allow(dead_code)] +//! TURN for the Sans-I/O WebRTC stack. +//! +//! Traversal Using Relays around NAT ([RFC 5766]) with IPv6 support ([RFC 6156]). TURN is +//! ICE's fallback: when no direct path between two peers can be found, each relays its +//! media through a server, which allocates a public address on their behalf. +//! +//! # Structure +//! +//! * [`client`] — the Sans-I/O client: allocate a relayed address, create permissions, +//! bind channels, and send or receive through the allocation. It owns no sockets. +//! * [`proto`] — the TURN-specific STUN attributes and methods (`ALLOCATE`, +//! `CREATE-PERMISSION`, `CHANNEL-BIND`, `XOR-RELAYED-ADDRESS`, ChannelData framing), +//! built on [`rtc-stun`]. +//! +//! # Example +//! +//! A client is configured with the server to allocate from and the long-term credentials to +//! authenticate with; driving it is then a matter of feeding it datagrams and polling for +//! [`Event`](client::Event)s: +//! +//! ``` +//! use rtc_turn::client::ClientConfig; +//! use shared::TransportProtocol; +//! +//! let config = ClientConfig { +//! turn_serv_addr: "turn.example.com:3478".to_owned(), +//! local_addr: "0.0.0.0:0".parse().unwrap(), +//! transport_protocol: TransportProtocol::UDP, +//! username: "user".to_owned(), +//! password: "pass".to_owned(), +//! realm: "example.com".to_owned(), +//! stun_serv_addr: String::new(), // optional: only for Binding requests +//! software: String::new(), +//! rto_in_ms: 0, // 0 selects the default retransmission timeout +//! }; +//! assert_eq!(config.turn_serv_addr, "turn.example.com:3478"); +//! ``` +//! +//! Most applications do not depend on this crate directly — [`rtc-ice`] gathers relay +//! candidates through it, and the [`rtc`](https://docs.rs/rtc) crate drives that. +//! +//! [RFC 5766]: https://datatracker.ietf.org/doc/html/rfc5766 +//! [RFC 6156]: https://datatracker.ietf.org/doc/html/rfc6156 +//! [`rtc-stun`]: https://docs.rs/rtc-stun +//! [`rtc-ice`]: https://docs.rs/rtc-ice + +/// The Sans-I/O TURN client: allocate a relayed address and send through it. +/// The crypto provider API. +/// +/// Re-exported because this crate's public constructors take an +/// [`Arc`](crypto::RTCCryptoProvider), which a caller must be able to name +/// without adding — and version-matching — a direct `rtc-crypto` dependency. +pub use crypto; + pub mod client; +/// The TURN-specific STUN attributes, methods and ChannelData framing. pub mod proto; diff --git a/rtc-turn/src/proto/addr.rs b/rtc-turn/src/proto/addr.rs index 70cc4f45..7fdb8fcf 100644 --- a/rtc-turn/src/proto/addr.rs +++ b/rtc-turn/src/proto/addr.rs @@ -49,9 +49,16 @@ impl Addr { // FiveTuple represents 5-TUPLE value. #[derive(PartialEq, Eq, Default)] +/// The five-tuple that uniquely identifies a TURN allocation. +/// +/// Client address, server address and transport together — the server keys allocations by +/// this, so the same client may hold several over different transports. pub struct FiveTuple { + /// The client's transport address. pub client: Addr, + /// The server's transport address. pub server: Addr, + /// The transport carrying the allocation. pub proto: Protocol, } diff --git a/rtc-turn/src/proto/chandata.rs b/rtc-turn/src/proto/chandata.rs index cb24bbf9..cf5f1fad 100644 --- a/rtc-turn/src/proto/chandata.rs +++ b/rtc-turn/src/proto/chandata.rs @@ -22,8 +22,11 @@ const CHANNEL_DATA_HEADER_SIZE: usize = CHANNEL_DATA_LENGTH_SIZE + CHANNEL_DATA_ /// [RFC 5766 Section 11.4](https://www.rfc-editor.org/rfc/rfc5766#section-11.4). #[derive(Default, Debug)] pub struct ChannelData { + /// The relayed payload. May be a subslice of [`Self::raw`]. pub data: Vec, // can be subslice of Raw + /// The channel this data belongs to, which identifies the peer. pub number: ChannelNumber, + /// The full encoded message, header included. pub raw: Vec, } diff --git a/rtc-turn/src/proto/channum.rs b/rtc-turn/src/proto/channum.rs index 0b9f6f0f..ab753f90 100644 --- a/rtc-turn/src/proto/channum.rs +++ b/rtc-turn/src/proto/channum.rs @@ -16,7 +16,11 @@ const CHANNEL_NUMBER_SIZE: usize = 4; // // 0x4000 through 0x7FFF: These values are the allowed channel // numbers (16,383 possible values). +/// The lowest channel number a client may bind. +/// +/// The range is chosen so ChannelData can be told apart from STUN messages on the same port. pub const MIN_CHANNEL_NUMBER: u16 = 0x4000; +/// The highest channel number a client may bind. pub const MAX_CHANNEL_NUMBER: u16 = 0x7FFF; /// `ChannelNumber` represents `CHANNEL-NUMBER` attribute. Encoded as `u16`. diff --git a/rtc-turn/src/proto/mod.rs b/rtc-turn/src/proto/mod.rs index ada03ffd..74ada82e 100644 --- a/rtc-turn/src/proto/mod.rs +++ b/rtc-turn/src/proto/mod.rs @@ -1,17 +1,39 @@ +//! TURN's STUN attributes and ChannelData framing. +//! +//! TURN is defined as a set of STUN methods and attributes, so these build on +//! [`rtc-stun`](https://docs.rs/rtc-stun). The attributes name the relay's parts: +//! [`relayaddr`](crate::proto::relayaddr) the allocated public address, [`peeraddr`](crate::proto::peeraddr) the far end, [`lifetime`](crate::proto::lifetime) the +//! allocation's expiry, [`data`](crate::proto::data) the relayed payload. +//! +//! [`chandata`](crate::proto::chandata) is the exception — a ChannelData message is not STUN at all, but a compact +//! four-byte framing that replaces the 36-byte Send/Data indication header once a channel is +//! bound. Its [`channum`](crate::proto::channum) range is chosen so the two can be told apart on a shared port. #[cfg(test)] mod proto_test; +/// Address helpers and the five-tuple that identifies an allocation. pub mod addr; +/// ChannelData messages — the compact 4-byte framing for relayed data. pub mod chandata; +/// The `CHANNEL-NUMBER` attribute. pub mod channum; +/// The `DATA` attribute, which carries relayed payloads in Send/Data indications. pub mod data; +/// The `DONT-FRAGMENT` attribute, asking the server to set DF on relayed packets. pub mod dontfrag; +/// The `EVEN-PORT` attribute, requesting an even relayed port (for RTP/RTCP pairs). pub mod evenport; +/// The `LIFETIME` attribute, which sets and reports allocation expiry. pub mod lifetime; +/// The `XOR-PEER-ADDRESS` attribute, naming the peer in permission and data messages. pub mod peeraddr; +/// The `XOR-RELAYED-ADDRESS` attribute, which reports the allocated public address. pub mod relayaddr; +/// The `REQUESTED-ADDRESS-FAMILY` attribute, for asking for an IPv4 or IPv6 allocation. pub mod reqfamily; +/// The `REQUESTED-TRANSPORT` attribute, which selects the relay's transport to peers. pub mod reqtrans; +/// The `RESERVATION-TOKEN` attribute, used to claim a previously reserved port. pub mod rsrvtoken; use std::fmt; diff --git a/rtc-turn/src/proto/peeraddr.rs b/rtc-turn/src/proto/peeraddr.rs index f56c738a..7cfcc95d 100644 --- a/rtc-turn/src/proto/peeraddr.rs +++ b/rtc-turn/src/proto/peeraddr.rs @@ -18,7 +18,9 @@ use stun::xoraddr::*; /// [RFC 5766 Section 14.3](https://www.rfc-editor.org/rfc/rfc5766#section-14.3). #[derive(PartialEq, Eq, Debug)] pub struct PeerAddress { + /// The peer IP address. pub ip: IpAddr, + /// The peer port. pub port: u16, } diff --git a/rtc-turn/src/proto/relayaddr.rs b/rtc-turn/src/proto/relayaddr.rs index 36a50dbf..dbc23f42 100644 --- a/rtc-turn/src/proto/relayaddr.rs +++ b/rtc-turn/src/proto/relayaddr.rs @@ -17,7 +17,9 @@ use stun::xoraddr::*; /// [RFC 5766 Section 14.5](https://www.rfc-editor.org/rfc/rfc5766#section-14.5). #[derive(PartialEq, Eq, Debug)] pub struct RelayedAddress { + /// The relayed IP address. pub ip: IpAddr, + /// The relayed port. pub port: u16, } diff --git a/rtc-turn/src/proto/reqfamily.rs b/rtc-turn/src/proto/reqfamily.rs index ea007286..40b99d2a 100644 --- a/rtc-turn/src/proto/reqfamily.rs +++ b/rtc-turn/src/proto/reqfamily.rs @@ -10,7 +10,9 @@ use stun::message::*; use shared::error::{Error, Result}; // Values for RequestedAddressFamily as defined in RFC 6156 Section 4.1.1. +/// Requests an IPv4 relayed address. pub const REQUESTED_FAMILY_IPV4: RequestedAddressFamily = RequestedAddressFamily(0x01); +/// Requests an IPv6 relayed address. pub const REQUESTED_FAMILY_IPV6: RequestedAddressFamily = RequestedAddressFamily(0x02); /// `RequestedAddressFamily` represents the `REQUESTED-ADDRESS-FAMILY` Attribute as diff --git a/rtc-turn/src/proto/reqtrans.rs b/rtc-turn/src/proto/reqtrans.rs index 94c80be9..5aa7ecaf 100644 --- a/rtc-turn/src/proto/reqtrans.rs +++ b/rtc-turn/src/proto/reqtrans.rs @@ -19,6 +19,7 @@ use shared::error::Result; /// [RFC 5766 Section 14.7](https://www.rfc-editor.org/rfc/rfc5766#section-14.7). #[derive(Default, Debug, PartialEq, Eq)] pub struct RequestedTransport { + /// The transport the relay should use toward peers. WebRTC always requests UDP. pub protocol: Protocol, } diff --git a/scripts/check-crypto-boundary.py b/scripts/check-crypto-boundary.py new file mode 100755 index 00000000..0991fdd9 --- /dev/null +++ b/scripts/check-crypto-boundary.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Enforce the G3 crypto boundary: `rtc-crypto` is the only crate that names a crypto +implementation. + +Checks each crate's `[dependencies]` / `[dev-dependencies]` / `[build-dependencies]` and +`[target.*.dependencies]` sections. Deliberately ignored: + +* `[features]` entries such as `ring = ["crypto/ring"]`, which are provider-feature forwarding + and are how a standalone crate exposes backend selection. +* `[workspace.dependencies]` in the root manifest, which declares versions on behalf of + `rtc-crypto`. +* `rand`. It is an entropy source, not a crypto implementation, appears in no public signature, + and is a documented exception (see `docs/crypto-provider-decisions.md`). + +Run from the workspace root. Exits non-zero and prints offenders on failure. +""" + +from __future__ import annotations + +import glob +import re +import sys +from pathlib import Path + +CRYPTO_IMPLEMENTATIONS = { + "ring", + "aws-lc-rs", + "aes", + "aes-gcm", + "sha1", + "sha2", + "hmac", + "hkdf", + "p256", + "p384", + "ctr", + "cbc", + "ccm", + "md-5", + "subtle", + "x25519-dalek", + "chacha20poly1305", + "sec1", + "ed25519-dalek", + "rsa", + "openssl", +} + +# Certificate format and trust policy are deliberately outside the provider (design section 5.2), +# so these stay in the crates that own X.509 handling. +CERTIFICATE_FORMAT_ALLOWED = {"rcgen", "rustls", "x509-parser", "der-parser", "pem"} + +DEPENDENCY_SECTION = re.compile( + r"^\[(?:target\.[^\]]+\.)?(?:dev-|build-)?dependencies\]$" +) +SECTION = re.compile(r"^\[.*\]$") +DEPENDENCY_NAME = re.compile(r"^([A-Za-z0-9_-]+)\s*[.=]") + + +def dependencies_of(manifest: Path) -> set[str]: + names: set[str] = set() + in_dependencies = False + for line in manifest.read_text().splitlines(): + stripped = line.strip() + if SECTION.match(stripped): + in_dependencies = bool(DEPENDENCY_SECTION.match(stripped)) + continue + if not in_dependencies: + continue + match = DEPENDENCY_NAME.match(stripped) + if match: + names.add(match.group(1)) + return names + + +def main() -> int: + offenders: list[str] = [] + for manifest_path in sorted(glob.glob("rtc-*/Cargo.toml")) + ["Cargo.toml"]: + manifest = Path(manifest_path) + crate = manifest.parent.name if manifest.parent.name else "rtc" + if crate == "rtc-crypto": + continue + found = dependencies_of(manifest) & CRYPTO_IMPLEMENTATIONS + for dependency in sorted(found): + offenders.append(f"{manifest_path}: {dependency}") + + if offenders: + print("Crypto implementation dependencies found outside rtc-crypto:") + for offender in offenders: + print(f" {offender}") + print() + print("Route the operation through rtc-crypto's provider traits instead.") + print(f"Certificate-format crates remain allowed: {sorted(CERTIFICATE_FORMAT_ALLOWED)}") + return 1 + + print("Crypto boundary holds: no crypto implementation outside rtc-crypto.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/data_channel/init.rs b/src/data_channel/init.rs index 295ada38..f0425dbd 100644 --- a/src/data_channel/init.rs +++ b/src/data_channel/init.rs @@ -21,7 +21,7 @@ /// ..Default::default() /// }; /// ``` -#[derive(Default, Clone)] +#[derive(Clone)] pub struct RTCDataChannelInit { /// If set to `false`, data is allowed to be delivered out of order. /// @@ -77,3 +77,25 @@ pub struct RTCDataChannelInit { /// [W3C specification]: https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit-negotiated pub negotiated: Option, } + +impl Default for RTCDataChannelInit { + /// The defaults defined by [W3C `RTCDataChannelInit`]. + /// + /// Note `ordered`: it defaults to `true`, so `..Default::default()` yields an ordered, + /// reliable channel — what an application asking for "a plain data channel" expects. + /// A derived `Default` would make it `false`, and an unordered channel does more than + /// reorder application messages: unordered chunks bypass SCTP's ordered-delivery queue, + /// so a first message can overtake the `DATA_CHANNEL_OPEN` sent on the same stream and + /// be dropped by the peer as data on a stream it has not accepted yet. + /// + /// [W3C `RTCDataChannelInit`]: https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit + fn default() -> Self { + Self { + ordered: true, + max_packet_life_time: None, + max_retransmits: None, + protocol: String::new(), + negotiated: None, + } + } +} diff --git a/src/data_channel/mod.rs b/src/data_channel/mod.rs index f59b06f5..79d7e301 100644 --- a/src/data_channel/mod.rs +++ b/src/data_channel/mod.rs @@ -241,11 +241,37 @@ where } } + /// Rejects a send the write path could not carry out. + /// + /// The condition mirrors what `DataChannelHandler::handle_write` requires: the channel + /// must be registered *and* its SCTP stream established. Checking it here, synchronously, + /// is what makes the failure visible — the handler runs later, on the pipeline's write + /// pass, where an `Err` is only logged and cannot reach the caller. + fn ensure_sendable(&self) -> Result<()> { + let dc = self + .peer_connection + .data_channels + .get(&self.id) + .ok_or(Error::ErrDataChannelClosed)?; + + if dc.data_channel.is_none() { + // No stream yet: either it is still being negotiated, or it is already gone. + return Err(if dc.ready_state == RTCDataChannelState::Connecting { + Error::ErrDataChannelNotOpen + } else { + Error::ErrDataChannelClosed + }); + } + + Ok(()) + } + /// send sends the binary message to the DataChannel peer + /// + /// Returns [`Error::ErrDataChannelNotOpen`] if the channel's SCTP stream has not been + /// established yet, and [`Error::ErrDataChannelClosed`] once it is gone. pub fn send(&mut self, data: BytesMut) -> Result<()> { - if !self.peer_connection.data_channels.contains_key(&self.id) { - return Err(Error::ErrDataChannelClosed); - } + self.ensure_sendable()?; let data_len = data.len(); self.peer_connection .handle_write(RTCMessage::DataChannelMessage( @@ -265,10 +291,10 @@ where } /// send_text sends the text message to the DataChannel peer + /// + /// Error contract matches [`send`](Self::send). pub fn send_text(&mut self, s: impl Into) -> Result<()> { - if !self.peer_connection.data_channels.contains_key(&self.id) { - return Err(Error::ErrDataChannelClosed); - } + self.ensure_sendable()?; let data = BytesMut::from(s.into().as_str()); let data_len = data.len(); self.peer_connection diff --git a/src/data_channel/state.rs b/src/data_channel/state.rs index 45ef598e..e48c46c1 100644 --- a/src/data_channel/state.rs +++ b/src/data_channel/state.rs @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize}; /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState /// [W3C]: https://w3c.github.io/webrtc-pc/#dom-rtcdatachannelstate #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCDataChannelState { /// The state is unspecified. #[serde(rename = "unspecified")] diff --git a/src/lib.rs b/src/lib.rs index cebcb4c0..11c7185f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -169,6 +169,8 @@ //! println!("Received data channel message on channel {:?}", channel_id); //! // Process data channel message //! } +//! // RTCMessage is #[non_exhaustive]: a wildcard arm is required. +//! _ => {} //! } //! } //! @@ -456,6 +458,49 @@ //! # } //! ``` //! +//! #### Type-erasing the interceptor chain +//! +//! [`RTCPeerConnection`](peer_connection::RTCPeerConnection) is generic over its +//! interceptor chain, and a chain's type spells out its entire composition +//! (`TwccReceiverInterceptor>`). That type propagates into +//! everything that *holds* a peer connection, which is a problem when the chain is chosen +//! at runtime or when connections have to live in your own structs and collections. +//! +//! [`Registry::boxed`](interceptor::Registry::boxed) erases the chain to +//! [`BoxedInterceptor`](interceptor::BoxedInterceptor), giving every connection the same +//! concrete type no matter how it was built: +//! +//! ```no_run +//! use rtc::interceptor::{BoxedInterceptor, Registry}; +//! use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors; +//! use rtc::peer_connection::configuration::media_engine::MediaEngine; +//! use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; +//! +//! // No type parameter: this struct does not have to know the chain. +//! struct Session { +//! peer_connection: RTCPeerConnection, +//! } +//! +//! # fn example(with_nack: bool) -> Result<(), Box> { +//! let mut media_engine = MediaEngine::default(); +//! let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; +//! +//! let mut sessions: Vec = Vec::new(); +//! sessions.push(Session { +//! peer_connection: RTCPeerConnectionBuilder::new() +//! .with_media_engine(media_engine) +//! .with_interceptor_registry(registry.boxed()) +//! .build()?, +//! }); +//! # Ok(()) +//! # } +//! ``` +//! +//! The cost is one virtual call per chain entry point (`handle_read`, `poll_write`, +//! `handle_timeout`, …); the chain's interior remains statically dispatched and inlined. +//! Static dispatch is still the default — keep the generic form when the chain is fixed at +//! compile time. +//! //! ### Creating and Using Data Channels //! //! ```no_run @@ -655,8 +700,8 @@ #![allow(dead_code)] pub use { - datachannel, dtls, ice, interceptor, mdns, media, rtcp, rtp, sansio, sctp, sdp, shared, srtp, - stun, turn, + crypto, datachannel, dtls, ice, interceptor, interceptor_derive, mdns, media, rtcp, rtp, + sansio, sctp, sdp, shared, srtp, stun, turn, }; pub mod data_channel; @@ -664,10 +709,3 @@ pub mod media_stream; pub mod peer_connection; pub mod rtp_transceiver; pub mod statistics; - -#[cfg(all(feature = "aws-lc-rs", feature = "ring"))] -compile_error!("At most one of the features \"aws-lc-rs\" and \"ring\" can be enabled."); -#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))] -compile_error!("At least one of the features \"aws-lc-rs\" and \"ring\" must be enabled."); -#[cfg(feature = "aws-lc-rs")] -extern crate aws_lc_rs as ring; diff --git a/src/media_stream/track.rs b/src/media_stream/track.rs index 17b79a24..76c64bc3 100644 --- a/src/media_stream/track.rs +++ b/src/media_stream/track.rs @@ -1,71 +1,8 @@ -//! MediaStreamTrack API +//! `MediaStreamTrack` implementation. //! -//! This module implements the `MediaStreamTrack` interface as defined in the -//! [W3C Media Capture and Streams specification](https://www.w3.org/TR/mediacapture-streams/#mediastreamtrack). -//! -//! A [`MediaStreamTrack`] represents a single media track within a media stream, such as -//! an audio track from a microphone or a video track from a camera. Each track has properties -//! like enabled/disabled state, mute status, and lifecycle state (live or ended). -//! -//! # Examples -//! -//! ## Creating a video track -//! -//! ``` -//! use rtc::media_stream::MediaStreamTrack; -//! use rtc::rtp_transceiver::rtp_sender::{RTCRtpCodec, RtpCodecKind}; -//! use rtc::rtp_transceiver::rtp_sender::{RTCRtpEncodingParameters, RTCRtpCodingParameters}; -//! use rtc::peer_connection::configuration::media_engine::MIME_TYPE_VP8; -//! -//! # fn example() -> Result<(), Box> { -//! let codec = RTCRtpCodec { -//! mime_type: MIME_TYPE_VP8.to_string(), -//! clock_rate: 90000, -//! channels: 0, -//! sdp_fmtp_line: String::new(), -//! rtcp_feedback: vec![], -//! }; -//! -//! let video_track = MediaStreamTrack::new( -//! "stream-123".to_string(), -//! "track-456".to_string(), -//! "Front Camera".to_string(), -//! RtpCodecKind::Video, -//! vec![RTCRtpEncodingParameters { -//! rtp_coding_parameters: RTCRtpCodingParameters { -//! ssrc: Some(789012), -//! ..Default::default() -//! }, -//! codec, -//! ..Default::default() -//! }], -//! ); -//! -//! assert_eq!(video_track.kind(), RtpCodecKind::Video); -//! assert_eq!(video_track.label(), "Front Camera"); -//! assert!(video_track.enabled()); -//! # Ok(()) -//! # } -//! ``` -//! -//! ## Controlling track state -//! -//! ``` -//! # use rtc::media_stream::MediaStreamTrack; -//! # use rtc::rtp_transceiver::rtp_sender::{RTCRtpCodec, RtpCodecKind}; -//! # fn example(mut track: MediaStreamTrack) { -//! // Disable the track temporarily -//! track.set_enabled(false); -//! assert!(!track.enabled()); -//! -//! // Re-enable the track -//! track.set_enabled(true); -//! assert!(track.enabled()); -//! -//! // Stop the track permanently -//! track.stop(); -//! # } -//! ``` +//! This module is private; the type is re-exported from +//! [`crate::media_stream`]. All user-facing documentation and examples live on +//! [`MediaStreamTrack`] itself, so that rustdoc renders them and their doctests run. //! //! # Specifications //! @@ -131,6 +68,25 @@ pub type MediaStreamTrackId = String; /// # Ok(()) /// # } /// ``` +/// +/// ## Controlling track state +/// +/// ``` +/// use rtc::media_stream::MediaStreamTrack; +/// +/// # fn example(mut track: MediaStreamTrack) { +/// // Disable the track temporarily +/// track.set_enabled(false); +/// assert!(!track.enabled()); +/// +/// // Re-enable the track +/// track.set_enabled(true); +/// assert!(track.enabled()); +/// +/// // Stop the track permanently — a stopped track cannot be restarted +/// track.stop(); +/// # } +/// ``` #[derive(Default, Debug, Clone)] pub struct MediaStreamTrack { stream_id: MediaStreamId, diff --git a/src/media_stream/track_state.rs b/src/media_stream/track_state.rs index 6085a6f1..112fc0ce 100644 --- a/src/media_stream/track_state.rs +++ b/src/media_stream/track_state.rs @@ -31,6 +31,7 @@ use std::fmt; /// assert_eq!(state.to_string(), "ended"); /// ``` #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum MediaStreamTrackState { /// Unspecified or unknown state. Unspecified, diff --git a/src/peer_connection/certificate/mod.rs b/src/peer_connection/certificate/mod.rs index 8c3de511..b9f582e1 100644 --- a/src/peer_connection/certificate/mod.rs +++ b/src/peer_connection/certificate/mod.rs @@ -31,12 +31,17 @@ //! use rtc::peer_connection::RTCPeerConnectionBuilder; //! use rtc::peer_connection::configuration::RTCConfigurationBuilder; //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { +//! let provider = crypto::default_provider()?; //! // Generate ECDSA certificate (recommended) -//! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -//! let certificate = RTCCertificate::from_key_pair(key_pair)?; +//! let certificate = RTCCertificate::generate( +//! provider.crypto(), +//! SignatureScheme::EcdsaP256Sha256, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! //! // Use in peer connection //! let peer_connection = RTCPeerConnectionBuilder::new() @@ -54,15 +59,20 @@ //! //! ``` //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { +//! let provider = crypto::default_provider()?; //! // Ed25519 provides the best security with excellent performance -//! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ED25519)?; -//! let certificate = RTCCertificate::from_key_pair(key_pair)?; +//! let certificate = RTCCertificate::generate( +//! provider.crypto(), +//! SignatureScheme::Ed25519, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! //! // Get fingerprint for SDP signaling -//! let fingerprints = certificate.get_fingerprints(); +//! let fingerprints = certificate.get_fingerprints(provider.crypto())?; //! println!("Fingerprint: {}", fingerprints[0].value); //! # Ok(()) //! # } @@ -71,21 +81,25 @@ //! ## Persist Certificate Across Sessions //! //! ```no_run -//! # #[cfg(feature = "pem")] //! # fn example() -> Result<(), Box> { +//! let provider = crypto::default_provider()?; //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! use std::fs; //! //! // First run: Generate and save certificate -//! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -//! let certificate = RTCCertificate::from_key_pair(key_pair)?; -//! let pem_data = certificate.serialize_pem(); +//! let certificate = RTCCertificate::generate( +//! provider.crypto(), +//! SignatureScheme::EcdsaP256Sha256, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; +//! let pem_data = certificate.serialize_pem()?; //! fs::write("my_cert.pem", pem_data)?; //! //! // Later runs: Load existing certificate //! let pem_data = fs::read_to_string("my_cert.pem")?; -//! let certificate = RTCCertificate::from_pem(&pem_data)?; +//! let certificate = RTCCertificate::from_pem(&pem_data, provider.crypto())?; //! // Same identity maintained across restarts! //! # Ok(()) //! # } @@ -95,14 +109,19 @@ //! //! ``` //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { -//! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -//! let certificate = RTCCertificate::from_key_pair(key_pair)?; +//! let provider = crypto::default_provider()?; +//! let certificate = RTCCertificate::generate( +//! provider.crypto(), +//! SignatureScheme::EcdsaP256Sha256, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! //! // Get fingerprints for SDP offer/answer -//! let fingerprints = certificate.get_fingerprints(); +//! let fingerprints = certificate.get_fingerprints(provider.crypto())?; //! for fp in fingerprints { //! // Format for SDP: a=fingerprint:sha-256 XX:XX:XX:... //! println!("a=fingerprint:{} {}", fp.algorithm, fp.value); @@ -115,20 +134,28 @@ //! //! ``` //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! use std::time::Instant; //! //! # fn example() -> Result<(), Box> { +//! let provider = crypto::default_provider()?; //! // ECDSA P-256: Good balance of speed and security //! let start = Instant::now(); -//! let ecdsa_kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -//! let _ecdsa_cert = RTCCertificate::from_key_pair(ecdsa_kp)?; +//! let _ecdsa_cert = RTCCertificate::generate( +//! provider.crypto(), +//! SignatureScheme::EcdsaP256Sha256, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! println!("ECDSA generation: {:?}", start.elapsed()); //! //! // Ed25519: Fastest and most secure //! let start = Instant::now(); -//! let ed_kp = KeyPair::generate_for(&rcgen::PKCS_ED25519)?; -//! let _ed_cert = RTCCertificate::from_key_pair(ed_kp)?; +//! let _ed_cert = RTCCertificate::generate( +//! provider.crypto(), +//! SignatureScheme::Ed25519, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! println!("Ed25519 generation: {:?}", start.elapsed()); //! # Ok(()) //! # } @@ -172,10 +199,6 @@ //! - Mismatched fingerprints indicate MITM attack - abort connection //! - Use out-of-band verification for high-security scenarios //! -//! # Feature Flags -//! -//! - `pem` - Enable PEM serialization/deserialization (enabled by default) -//! //! # Specifications //! //! * [W3C RTCCertificate](https://w3c.github.io/webrtc-pc/#dom-rtccertificate) @@ -184,19 +207,21 @@ //! * [RFC 8122 - WebRTC Security Architecture](https://tools.ietf.org/html/rfc8122) use std::ops::Add; +use std::sync::Arc; use std::time::{Duration, SystemTime}; -use dtls::crypto::{CryptoPrivateKey, CryptoPrivateKeyKind}; -use rcgen::{CertificateParams, KeyPair}; -#[cfg(feature = "ring")] -use ring::rand::SystemRandom; -use ring::rsa; -use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; -use sha2::{Digest, Sha256}; +use crypto::{HashAlgorithm, PublicKeyEncoding, RTCCrypto, SignatureScheme, SigningKey}; +/// X.509 certificate parameters — subject alt names, validity window, distinguished name. +/// +/// Re-exported from `rcgen` because it appears in [`RTCCertificate::generate`]'s signature and +/// must therefore be nameable without adding a direct `rcgen` dependency. Certificate *format* +/// is deliberately not a crypto-provider concern (see `docs/crypto-provider-decisions.md`), so +/// this type stays an rcgen type rather than being wrapped. +pub use rcgen::CertificateParams; +use rustls::pki_types::CertificateDer; use crate::peer_connection::transport::dtls::fingerprint::RTCDtlsFingerprint; use shared::error::{Error, Result}; -use shared::util::math_rand_alpha; /// X.509 certificate used to authenticate WebRTC peer-to-peer communications. /// @@ -221,14 +246,19 @@ use shared::util::math_rand_alpha; /// /// ``` /// # use rtc::peer_connection::certificate::RTCCertificate; -/// # use rcgen::KeyPair; +/// # use rtc::crypto::{self, SignatureScheme}; +/// # use rtc::peer_connection::certificate::CertificateParams; /// # fn example() -> Result<(), Box> { +/// let provider = crypto::default_provider()?; /// // Generate ECDSA P-256 key pair and certificate -/// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -/// let certificate = RTCCertificate::from_key_pair(key_pair)?; +/// let certificate = RTCCertificate::generate( +/// provider.crypto(), +/// SignatureScheme::EcdsaP256Sha256, +/// CertificateParams::new(vec!["localhost".to_owned()])?, +/// )?; /// /// // Certificate is ready to use -/// let fingerprints = certificate.get_fingerprints(); +/// let fingerprints = certificate.get_fingerprints(provider.crypto())?; /// println!("Certificate has {} fingerprint(s)", fingerprints.len()); /// # Ok(()) /// # } @@ -238,14 +268,19 @@ use shared::util::math_rand_alpha; /// /// ``` /// # use rtc::peer_connection::certificate::RTCCertificate; -/// # use rcgen::KeyPair; +/// # use rtc::crypto::{self, SignatureScheme}; +/// # use rtc::peer_connection::certificate::CertificateParams; /// # fn example() -> Result<(), Box> { +/// let provider = crypto::default_provider()?; /// // Generate Ed25519 key pair and certificate -/// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ED25519)?; -/// let certificate = RTCCertificate::from_key_pair(key_pair)?; +/// let certificate = RTCCertificate::generate( +/// provider.crypto(), +/// SignatureScheme::Ed25519, +/// CertificateParams::new(vec!["localhost".to_owned()])?, +/// )?; /// /// // Get fingerprints for SDP signaling -/// let fingerprints = certificate.get_fingerprints(); +/// let fingerprints = certificate.get_fingerprints(provider.crypto())?; /// for fp in fingerprints { /// println!("Fingerprint ({}):\n{}", fp.algorithm, fp.value); /// } @@ -256,20 +291,25 @@ use shared::util::math_rand_alpha; /// ## Persisting and loading certificates /// /// ``` -/// # #[cfg(feature = "pem")] /// # fn example() -> Result<(), Box> { /// # use rtc::peer_connection::certificate::RTCCertificate; -/// # use rcgen::KeyPair; -/// # let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -/// # let certificate = RTCCertificate::from_key_pair(key_pair)?; +/// # use rtc::crypto::{self, SignatureScheme}; +/// # use rtc::peer_connection::certificate::CertificateParams; +/// # let params = CertificateParams::new(vec!["localhost".to_owned()])?; +/// # let provider = crypto::default_provider()?; +/// # let certificate = RTCCertificate::generate( +/// # provider.crypto(), +/// # SignatureScheme::EcdsaP256Sha256, +/// # params, +/// # )?; /// // Serialize certificate to PEM format (includes private key) -/// let pem_string = certificate.serialize_pem(); +/// let pem_string = certificate.serialize_pem()?; /// /// // Save to file or database... /// // std::fs::write("cert.pem", &pem_string)?; /// /// // Later, load the certificate back -/// let loaded_cert = RTCCertificate::from_pem(&pem_string)?; +/// let loaded_cert = RTCCertificate::from_pem(&pem_string, provider.crypto())?; /// assert_eq!(loaded_cert, certificate); /// # Ok(()) /// # } @@ -281,11 +321,16 @@ use shared::util::math_rand_alpha; /// # use rtc::peer_connection::RTCPeerConnectionBuilder; /// # use rtc::peer_connection::configuration::RTCConfigurationBuilder; /// # use rtc::peer_connection::certificate::RTCCertificate; -/// # use rcgen::KeyPair; +/// # use rtc::crypto::{self, SignatureScheme}; +/// # use rtc::peer_connection::certificate::CertificateParams; /// # fn example() -> Result<(), Box> { +/// let provider = crypto::default_provider()?; /// // Generate certificate -/// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -/// let certificate = RTCCertificate::from_key_pair(key_pair)?; +/// let certificate = RTCCertificate::generate( +/// provider.crypto(), +/// SignatureScheme::EcdsaP256Sha256, +/// CertificateParams::new(vec!["localhost".to_owned()])?, +/// )?; /// /// // Configure peer connection with custom certificate /// let peer_connection = RTCPeerConnectionBuilder::new() @@ -319,127 +364,77 @@ impl PartialEq for RTCCertificate { } impl RTCCertificate { - /// Generates a new certificate from custom parameters. - /// - /// This is an internal method used to create certificates with specific configuration. - /// Most users should use [`from_key_pair`](Self::from_key_pair) instead. - /// - /// # Parameters - /// - /// * `params` - Certificate parameters including validity period and subject - /// * `key_pair` - The cryptographic key pair to use - /// - /// # Errors - /// - /// Returns an error if: - /// - The key pair type is not supported (must be Ed25519, ECDSA P-256, or RSA) - /// - Certificate generation fails - /// - /// # Platform Notes - /// - /// On ARM architectures, certificate expiration is capped at 48 hours due to - /// overflow issues with SystemTime arithmetic. - fn from_params(params: CertificateParams, key_pair: KeyPair) -> Result { - let not_after = params.not_after; - - let x509_cert = params.self_signed(&key_pair).unwrap(); - let serialized_der = key_pair.serialize_der(); - - let private_key = if key_pair.is_compatible(&rcgen::PKCS_ED25519) { - CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ed25519( - Ed25519KeyPair::from_pkcs8(&serialized_der) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - } - } else if key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) { - CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Ecdsa256( - EcdsaKeyPair::from_pkcs8( - &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, - &serialized_der, - #[cfg(feature = "ring")] - &SystemRandom::new(), - ) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - } - } else if key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256) { - CryptoPrivateKey { - kind: CryptoPrivateKeyKind::Rsa256( - rsa::KeyPair::from_pkcs8(&serialized_der) - .map_err(|e| Error::Other(e.to_string()))?, - ), - serialized_der, - } - } else { - return Err(Error::Other("Unsupported key_pair".to_owned())); - }; + /// Generates a self-signed certificate with a provider-owned signing key. + /// + /// `params` controls X.509 formatting and validity while `provider` owns key generation and + /// signing. This keeps certificate formatting independent from the primitive backend. + pub fn generate( + crypto: &dyn RTCCrypto, + scheme: SignatureScheme, + params: CertificateParams, + ) -> Result { + let signing_key = crypto.generate_signing_key(scheme).map_err(crypto_error)?; + Self::generate_from_signing_key(params, scheme, signing_key) + } - let expires = if cfg!(target_arch = "arm") { - // Workaround for issue overflow when adding duration to instant on armv7 - // https://github.com/webrtc-rs/examples/issues/5 https://github.com/chronotope/chrono/issues/343 - SystemTime::now().add(Duration::from_secs(172800)) //60*60*48 or 2 days - } else { - not_after.into() - }; + /// Imports a PKCS#8 private key through `provider` and associates it with an existing chain. + pub fn from_pkcs8( + crypto: &dyn RTCCrypto, + scheme: SignatureScheme, + certificate_chain: Vec>, + private_key_der: &[u8], + expires: SystemTime, + ) -> Result { + let signing_key = crypto + .import_signing_key(scheme, private_key_der) + .map_err(crypto_error)?; + Ok(Self::from_signing_key( + certificate_chain, + signing_key, + expires, + )) + } - Ok(Self { - dtls_certificate: dtls::crypto::Certificate { - certificate: vec![x509_cert.der().to_owned()], - private_key, - }, + /// Builds a certificate around an application-owned signing key, including HSM/KMS keys. + #[must_use] + pub fn from_signing_key( + certificate_chain: Vec>, + signing_key: Arc, + expires: SystemTime, + ) -> Self { + Self { + dtls_certificate: dtls::crypto::Certificate::from_signing_key( + certificate_chain, + signing_key, + ), expires, - }) + } } - /// Generates a new self-signed certificate with default parameters. - /// - /// Creates a certificate with a randomly generated common name and default - /// validity period. This is the recommended method for generating certificates - /// for WebRTC connections. - /// - /// # Parameters - /// - /// * `key_pair` - A cryptographic key pair. Must be one of: - /// - `rcgen::PKCS_ED25519` - Ed25519 (recommended for security) - /// - `rcgen::PKCS_ECDSA_P256_SHA256` - ECDSA P-256 (recommended for performance) - /// - `rcgen::PKCS_RSA_SHA256` - RSA (generation not available) - /// - /// # Errors - /// - /// Returns an error if the key pair type is not supported. - /// - /// # Examples + /// Builds a self-signed certificate around an existing provider-owned signing key. /// - /// ``` - /// # use rtc::peer_connection::certificate::RTCCertificate; - /// # use rcgen::KeyPair; - /// # fn example() -> Result<(), Box> { - /// // Generate ECDSA certificate - /// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - /// let certificate = RTCCertificate::from_key_pair(key_pair)?; + /// Use this when the key already exists — imported from PKCS#8 with + /// [`RTCCrypto::import_signing_key`](crypto::RTCCrypto::import_signing_key), or held by an + /// HSM/KMS — and a fresh self-signed X.509 wrapper is needed. Use + /// [`generate`](Self::generate) instead when the provider should create the key too. /// - /// // Certificate is ready to use in peer connection - /// let fingerprints = certificate.get_fingerprints(); - /// println!("Generated certificate with {} fingerprint(s)", fingerprints.len()); - /// # Ok(()) - /// # } - /// ``` - pub fn from_key_pair(key_pair: KeyPair) -> Result { - if !(key_pair.is_compatible(&rcgen::PKCS_ED25519) - || key_pair.is_compatible(&rcgen::PKCS_ECDSA_P256_SHA256) - || key_pair.is_compatible(&rcgen::PKCS_RSA_SHA256)) - { - return Err(Error::Other("Unsupported key_pair".to_owned())); - } - - RTCCertificate::from_params( - CertificateParams::new(vec![math_rand_alpha(16)]).unwrap(), - key_pair, - ) + /// This is the provider-neutral replacement for the removed `from_key_pair`. + pub fn generate_from_signing_key( + params: CertificateParams, + scheme: SignatureScheme, + signing_key: Arc, + ) -> Result { + let not_after = params.not_after; + let adapter = RcgenSigningKey::new(scheme, signing_key.clone())?; + let x509_cert = params + .self_signed(&adapter) + .map_err(|error| Error::Other(error.to_string()))?; + let expires = certificate_expiration(not_after); + Ok(Self::from_signing_key( + vec![x509_cert.der().to_owned()], + signing_key, + expires, + )) } /// Parses a certificate from PEM format string. @@ -467,24 +462,28 @@ impl RTCCertificate { /// # Examples /// /// ``` - /// # #[cfg(feature = "pem")] /// # fn example() -> Result<(), Box> { /// # use rtc::peer_connection::certificate::RTCCertificate; - /// # use rcgen::KeyPair; - /// # let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - /// # let original = RTCCertificate::from_key_pair(key_pair)?; + /// # use rtc::crypto::{self, SignatureScheme}; + /// # use rtc::peer_connection::certificate::CertificateParams; + /// # let params = CertificateParams::new(vec!["localhost".to_owned()])?; + /// # let provider = crypto::default_provider()?; + /// # let original = RTCCertificate::generate( + /// # provider.crypto(), + /// # SignatureScheme::EcdsaP256Sha256, + /// # params, + /// # )?; /// // Load certificate from PEM string - /// # let pem_str = original.serialize_pem(); - /// let certificate = RTCCertificate::from_pem(&pem_str)?; + /// # let pem_str = original.serialize_pem()?; + /// let certificate = RTCCertificate::from_pem(&pem_str, provider.crypto())?; /// /// // Certificate is ready to use - /// let fingerprints = certificate.get_fingerprints(); + /// let fingerprints = certificate.get_fingerprints(provider.crypto())?; /// println!("Loaded certificate with {} fingerprint(s)", fingerprints.len()); /// # Ok(()) /// # } /// ``` - #[cfg(feature = "pem")] - pub fn from_pem(pem_str: &str) -> Result { + pub fn from_pem(pem_str: &str, crypto: &dyn RTCCrypto) -> Result { let mut pem_blocks = pem_str.split("\n\n"); let first_block = if let Some(b) = pem_blocks.next() { b @@ -508,8 +507,10 @@ impl RTCCertificate { } else { return Err(Error::InvalidPEM("failed to calculate SystemTime".into())); }; - let dtls_certificate = - dtls::crypto::Certificate::from_pem(&pem_blocks.collect::>().join("\n\n"))?; + let dtls_certificate = dtls::crypto::Certificate::from_pem( + &pem_blocks.collect::>().join("\n\n"), + crypto, + )?; Ok(RTCCertificate::from_existing(dtls_certificate, expires)) } @@ -534,16 +535,19 @@ impl RTCCertificate { /// /// ```no_run /// # use rtc::peer_connection::certificate::RTCCertificate; + /// # use rtc::crypto; + /// # use rtc::dtls; /// # use std::time::{SystemTime, Duration}; /// # fn example( /// # dtls_cert: dtls::crypto::Certificate /// # ) -> Result<(), Box> { + /// # let provider = crypto::default_provider()?; /// // Use an externally managed certificate /// let expires = SystemTime::now() + Duration::from_secs(86400 * 30); // 30 days /// let certificate = RTCCertificate::from_existing(dtls_cert, expires); /// /// // Certificate is ready to use - /// let fingerprints = certificate.get_fingerprints(); + /// let fingerprints = certificate.get_fingerprints(provider.crypto())?; /// println!("Certificate has {} fingerprint(s)", fingerprints.len()); /// # Ok(()) /// # } @@ -559,7 +563,7 @@ impl RTCCertificate { /// /// Produces a PEM-encoded string containing both the certificate and its private /// key in PKCS#8 format. The output can be safely stored and later loaded with - /// `from_pem` (requires the `pem` feature). + /// `from_pem`. /// /// # Security Warning /// @@ -576,26 +580,30 @@ impl RTCCertificate { /// # Examples /// /// ``` - /// # #[cfg(feature = "pem")] /// # fn example() -> Result<(), Box> { /// # use rtc::peer_connection::certificate::RTCCertificate; - /// # use rcgen::KeyPair; - /// # let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - /// # let certificate = RTCCertificate::from_key_pair(key_pair)?; + /// # use rtc::crypto::{self, SignatureScheme}; + /// # use rtc::peer_connection::certificate::CertificateParams; + /// # let params = CertificateParams::new(vec!["localhost".to_owned()])?; + /// # let provider = crypto::default_provider()?; + /// # let certificate = RTCCertificate::generate( + /// # provider.crypto(), + /// # SignatureScheme::EcdsaP256Sha256, + /// # params, + /// # )?; /// // Serialize for storage - /// let pem_string = certificate.serialize_pem(); + /// let pem_string = certificate.serialize_pem()?; /// /// // Save to secure storage /// // std::fs::write("private/cert.pem", &pem_string)?; /// /// // Later, reload it - /// let reloaded = RTCCertificate::from_pem(&pem_string)?; + /// let reloaded = RTCCertificate::from_pem(&pem_string, provider.crypto())?; /// assert_eq!(certificate, reloaded); /// # Ok(()) /// # } /// ``` - #[cfg(any(doc, feature = "pem"))] - pub fn serialize_pem(&self) -> String { + pub fn serialize_pem(&self) -> Result { // Encode `expires` as a PEM block. // // TODO: serialize as nanos when https://github.com/rust-lang/rust/issues/103332 is fixed. @@ -608,11 +616,11 @@ impl RTCCertificate { .to_le_bytes() .to_vec(), ); - format!( + Ok(format!( "{}\n{}", pem::encode(&expires_pem), - self.dtls_certificate.serialize_pem() - ) + self.dtls_certificate.serialize_pem()? + )) } /// Returns SHA-256 fingerprints of the certificate chain. @@ -640,26 +648,31 @@ impl RTCCertificate { /// /// ``` /// # use rtc::peer_connection::certificate::RTCCertificate; - /// # use rcgen::KeyPair; + /// # use rtc::crypto::{self, SignatureScheme}; + /// # use rtc::peer_connection::certificate::CertificateParams; /// # fn example() -> Result<(), Box> { - /// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - /// let certificate = RTCCertificate::from_key_pair(key_pair)?; + /// let provider = crypto::default_provider()?; + /// let certificate = RTCCertificate::generate( + /// provider.crypto(), + /// SignatureScheme::EcdsaP256Sha256, + /// CertificateParams::new(vec!["localhost".to_owned()])?, + /// )?; /// /// // Get fingerprints for SDP - /// let fingerprints = certificate.get_fingerprints(); + /// let fingerprints = certificate.get_fingerprints(provider.crypto())?; /// for fp in fingerprints { /// println!("a=fingerprint:{} {}", fp.algorithm, fp.value); /// } /// # Ok(()) /// # } /// ``` - pub fn get_fingerprints(&self) -> Vec { + pub fn get_fingerprints(&self, crypto: &dyn RTCCrypto) -> Result> { let mut fingerprints = Vec::new(); for c in &self.dtls_certificate.certificate { - let mut h = Sha256::new(); - h.update(c.as_ref()); - let hashed = h.finalize(); + let hashed = crypto + .hash(HashAlgorithm::Sha256, c.as_ref()) + .map_err(crypto_error)?; let values: Vec = hashed.iter().map(|x| format! {"{x:02x}"}).collect(); fingerprints.push(RTCDtlsFingerprint { @@ -668,48 +681,206 @@ impl RTCCertificate { }); } - fingerprints + Ok(fingerprints) + } +} + +fn crypto_error(error: crypto::CryptoError) -> Error { + Error::Crypto(error.to_string()) +} + +fn certificate_expiration(not_after: impl Into) -> SystemTime { + if cfg!(target_arch = "arm") { + // Workaround for issue overflow when adding duration to instant on armv7. + SystemTime::now().add(Duration::from_secs(172800)) + } else { + not_after.into() + } +} + +struct RcgenSigningKey { + scheme: SignatureScheme, + algorithm: &'static rcgen::SignatureAlgorithm, + signing_key: Arc, + public_key: Vec, +} + +impl RcgenSigningKey { + fn new(scheme: SignatureScheme, signing_key: Arc) -> Result { + let algorithm = match scheme { + SignatureScheme::Ed25519 => &rcgen::PKCS_ED25519, + SignatureScheme::EcdsaP256Sha256 => &rcgen::PKCS_ECDSA_P256_SHA256, + SignatureScheme::EcdsaP384Sha384 => &rcgen::PKCS_ECDSA_P384_SHA384, + SignatureScheme::RsaPkcs1Sha256 => &rcgen::PKCS_RSA_SHA256, + SignatureScheme::RsaPkcs1Sha384 => &rcgen::PKCS_RSA_SHA384, + SignatureScheme::RsaPkcs1Sha512 => &rcgen::PKCS_RSA_SHA512, + _ => { + return Err(Error::Crypto(format!( + "certificate generation does not support {scheme:?}" + ))); + } + }; + if !signing_key.supports(scheme) { + return Err(Error::Crypto(format!( + "signing key does not support {scheme:?}" + ))); + } + let public_key = signing_key.public_key(); + let public_key = match public_key.encoding { + PublicKeyEncoding::SubjectPublicKeyInfoDer => { + use x509_parser::prelude::FromDer; + let (remaining, subject_public_key_info) = + x509_parser::x509::SubjectPublicKeyInfo::from_der(public_key.bytes) + .map_err(|error| Error::Other(error.to_string()))?; + if !remaining.is_empty() { + return Err(Error::Other( + "trailing bytes in SubjectPublicKeyInfo".to_owned(), + )); + } + subject_public_key_info.subject_public_key.data.to_vec() + } + PublicKeyEncoding::EcUncompressedPoint + | PublicKeyEncoding::Ed25519Raw + | PublicKeyEncoding::RsaPkcs1Der => public_key.bytes.to_vec(), + _ => { + return Err(Error::Crypto(format!( + "certificate generation does not support public-key encoding {:?}", + public_key.encoding + ))); + } + }; + Ok(Self { + scheme, + algorithm, + signing_key, + public_key, + }) + } +} + +impl rcgen::PublicKeyData for RcgenSigningKey { + fn der_bytes(&self) -> &[u8] { + &self.public_key + } + + fn algorithm(&self) -> &'static rcgen::SignatureAlgorithm { + self.algorithm + } +} + +impl rcgen::SigningKey for RcgenSigningKey { + fn sign(&self, message: &[u8]) -> std::result::Result, rcgen::Error> { + self.signing_key + .sign(self.scheme, message) + .map_err(|_| rcgen::Error::RemoteKeyError) } } -#[cfg(test)] +#[cfg(all(test, any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] mod test { use super::*; + use crypto::RTCCryptoProvider; + + struct NonExportableSigningKey(Arc); + + impl SigningKey for NonExportableSigningKey { + fn supports(&self, scheme: SignatureScheme) -> bool { + self.0.supports(scheme) + } + + fn public_key(&self) -> crypto::PublicKey<'_> { + self.0.public_key() + } + + fn sign( + &self, + scheme: SignatureScheme, + message: &[u8], + ) -> std::result::Result, crypto::CryptoError> { + self.0.sign(scheme, message) + } + } + + fn default_test_provider() -> Result> { + crypto::default_provider().map_err(crypto_error) + } + + fn provider_certificate(crypto: &dyn RTCCrypto) -> Result { + RTCCertificate::generate( + crypto, + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + ) + } #[test] fn test_generate_certificate_rsa() -> Result<()> { - let key_pair = KeyPair::generate_for(&rcgen::PKCS_RSA_SHA256); - #[cfg(feature = "ring")] - assert!(key_pair.is_err(), "RcgenError::KeyGenerationUnavailable"); - #[cfg(feature = "aws-lc-rs")] - let _cert = RTCCertificate::from_key_pair(key_pair?)?; + let provider = default_test_provider()?; + + // Neither built-in provider generates RSA keys, mirroring rcgen's + // `KeyGenerationUnavailable` under `ring`. The certificate path must still work for any + // provider that does support it, so this asserts success or an explicit + // unsupported-algorithm error, never a silent failure. `tests/dtls_rsa_certificate.rs` + // covers RSA end to end using an imported fixture key. + if !provider + .crypto() + .supports(crypto::CryptoAlgorithm::SigningKeyGeneration( + SignatureScheme::RsaPkcs1Sha256, + )) + { + return Ok(()); + } + + let _certificate = RTCCertificate::generate( + provider.crypto(), + SignatureScheme::RsaPkcs1Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; Ok(()) } #[test] fn test_generate_certificate_ecdsa() -> Result<()> { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let _cert = RTCCertificate::from_key_pair(kp)?; + let _cert = RTCCertificate::generate( + default_test_provider()?.crypto(), + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; Ok(()) } #[test] fn test_generate_certificate_eddsa() -> Result<()> { - let kp = KeyPair::generate_for(&rcgen::PKCS_ED25519)?; - let _cert = RTCCertificate::from_key_pair(kp)?; + let _cert = RTCCertificate::generate( + default_test_provider()?.crypto(), + SignatureScheme::Ed25519, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; Ok(()) } #[test] fn test_certificate_equal() -> Result<()> { - let kp1 = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert1 = RTCCertificate::from_key_pair(kp1)?; - - let kp2 = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert2 = RTCCertificate::from_key_pair(kp2)?; + let cert1 = RTCCertificate::generate( + default_test_provider()?.crypto(), + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; + + let cert2 = RTCCertificate::generate( + default_test_provider()?.crypto(), + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; assert_ne!(cert1, cert2); @@ -718,8 +889,12 @@ mod test { #[test] fn test_generate_certificate_expires() -> Result<()> { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert = RTCCertificate::from_key_pair(kp)?; + let cert = RTCCertificate::generate( + default_test_provider()?.crypto(), + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; let now = SystemTime::now(); assert!(cert.expires.duration_since(now).is_ok()); @@ -727,17 +902,78 @@ mod test { Ok(()) } - #[cfg(feature = "pem")] #[test] fn test_certificate_serialize_pem_and_from_pem() -> Result<()> { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert = RTCCertificate::from_key_pair(kp)?; + let cert = RTCCertificate::generate( + default_test_provider()?.crypto(), + SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; - let pem = cert.serialize_pem(); - let loaded_cert = RTCCertificate::from_pem(&pem)?; + let pem = cert.serialize_pem()?; + let loaded_cert = RTCCertificate::from_pem(&pem, default_test_provider()?.crypto())?; assert_eq!(loaded_cert, cert); Ok(()) } + + #[cfg(feature = "crypto-ring")] + #[test] + fn ring_provider_generates_imports_and_fingerprints_certificates() -> Result<()> { + provider_certificate_round_trip(Arc::new(crypto::providers::RingProvider::new()).crypto()) + } + + #[cfg(feature = "crypto-aws-lc-rs")] + #[test] + fn aws_provider_generates_imports_and_fingerprints_certificates() -> Result<()> { + provider_certificate_round_trip( + Arc::new(crypto::providers::AwsLcRsProvider::new()).crypto(), + ) + } + + fn provider_certificate_round_trip(crypto: &dyn RTCCrypto) -> Result<()> { + let certificate = provider_certificate(crypto)?; + let fingerprints = certificate.get_fingerprints(crypto)?; + assert_eq!(fingerprints.len(), 1); + assert_eq!(fingerprints[0].algorithm, "sha-256"); + + let pem = certificate.serialize_pem()?; + let imported = RTCCertificate::from_pem(&pem, crypto)?; + assert_eq!(imported, certificate); + + let private_key = certificate + .dtls_certificate + .private_key + .signing_key + .to_pkcs8_der() + .map_err(crypto_error)? + .expect("built-in generated keys are exportable"); + let imported = RTCCertificate::from_pkcs8( + crypto, + SignatureScheme::EcdsaP256Sha256, + certificate.dtls_certificate.certificate.clone(), + private_key.as_ref(), + certificate.expires, + )?; + assert_eq!(imported, certificate); + Ok(()) + } + + #[test] + fn non_exportable_signing_key_returns_an_explicit_pem_error() -> Result<()> { + let provider = crypto::default_provider().map_err(crypto_error)?; + let certificate = provider_certificate(provider.crypto())?; + let signing_key = certificate.dtls_certificate.private_key.signing_key.clone(); + let certificate = RTCCertificate::from_signing_key( + certificate.dtls_certificate.certificate, + Arc::new(NonExportableSigningKey(signing_key)), + certificate.expires, + ); + + let error = certificate.serialize_pem().unwrap_err(); + assert!(error.to_string().contains("not exportable")); + Ok(()) + } } diff --git a/src/peer_connection/configuration/bundle_policy.rs b/src/peer_connection/configuration/bundle_policy.rs index 9ab86ab6..755f6c1b 100644 --- a/src/peer_connection/configuration/bundle_policy.rs +++ b/src/peer_connection/configuration/bundle_policy.rs @@ -32,6 +32,7 @@ use serde::{Deserialize, Serialize}; /// /// * [W3C RTCBundlePolicy](https://w3c.github.io/webrtc-pc/#rtcbundlepolicy-enum) #[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCBundlePolicy { /// Unspecified - not a valid policy, used as default value #[default] diff --git a/src/peer_connection/configuration/ice_transport_policy.rs b/src/peer_connection/configuration/ice_transport_policy.rs index d783ca73..a482031e 100644 --- a/src/peer_connection/configuration/ice_transport_policy.rs +++ b/src/peer_connection/configuration/ice_transport_policy.rs @@ -57,6 +57,7 @@ use serde::{Deserialize, Serialize}; /// * [W3C RTCIceTransportPolicy](https://w3c.github.io/webrtc-pc/#rtcicetransportpolicy-enum) /// * [RFC 8445 - ICE](https://tools.ietf.org/html/rfc8445) #[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceTransportPolicy { /// Unspecified - not a valid policy, used as default value #[default] diff --git a/src/peer_connection/configuration/interceptor_registry.rs b/src/peer_connection/configuration/interceptor_registry.rs index e23823e1..51e2856b 100644 --- a/src/peer_connection/configuration/interceptor_registry.rs +++ b/src/peer_connection/configuration/interceptor_registry.rs @@ -28,33 +28,40 @@ //! For most applications, use [`register_default_interceptors`] to enable //! standard WebRTC functionality: //! -//! ```ignore +//! ```no_run +//! use rtc::interceptor::Registry; +//! use rtc::peer_connection::RTCPeerConnectionBuilder; //! use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors; //! use rtc::peer_connection::configuration::media_engine::MediaEngine; -//! use interceptor::Registry; //! +//! # fn example() -> Result<(), Box> { //! let mut media_engine = MediaEngine::default(); //! let registry = Registry::new(); //! -//! // Register NACK, RTCP reports, simulcast headers, and TWCC receiver +//! // Register NACK, RTCP reports, simulcast headers, and TWCC receiver. +//! // Note this takes `&mut media_engine`: it registers the RTCP feedback types and +//! // header extensions the interceptors need, so pass that same engine to the builder. //! let registry = register_default_interceptors(registry, &mut media_engine)?; //! -//! // Use with RTCConfigurationBuilder -//! let config = RTCConfigurationBuilder::new() +//! let pc = RTCPeerConnectionBuilder::new() //! .with_media_engine(media_engine) //! .with_interceptor_registry(registry) -//! .build(); +//! .build()?; +//! # Ok(()) +//! # } //! ``` //! //! # Custom Configuration //! //! For fine-grained control, configure individual interceptors: //! -//! ```ignore -//! use rtc::peer_connection::configuration::interceptor_registry::*; +//! ```no_run +//! use rtc::interceptor::Registry; +//! use rtc::peer_connection::RTCPeerConnectionBuilder; +//! use rtc::peer_connection::configuration::interceptor_registry::{configure_nack, configure_twcc}; //! use rtc::peer_connection::configuration::media_engine::MediaEngine; -//! use interceptor::Registry; //! +//! # fn example() -> Result<(), Box> { //! let mut media_engine = MediaEngine::default(); //! let registry = Registry::new(); //! @@ -63,6 +70,13 @@ //! //! // Or enable full TWCC for bandwidth estimation //! let registry = configure_twcc(registry, &mut media_engine)?; +//! +//! let pc = RTCPeerConnectionBuilder::new() +//! .with_media_engine(media_engine) +//! .with_interceptor_registry(registry) +//! .build()?; +//! # Ok(()) +//! # } //! ``` //! //! # Available Configurations @@ -117,14 +131,17 @@ use shared::error::Result; /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::interceptor::Registry; /// use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors; /// use rtc::peer_connection::configuration::media_engine::MediaEngine; -/// use interceptor::Registry; /// +/// # fn example() -> Result<(), Box> { /// let mut media_engine = MediaEngine::default(); /// let registry = Registry::new(); /// let registry = register_default_interceptors(registry, &mut media_engine)?; +/// # Ok(()) +/// # } /// ``` /// /// # Customization @@ -169,10 +186,10 @@ where /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::interceptor::Registry; /// use rtc::peer_connection::configuration::interceptor_registry::configure_nack; /// use rtc::peer_connection::configuration::media_engine::MediaEngine; -/// use interceptor::Registry; /// /// let mut media_engine = MediaEngine::default(); /// let registry = Registry::new(); @@ -237,9 +254,9 @@ where /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::interceptor::Registry; /// use rtc::peer_connection::configuration::interceptor_registry::configure_rtcp_reports; -/// use interceptor::Registry; /// /// let registry = Registry::new(); /// let registry = configure_rtcp_reports(registry); @@ -276,12 +293,15 @@ where /// /// # Example /// -/// ```ignore +/// ``` /// use rtc::peer_connection::configuration::interceptor_registry::configure_simulcast_extension_headers; /// use rtc::peer_connection::configuration::media_engine::MediaEngine; /// +/// # fn example() -> Result<(), Box> { /// let mut media_engine = MediaEngine::default(); /// configure_simulcast_extension_headers(&mut media_engine)?; +/// # Ok(()) +/// # } /// ``` /// /// # References @@ -344,14 +364,17 @@ pub fn configure_simulcast_extension_headers(media_engine: &mut MediaEngine) -> /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::interceptor::Registry; /// use rtc::peer_connection::configuration::interceptor_registry::configure_twcc; /// use rtc::peer_connection::configuration::media_engine::MediaEngine; -/// use interceptor::Registry; /// +/// # fn example() -> Result<(), Box> { /// let mut media_engine = MediaEngine::default(); /// let registry = Registry::new(); /// let registry = configure_twcc(registry, &mut media_engine)?; +/// # Ok(()) +/// # } /// ``` /// /// # References @@ -423,14 +446,17 @@ where /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::interceptor::Registry; /// use rtc::peer_connection::configuration::interceptor_registry::configure_twcc_sender_only; /// use rtc::peer_connection::configuration::media_engine::MediaEngine; -/// use interceptor::Registry; /// +/// # fn example() -> Result<(), Box> { /// let mut media_engine = MediaEngine::default(); /// let registry = Registry::new(); /// let registry = configure_twcc_sender_only(registry, &mut media_engine)?; +/// # Ok(()) +/// # } /// ``` pub fn configure_twcc_sender_only

( registry: Registry

, @@ -501,14 +527,17 @@ where /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::interceptor::Registry; /// use rtc::peer_connection::configuration::interceptor_registry::configure_twcc_receiver_only; /// use rtc::peer_connection::configuration::media_engine::MediaEngine; -/// use interceptor::Registry; /// +/// # fn example() -> Result<(), Box> { /// let mut media_engine = MediaEngine::default(); /// let registry = Registry::new(); /// let registry = configure_twcc_receiver_only(registry, &mut media_engine)?; +/// # Ok(()) +/// # } /// ``` pub fn configure_twcc_receiver_only

( registry: Registry

, diff --git a/src/peer_connection/configuration/media_engine.rs b/src/peer_connection/configuration/media_engine.rs index 1c301c3b..a715410c 100644 --- a/src/peer_connection/configuration/media_engine.rs +++ b/src/peer_connection/configuration/media_engine.rs @@ -403,9 +403,7 @@ impl MediaEngine { // type) fmtp parameter points at the primary codec this RTX stream // repairs. Browsers offer an RTX codec for every video codec by default // and drop RTX negotiation for any primary we do not pair one with, so - // register one RTX codec per primary video codec (mirroring pion's - // RegisterDefaultCodecs). ulpfec is a FEC codec, not a media codec, and - // does not get an RTX pairing. + // register one RTX codec per primary video codec. let rtx_codec = |payload_type: PayloadType, apt: PayloadType| RTCRtpCodecParameters { rtp_codec: RTCRtpCodec { mime_type: MIME_TYPE_RTX.to_owned(), @@ -550,16 +548,6 @@ impl MediaEngine { payload_type: 126, }, rtx_codec(107, 126), - RTCRtpCodecParameters { - rtp_codec: RTCRtpCodec { - mime_type: "video/ulpfec".to_owned(), - clock_rate: 90000, - channels: 0, - sdp_fmtp_line: "".to_owned(), - rtcp_feedback: vec![], - }, - payload_type: 116, - }, ] { self.register_codec(codec, RtpCodecKind::Video)?; } @@ -1166,6 +1154,61 @@ impl MediaEngine { } } +#[cfg(test)] +mod default_fec_codec_tests { + use super::*; + + // Issue #837: the default media engine must not offer ULPFEC. The receive path does + // not recover media from ULPFEC packets, so advertising it invites a peer to send + // repair packets we cannot use. `MIME_TYPE_ULP_FEC` stays exported for applications + // that register it deliberately; only the *default* registration is removed. + #[test] + fn default_codecs_do_not_offer_ulpfec() { + let mut me = MediaEngine::default(); + me.register_default_codecs().unwrap(); + + let ulpfec: Vec<_> = me + .video_codecs + .iter() + .filter(|c| { + UniCase::new(c.rtp_codec.mime_type.as_str()) == UniCase::new(MIME_TYPE_ULP_FEC) + }) + .map(|c| c.payload_type) + .collect(); + + assert!( + ulpfec.is_empty(), + "default codecs must not advertise ULPFEC while the receive path cannot \ + recover it; found payload type(s) {ulpfec:?}" + ); + } + + // The constant remains part of the public API so an application can opt in. + #[test] + fn ulpfec_remains_registerable_explicitly() { + let mut me = MediaEngine::default(); + me.register_default_codecs().unwrap(); + me.register_codec( + RTCRtpCodecParameters { + rtp_codec: RTCRtpCodec { + mime_type: MIME_TYPE_ULP_FEC.to_owned(), + clock_rate: 90000, + channels: 0, + sdp_fmtp_line: "".to_owned(), + rtcp_feedback: vec![], + }, + payload_type: 116, + }, + RtpCodecKind::Video, + ) + .expect("an application may still register ULPFEC itself"); + + assert!(me.video_codecs.iter().any(|c| { + UniCase::new(c.rtp_codec.mime_type.as_str()) == UniCase::new(MIME_TYPE_ULP_FEC) + })); + } +} + #[cfg(test)] mod default_rtx_codec_tests { use super::*; @@ -1186,10 +1229,7 @@ mod default_rtx_codec_tests { let apt = parse_rtx_apt(&codec.rtp_codec.sdp_fmtp_line) .expect("rtx codec must carry an apt= parameter"); rtx_apts.push(apt); - } else if UniCase::new(codec.rtp_codec.mime_type.as_str()) - != UniCase::new("video/ulpfec") - { - // ulpfec is a FEC codec and has no RTX pairing. + } else { primary_payload_types.push(codec.payload_type); } } diff --git a/src/peer_connection/configuration/mod.rs b/src/peer_connection/configuration/mod.rs index 0c2b6ac2..705a6e12 100644 --- a/src/peer_connection/configuration/mod.rs +++ b/src/peer_connection/configuration/mod.rs @@ -97,12 +97,17 @@ //! ``` //! use rtc::peer_connection::configuration::RTCConfigurationBuilder; //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { +//! let provider = crypto::default_provider()?; //! // Generate custom certificate for peer identity -//! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -//! let certificate = RTCCertificate::from_key_pair(key_pair)?; +//! let certificate = RTCCertificate::generate( +//! provider.crypto(), +//! SignatureScheme::EcdsaP256Sha256, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! //! let config = RTCConfigurationBuilder::new() //! .with_certificates(vec![certificate]) @@ -150,11 +155,16 @@ //! }; //! use rtc::peer_connection::configuration::RTCIceServer; //! use rtc::peer_connection::certificate::RTCCertificate; -//! use rcgen::KeyPair; +//! use rtc::crypto::{self, SignatureScheme}; +//! use rtc::peer_connection::certificate::CertificateParams; //! //! # fn example() -> Result<(), Box> { -//! let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -//! let certificate = RTCCertificate::from_key_pair(key_pair)?; +//! let provider = crypto::default_provider()?; +//! let certificate = RTCCertificate::generate( +//! provider.crypto(), +//! SignatureScheme::EcdsaP256Sha256, +//! CertificateParams::new(vec!["localhost".to_owned()])?, +//! )?; //! //! let config = RTCConfigurationBuilder::new() //! .with_ice_servers(vec![ @@ -205,7 +215,6 @@ use crate::peer_connection::certificate::RTCCertificate; pub use crate::peer_connection::transport::ice::server::RTCIceServer; -use rcgen::KeyPair; use shared::error::{Error, Result}; use std::time::SystemTime; @@ -389,11 +398,7 @@ impl RTCConfiguration { .duration_since(now) .map_err(|_| Error::ErrCertificateExpired)?; } - } else { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert = RTCCertificate::from_key_pair(kp)?; - self.certificates = vec![cert]; - }; + } Ok(()) } @@ -478,11 +483,16 @@ impl RTCConfiguration { /// ``` /// use rtc::peer_connection::configuration::RTCConfigurationBuilder; /// use rtc::peer_connection::certificate::RTCCertificate; -/// use rcgen::KeyPair; +/// use rtc::crypto::{self, SignatureScheme}; +/// use rtc::peer_connection::certificate::CertificateParams; /// /// # fn example() -> Result<(), Box> { -/// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; -/// let certificate = RTCCertificate::from_key_pair(key_pair)?; +/// let provider = crypto::default_provider()?; +/// let certificate = RTCCertificate::generate( +/// provider.crypto(), +/// SignatureScheme::EcdsaP256Sha256, +/// CertificateParams::new(vec!["localhost".to_owned()])?, +/// )?; /// /// let config = RTCConfigurationBuilder::new() /// .with_certificates(vec![certificate]) @@ -677,11 +687,16 @@ impl RTCConfigurationBuilder { /// ``` /// use rtc::peer_connection::configuration::RTCConfigurationBuilder; /// use rtc::peer_connection::certificate::RTCCertificate; - /// use rcgen::KeyPair; + /// use rtc::crypto::{self, SignatureScheme}; + /// use rtc::peer_connection::certificate::CertificateParams; /// /// # fn example() -> Result<(), Box> { - /// let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - /// let certificate = RTCCertificate::from_key_pair(key_pair)?; + /// let provider = crypto::default_provider()?; + /// let certificate = RTCCertificate::generate( + /// provider.crypto(), + /// SignatureScheme::EcdsaP256Sha256, + /// CertificateParams::new(vec!["localhost".to_owned()])?, + /// )?; /// /// let config = RTCConfigurationBuilder::new() /// .with_certificates(vec![certificate]) diff --git a/src/peer_connection/configuration/rtcp_mux_policy.rs b/src/peer_connection/configuration/rtcp_mux_policy.rs index 9bf83ea5..d584f2b7 100644 --- a/src/peer_connection/configuration/rtcp_mux_policy.rs +++ b/src/peer_connection/configuration/rtcp_mux_policy.rs @@ -34,6 +34,7 @@ use serde::{Deserialize, Serialize}; /// * [W3C RTCRtcpMuxPolicy](https://w3c.github.io/webrtc-pc/#rtcrtcpmuxpolicy-enum) /// * [RFC 5761 - Multiplexing RTP and RTCP](https://tools.ietf.org/html/rfc5761) #[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCRtcpMuxPolicy { /// Unspecified - not a valid policy, used as default value #[default] diff --git a/src/peer_connection/configuration/sdp_semantics.rs b/src/peer_connection/configuration/sdp_semantics.rs index 10e93b12..c17d063e 100644 --- a/src/peer_connection/configuration/sdp_semantics.rs +++ b/src/peer_connection/configuration/sdp_semantics.rs @@ -34,6 +34,7 @@ use serde::{Deserialize, Serialize}; /// * [Unified Plan](https://tools.ietf.org/html/draft-roach-mmusic-unified-plan-00) /// * [Plan B (deprecated)](https://tools.ietf.org/html/draft-uberti-rtcweb-plan-00) #[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCSdpSemantics { /// Unspecified - not a valid semantic Unspecified = 0, diff --git a/src/peer_connection/configuration/setting_engine.rs b/src/peer_connection/configuration/setting_engine.rs index 0e96fc24..680520a5 100644 --- a/src/peer_connection/configuration/setting_engine.rs +++ b/src/peer_connection/configuration/setting_engine.rs @@ -108,6 +108,8 @@ use std::net::IpAddr; use std::sync::Arc; +use crypto::RTCCryptoProvider; +use dtls::cipher_suite::CipherSuiteId; use dtls::extension::extension_use_srtp::SrtpProtectionProfile; //TODO: use ice::agent::agent_config::{InterfaceFilterFn, IpFilterFn}; //TODO: use ice::mdns::MulticastDnsMode; @@ -250,6 +252,7 @@ pub struct ReplayProtection { /// Controls the maximum size of messages that can be sent through data channels. /// Per [RFC 8841](https://datatracker.ietf.org/doc/html/rfc8841), the default is 64KB. #[derive(Copy, Clone)] +#[non_exhaustive] pub enum SctpMaxMessageSize { /// Fixed maximum message size in bytes. Bounded(u32), @@ -330,6 +333,7 @@ impl Default for SctpMaxMessageSize { /// - [RFC 8445 - ICE](https://datatracker.ietf.org/doc/html/rfc8445) #[derive(Default, Clone)] pub struct SettingEngine { + pub(crate) crypto_provider: Option>, pub(crate) timeout: Timeout, pub(crate) candidates: Candidates, pub(crate) multicast_dns: MulticastDNS, @@ -346,6 +350,7 @@ pub struct SettingEngine { pub(crate) disable_media_engine_copy: bool, pub(crate) disable_media_engine_multiple_codecs: bool, pub(crate) srtp_protection_profiles: Vec, + pub(crate) dtls_cipher_suites: Vec, pub(crate) receive_mtu: usize, pub(crate) mid_generator: Option String + Send + Sync>>, /// Determines the max size of any message that may be sent through an SCTP transport. @@ -358,6 +363,28 @@ pub struct SettingEngine { } impl SettingEngine { + /// Selects the cryptographic provider used by peer connections built with this setting engine. + /// + /// The provider is resolved once at peer-connection construction and shared with ICE, DTLS, + /// SRTP, certificate, and fingerprint operations. Different peer connections may select + /// different providers in the same process. + pub fn set_crypto_provider(&mut self, provider: Arc) { + self.crypto_provider = Some(provider); + } + + /// The crypto provider configured on this engine, if any. + /// + /// `None` means no provider has been set, so building a peer connection will resolve the + /// feature-selected built-in. + /// + /// Callers assembling additional components around a peer connection — an async wrapper's + /// TURN client, for instance — read the provider from here and pass it to + /// [`set_crypto_provider`](Self::set_crypto_provider) before building, so the whole + /// connection provably shares one provider instead of resolving a second. + pub fn crypto_provider(&self) -> Option<&Arc> { + self.crypto_provider.as_ref() + } + /// Returns the configured receive MTU, or the default if not set. pub(crate) fn get_receive_mtu(&self) -> usize { if self.receive_mtu != 0 { @@ -397,6 +424,38 @@ impl SettingEngine { self.srtp_protection_profiles = profiles } + /// Restricts the DTLS cipher suites offered during the handshake. + /// + /// An empty list (the default) uses the `dtls` crate's built-in set, which offers + /// **both** ECDHE_ECDSA and ECDHE_RSA suites. That is deliberate — the local + /// certificate is not known when the list is compiled — but it means a remote peer may + /// select an ECDHE_RSA suite that an ECDSA certificate cannot satisfy, and the + /// handshake then stalls with the connection stuck in `Connecting`. + /// + /// Certificates generated by this crate are ECDSA (P-256), so an application that does + /// not supply its own RSA certificate can pin the ECDSA suites and remove that + /// possibility: + /// + /// ``` + /// use dtls::cipher_suite::CipherSuiteId; + /// use rtc::peer_connection::configuration::setting_engine::SettingEngine; + /// + /// let mut setting_engine = SettingEngine::default(); + /// + /// setting_engine.set_dtls_cipher_suites(vec![ + /// CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + /// CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha, + /// CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256, + /// ]); + /// ``` + /// + /// The order is a preference order. Every suite named must be one the `dtls` crate + /// implements, or building the transport fails with `ErrInvalidCipherSuite`; a list + /// that filters down to nothing usable fails with `ErrNoAvailableCipherSuites`. + pub fn set_dtls_cipher_suites(&mut self, cipher_suites: Vec) { + self.dtls_cipher_suites = cipher_suites + } + /// Configures ICE timeout behavior for connection health monitoring. /// /// These timeouts control when ICE transitions between connection states @@ -1133,7 +1192,7 @@ impl SettingEngine { /// bytes**; smaller values (including `0`) are raised to that floor here, because a /// sub-1500 window makes the peer reject this endpoint's INIT/INIT-ACK and the SCTP /// association never establishes. The window should also be **≥ the largest SCTP - /// message this endpoint will receive** ([`set_sctp_max_message_size`], default + /// message this endpoint will receive** ([`Self::set_sctp_max_message_size`], default /// 64 KiB): a buffer smaller than one message cannot hold it for reassembly, so a /// full-size inbound message would stall that receive direction. `0` here is *not* /// "unbounded" (unlike some other knobs) — to keep the default window, leave this diff --git a/src/peer_connection/event/data_channel_event.rs b/src/peer_connection/event/data_channel_event.rs index 92a9a5b8..2bb2b4c9 100644 --- a/src/peer_connection/event/data_channel_event.rs +++ b/src/peer_connection/event/data_channel_event.rs @@ -81,6 +81,7 @@ use crate::data_channel::RTCDataChannelId; /// - [W3C RTCDataChannel](https://www.w3.org/TR/webrtc/#rtcdatachannel) #[allow(clippy::enum_variant_names)] #[derive(Debug, Clone)] +#[non_exhaustive] pub enum RTCDataChannelEvent { /// Data channel has opened and is ready to send/receive data. /// diff --git a/src/peer_connection/event/mod.rs b/src/peer_connection/event/mod.rs index 710d693a..8641ff62 100644 --- a/src/peer_connection/event/mod.rs +++ b/src/peer_connection/event/mod.rs @@ -12,18 +12,25 @@ //! //! # Examples //! -//! ## Basic event loop pattern (conceptual) +//! ## Basic event loop pattern //! -//! ```ignore -//! // Note: poll_event() and related methods are part of the sans-I/O design -//! // but may not be fully exposed in the current public API -//! use rtc::peer_connection::RTCPeerConnection; +//! `poll_event()`, `poll_timeout()` and `handle_timeout()` come from the +//! [`sansio::Protocol`] implementation on [`RTCPeerConnection`](crate::peer_connection::RTCPeerConnection), +//! so that trait must be in +//! scope to call them. +//! +//! ```no_run +//! use rtc::peer_connection::RTCPeerConnectionBuilder; //! use rtc::peer_connection::configuration::RTCConfigurationBuilder; //! use rtc::peer_connection::event::RTCPeerConnectionEvent; +//! use rtc::sansio::Protocol; //! use std::time::Instant; //! +//! # fn example() -> Result<(), Box> { //! let config = RTCConfigurationBuilder::new().build(); -//! let mut peer_connection = RTCPeerConnection::new(config)?; +//! let mut peer_connection = RTCPeerConnectionBuilder::new() +//! .with_configuration(config) +//! .build()?; //! //! loop { //! // Poll and handle events @@ -49,7 +56,8 @@ //! //! break; // Exit for example //! } -//! # Ok::<(), Box>(()) +//! # Ok(()) +//! # } //! ``` //! //! ## Handling connection state changes @@ -216,6 +224,7 @@ pub use track_event::{RTCTrackEvent, RTCTrackEventInit}; /// See [RTCPeerConnection Events](https://www.w3.org/TR/webrtc/#rtcpeerconnection-interface) #[allow(clippy::enum_variant_names)] #[derive(Default, Clone, Debug)] +#[non_exhaustive] pub enum RTCPeerConnectionEvent { /// Fired when negotiation is needed to maintain the connection. /// @@ -512,6 +521,7 @@ pub enum RTCPeerConnectionEvent { /// /// This enum is currently empty but reserved for potential future event types. #[derive(Debug, Clone)] +#[non_exhaustive] pub enum RTCEvent {} /// Internal event types for WebRTC implementation. diff --git a/src/peer_connection/event/track_event.rs b/src/peer_connection/event/track_event.rs index 9729a0e4..7461ae66 100644 --- a/src/peer_connection/event/track_event.rs +++ b/src/peer_connection/event/track_event.rs @@ -22,30 +22,27 @@ use crate::rtp_transceiver::{RTCRtpReceiverId, RtpStreamId}; /// /// ## Accessing track components /// -/// ```ignore -/// // Note: Accessing receiver/transceiver requires &mut RTCPeerConnection +/// ``` +/// // Note: accessing the receiver requires `&mut RTCPeerConnection` /// use rtc::peer_connection::RTCPeerConnection; /// use rtc::peer_connection::event::{RTCPeerConnectionEvent, RTCTrackEvent}; /// -/// fn handle_event(mut peer_connection: RTCPeerConnection, event: RTCPeerConnectionEvent) { -/// match event { -/// RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(init)) => { -/// // Access the receiver -/// if let Some(receiver) = peer_connection.rtp_receiver(init.receiver_id) { -/// // Get receiver parameters -/// let params = receiver.get_parameters(); -/// println!("Codecs: {:?}", params.codecs); -/// } -/// -/// // Print associated stream IDs -/// println!("Stream IDs: {:?}", init.stream_ids); -/// -/// // Check if this is a simulcast stream -/// if let Some(rid) = &init.rid { -/// println!("Simulcast RID: {}", rid); -/// } +/// fn handle_event(peer_connection: &mut RTCPeerConnection, event: RTCPeerConnectionEvent) { +/// if let RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(init)) = event { +/// // Access the receiver +/// if let Some(mut receiver) = peer_connection.rtp_receiver(init.receiver_id) { +/// // Get receiver parameters +/// let params = receiver.get_parameters(); +/// println!("Codecs: {:?}", params.rtp_parameters.codecs); +/// } +/// +/// // Print associated stream IDs +/// println!("Stream IDs: {:?}", init.stream_ids); +/// +/// // Check if this is a simulcast stream +/// if let Some(rid) = &init.rid { +/// println!("Simulcast RID: {}", rid); /// } -/// _ => {} /// } /// } /// ``` @@ -129,30 +126,35 @@ pub struct RTCTrackEventInit { /// # } /// ``` /// -/// ## Reading media from track (conceptual) +/// ## Reading media from track /// -/// ```ignore -/// // Note: poll_read() is part of sans-I/O design +/// `poll_event()` and `poll_read()` come from the [`sansio::Protocol`] implementation on +/// [`RTCPeerConnection`](crate::peer_connection::RTCPeerConnection), so that trait must be +/// in scope. +/// +/// ``` /// use rtc::peer_connection::RTCPeerConnection; /// use rtc::peer_connection::event::{RTCPeerConnectionEvent, RTCTrackEvent}; /// use rtc::peer_connection::message::RTCMessage; +/// use rtc::sansio::Protocol; /// -/// fn handle_events(mut peer_connection: RTCPeerConnection) { +/// fn handle_events(peer_connection: &mut RTCPeerConnection) { /// // Poll events /// while let Some(event) = peer_connection.poll_event() { -/// if let RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(init)) = event { +/// if let RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(_init)) = event { /// println!("Track opened, ready to receive media"); /// } /// } /// -/// // Poll incoming media +/// // Poll incoming media. Both RTP and RTCP are tagged with the *track* id they +/// // belong to. /// while let Some(message) = peer_connection.poll_read() { /// match message { /// RTCMessage::RtpPacket(track_id, rtp) => { /// println!("RTP packet from track {:?}: {} bytes", track_id, rtp.payload.len()); /// } -/// RTCMessage::RtcpPacket(receiver_id, rtcp) => { -/// println!("RTCP packet: {:?}", rtcp); +/// RTCMessage::RtcpPacket(track_id, rtcp) => { +/// println!("RTCP packet for track {:?}: {:?}", track_id, rtcp); /// } /// _ => {} /// } @@ -165,6 +167,7 @@ pub struct RTCTrackEventInit { /// See [RTCTrackEvent](https://www.w3.org/TR/webrtc/#rtctrackevent) #[allow(clippy::enum_variant_names)] #[derive(Debug, Clone)] +#[non_exhaustive] pub enum RTCTrackEvent { /// Track has opened and is ready to receive media. /// diff --git a/src/peer_connection/handler/dtls.rs b/src/peer_connection/handler/dtls.rs index 7d4fbe63..555f6381 100644 --- a/src/peer_connection/handler/dtls.rs +++ b/src/peer_connection/handler/dtls.rs @@ -1,6 +1,5 @@ use crate::peer_connection::configuration::setting_engine::ReplayProtection; use crate::peer_connection::event::RTCEventInternal; -use crate::peer_connection::handler::DEFAULT_TIMEOUT_DURATION; use crate::peer_connection::message::internal::{ DTLSMessage, RTCMessageInternal, TaggedRTCMessageInternal, }; @@ -8,20 +7,20 @@ use crate::peer_connection::transport::dtls::RTCDtlsTransport; use crate::peer_connection::transport::dtls::role::RTCDtlsRole; use crate::peer_connection::transport::dtls::state::RTCDtlsTransportState; use crate::statistics::accumulator::{CertificateStatsAccumulator, RTCStatsAccumulator}; +use crypto::RTCCryptoProvider; use dtls::endpoint::EndpointEvent; use dtls::extension::extension_use_srtp::SrtpProtectionProfile; use dtls::state::State; use log::{debug, warn}; -use sha2::{Digest, Sha256}; use shared::TransportContext; use shared::error::{Error, Result}; use srtp::option::{srtcp_replay_protection, srtp_replay_protection}; use srtp::protection_profile::ProtectionProfile; use std::collections::VecDeque; use std::net::SocketAddr; +use std::sync::Arc; use std::time::Instant; -#[derive(Default)] pub(crate) struct DtlsHandlerContext { pub(crate) dtls_transport: RTCDtlsTransport, @@ -62,7 +61,7 @@ impl<'a> DtlsHandler<'a> { srtp_profile: SrtpProtectionProfile, peer_certificates: &[Vec], dtls_cipher: Option, - ) { + ) -> Result<()> { // Update transport DTLS state self.stats .transport @@ -91,7 +90,8 @@ impl<'a> DtlsHandler<'a> { // Register local certificate and set local_certificate_id if let Some(local_cert) = self.ctx.dtls_transport.certificates.first() { - let fingerprints = local_cert.get_fingerprints(); + let fingerprints = + local_cert.get_fingerprints(self.ctx.dtls_transport.crypto_provider.crypto())?; if let Some(fp) = fingerprints.first() { // Register certificate in accumulator // Use hex encoding for certificate (base64 would need additional dependency) @@ -115,9 +115,13 @@ impl<'a> DtlsHandler<'a> { // Register remote certificate and set remote_certificate_id if let Some(peer_cert_der) = peer_certificates.first() { // Compute fingerprint from peer certificate - let mut hasher = Sha256::new(); - hasher.update(peer_cert_der); - let hash = hasher.finalize(); + let hash = self + .ctx + .dtls_transport + .crypto_provider + .crypto() + .hash(crypto::HashAlgorithm::Sha256, peer_cert_der) + .map_err(|error| Error::Crypto(error.to_string()))?; let fingerprint: String = hash .iter() .map(|b| format!("{:02x}", b)) @@ -140,6 +144,7 @@ impl<'a> DtlsHandler<'a> { // Set remote certificate ID in transport stats self.stats.transport.remote_certificate_id = cert_id; } + Ok(()) } } @@ -195,6 +200,7 @@ impl<'a> sansio::Protocol sansio::Protocol {} } } @@ -238,7 +245,7 @@ impl<'a> sansio::Protocol sansio::Protocol Result<()> { - let dtls_endpoint = self - .ctx - .dtls_transport - .dtls_endpoint - .as_mut() - .ok_or(Error::ErrDtlsTransportNotStarted)?; + let Some(dtls_endpoint) = self.ctx.dtls_transport.dtls_endpoint.as_mut() else { + return Ok(()); + }; let remotes: Vec = dtls_endpoint.get_connections_keys().copied().collect(); for remote in remotes { - let _ = dtls_endpoint.handle_timeout(remote, now); + dtls_endpoint.handle_timeout(remote, now)?; } while let Some(transmit) = dtls_endpoint.poll_transmit() { self.ctx.write_outs.push_back(TaggedRTCMessageInternal { @@ -347,15 +351,16 @@ impl<'a> sansio::Protocol Option { if let Some(dtls_endpoint) = self.ctx.dtls_transport.dtls_endpoint.as_ref() { - let max_eto = Instant::now() + DEFAULT_TIMEOUT_DURATION; - let mut eto = max_eto; + let mut eto = None; let remotes = dtls_endpoint.get_connections_keys(); for remote in remotes { - let _ = dtls_endpoint.poll_timeout(*remote, &mut eto); + if let Some(timeout) = dtls_endpoint.poll_timeout(remote) { + eto = Some(eto.map_or(timeout, |e: Instant| e.min(timeout))); + } } - if eto != max_eto { Some(eto) } else { None } + eto } else { None } @@ -372,6 +377,7 @@ impl<'a> DtlsHandler<'a> { pub(crate) fn update_srtp_contexts( state: &State, replay_protection: &ReplayProtection, + crypto_provider: &Arc, ) -> Result<(srtp::context::Context, srtp::context::Context)> { let profile = match state.srtp_protection_profile() { SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm => ProtectionProfile::AeadAes128Gcm, @@ -400,7 +406,13 @@ impl<'a> DtlsHandler<'a> { )); } - srtp_config.extract_session_keys_from_dtls(state, state.is_client())?; + let keying_material = state.export_keying_material( + srtp::config::LABEL_EXTRACTOR_DTLS_SRTP, + &[], + srtp_config.keying_material_len(), + )?; + srtp_config + .set_session_keys_from_keying_material(keying_material.as_ref(), state.is_client())?; let local_context = srtp::context::Context::new( &srtp_config.keys.local_master_key, @@ -408,6 +420,7 @@ impl<'a> DtlsHandler<'a> { srtp_config.profile, srtp_config.local_rtp_options, srtp_config.local_rtcp_options, + crypto_provider.crypto(), )?; let remote_context = srtp::context::Context::new( @@ -428,8 +441,38 @@ impl<'a> DtlsHandler<'a> { } else { srtp_config.remote_rtcp_options }, + crypto_provider.crypto(), )?; Ok((local_context, remote_context)) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn timeout_before_dtls_starts_is_a_noop() { + let provider = + crypto::default_provider().expect("a built-in crypto provider is enabled for tests"); + let transport = RTCDtlsTransport::new( + crate::peer_connection::transport::dtls::RTCDtlsTransportConfig { + certificates: vec![], + answering_dtls_role: Default::default(), + srtp_protection_profiles: vec![], + dtls_cipher_suites: vec![], + allow_insecure_verification_algorithm: false, + disable_certificate_fingerprint_verification: false, + replay_protection: Default::default(), + crypto_provider: provider, + }, + ) + .expect("transport"); + let mut context = DtlsHandlerContext::new(transport); + let mut stats = RTCStatsAccumulator::default(); + let mut handler = DtlsHandler::new(&mut context, &mut stats); + + sansio::Protocol::handle_timeout(&mut handler, Instant::now()).unwrap(); + } +} diff --git a/src/peer_connection/handler/ice.rs b/src/peer_connection/handler/ice.rs index 3fd35db7..a2c931e9 100644 --- a/src/peer_connection/handler/ice.rs +++ b/src/peer_connection/handler/ice.rs @@ -12,7 +12,6 @@ use shared::{TransportContext, TransportMessage}; use std::collections::VecDeque; use std::time::Instant; -#[derive(Default)] pub(crate) struct IceHandlerContext { pub(crate) ice_transport: RTCIceTransport, @@ -121,7 +120,10 @@ impl<'a> sansio::Protocol sansio::Protocol { self.stats.transport.on_ice_role_changed(is_controlling); } + _ => {} } } diff --git a/src/peer_connection/handler/interceptor.rs b/src/peer_connection/handler/interceptor.rs index 85f09b5e..69e5f62e 100644 --- a/src/peer_connection/handler/interceptor.rs +++ b/src/peer_connection/handler/interceptor.rs @@ -260,6 +260,7 @@ where ); } } + _ => {} } self.ctx.write_outs.push_back(TaggedRTCMessageInternal { diff --git a/src/peer_connection/handler/mod.rs b/src/peer_connection/handler/mod.rs index f212fca0..6e517f47 100644 --- a/src/peer_connection/handler/mod.rs +++ b/src/peer_connection/handler/mod.rs @@ -34,9 +34,7 @@ use log::warn; use shared::TaggedBytesMut; use shared::error::{Error, flatten_errs}; use std::collections::VecDeque; -use std::time::{Duration, Instant}; - -pub(crate) const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(86400); // 1 day duration +use std::time::Instant; /// Forward handler list - invokes callback with handler list macro_rules! forward_handlers { @@ -101,7 +99,6 @@ macro_rules! for_each_handler { }; } -#[derive(Default)] pub(crate) struct PipelineContext { // Handler contexts pub(crate) demuxer_handler_context: DemuxerHandlerContext, @@ -242,6 +239,7 @@ where Packet::Rtcp(packet) => { Some(RTCMessage::RtcpPacket(track_packet.track_id, packet)) } + _ => None, } } _ => None, diff --git a/src/peer_connection/handler/sctp.rs b/src/peer_connection/handler/sctp.rs index b6db0866..ba02ebd7 100644 --- a/src/peer_connection/handler/sctp.rs +++ b/src/peer_connection/handler/sctp.rs @@ -1,7 +1,6 @@ use crate::peer_connection::event::RTCEventInternal; use crate::peer_connection::event::RTCPeerConnectionEvent; use crate::peer_connection::event::data_channel_event::RTCDataChannelEvent; -use crate::peer_connection::handler::DEFAULT_TIMEOUT_DURATION; use crate::peer_connection::message::internal::{ DTLSMessage, RTCMessageInternal, TaggedRTCMessageInternal, }; @@ -72,7 +71,7 @@ impl<'a> SctpHandler<'a> { /// `flush_dirty` is set, after a burst of inbound packets has been ingested, so /// their SACKs coalesce into a single datagram. fn flush_transmits(&mut self, now: Instant) { - for (_ch, conn) in self.ctx.sctp_transport.sctp_associations.iter_mut() { + for conn in self.ctx.sctp_transport.sctp_associations.values_mut() { while let Some(x) = conn.poll_transmit(now) { for transmit in split_transmit(x) { if let Payload::RawEncode(raw_data) = transmit.message { @@ -143,6 +142,7 @@ impl<'a> sansio::Protocol { sctp_events.entry(ch).or_default().push_back(event); } + _ => {} } } @@ -543,18 +543,15 @@ impl<'a> sansio::Protocol Option { - let max_eto = Instant::now() + DEFAULT_TIMEOUT_DURATION; - let mut eto = max_eto; + let mut eto = None; for conn in self.ctx.sctp_transport.sctp_associations.values() { - if let Some(timeout) = conn.poll_timeout() - && timeout < eto - { - eto = timeout; + if let Some(timeout) = conn.poll_timeout() { + eto = Some(eto.map_or(timeout, |e: Instant| e.min(timeout))); } } - if eto != max_eto { Some(eto) } else { None } + eto } fn close(&mut self) -> Result<()> { @@ -670,6 +667,7 @@ mod tests { sc.handle_event(e); } } + _ => {} } } } @@ -774,6 +772,7 @@ mod tests { c.handle_event(e); } } + _ => {} } } } @@ -789,6 +788,7 @@ mod tests { c.handle_event(e); } } + _ => {} } } } diff --git a/src/peer_connection/handler/srtp.rs b/src/peer_connection/handler/srtp.rs index 2fe88b7b..6dcace5b 100644 --- a/src/peer_connection/handler/srtp.rs +++ b/src/peer_connection/handler/srtp.rs @@ -143,6 +143,11 @@ impl<'a> sansio::Protocol { + return Err(Error::Other( + "unsupported packet kind for srtp write".to_string(), + )); + } }; self.ctx.write_outs.push_back(TaggedRTCMessageInternal { diff --git a/src/peer_connection/internal.rs b/src/peer_connection/internal.rs index 274051b8..bb75fa67 100644 --- a/src/peer_connection/internal.rs +++ b/src/peer_connection/internal.rs @@ -1,5 +1,10 @@ use super::*; use crate::peer_connection::event::{RTCPeerConnectionEvent, RTCPeerConnectionIceEvent}; +use crate::peer_connection::handler::datachannel::DataChannelHandlerContext; +use crate::peer_connection::handler::demuxer::DemuxerHandlerContext; +use crate::peer_connection::handler::endpoint::EndpointHandlerContext; +use crate::peer_connection::handler::interceptor::InterceptorHandlerContext; +use crate::peer_connection::handler::srtp::SrtpHandlerContext; use crate::peer_connection::sdp::{ MediaSection, PopulateSdpParams, add_candidates_to_media_descriptions, get_by_mid, get_peer_direction, get_rids, have_data_channel, is_ext_map_allow_mixed_set, @@ -21,6 +26,7 @@ use crate::statistics::accumulator::IceCandidateAccumulator; use ::sdp::description::session::*; use ::sdp::util::ConnectionRole; use std::collections::HashSet; +use std::collections::VecDeque; impl RTCPeerConnection where @@ -29,11 +35,29 @@ where pub(super) fn new( mut configuration: RTCConfiguration, media_engine: MediaEngine, - setting_engine: SettingEngine, + mut setting_engine: SettingEngine, interceptor: I, ) -> Result { configuration.validate()?; + // The one place in `rtc` that resolves a default crypto provider. The application + // either supplies one through `SettingEngine::set_crypto_provider` — which is also how + // a wrapper crate injects a provider it resolved itself — or gets the feature-selected + // built-in here, once, at construction. Everything downstream — + // ICE, DTLS, SRTP, STUN, certificates — receives it explicitly, so no library code + // reaches for a default behind the caller's back. + let crypto_provider = match setting_engine.crypto_provider.take() { + Some(crypto_provider) => crypto_provider, + None => crypto::default_provider().map_err(|error| { + Error::Crypto(format!( + "peer connection requires a crypto provider: {error}; configure one with SettingEngine::set_crypto_provider" + )) + })?, + }; + // Record the resolution so `SettingEngine::crypto_provider` reports the provider this + // connection actually uses, not merely what was requested. + setting_engine.crypto_provider = Some(crypto_provider.clone()); + let mut candidate_types = vec![]; if setting_engine.candidates.ice_lite { candidate_types.push(ice::candidate::CandidateType::Host); @@ -83,17 +107,22 @@ where }; // Create the ICE transport - let ice_transport = RTCIceTransport::new(agent_config)?; + let ice_transport = RTCIceTransport::new(agent_config, crypto_provider.clone())?; // Create the DTLS transport let certificates = configuration.certificates.drain(..).collect(); - let dtls_transport = RTCDtlsTransport::new( + let dtls_transport = RTCDtlsTransport::new(RTCDtlsTransportConfig { certificates, - setting_engine.answering_dtls_role, - setting_engine.srtp_protection_profiles.clone(), - setting_engine.allow_insecure_verification_algorithm, - setting_engine.replay_protection, - )?; + answering_dtls_role: setting_engine.answering_dtls_role, + srtp_protection_profiles: setting_engine.srtp_protection_profiles.clone(), + dtls_cipher_suites: setting_engine.dtls_cipher_suites.clone(), + allow_insecure_verification_algorithm: setting_engine + .allow_insecure_verification_algorithm, + disable_certificate_fingerprint_verification: setting_engine + .disable_certificate_fingerprint_verification, + replay_protection: setting_engine.replay_protection, + crypto_provider, + })?; // Create the SCTP transport let sctp_transport = RTCSctpTransport::new( @@ -106,12 +135,22 @@ where let dtls_handler_context = DtlsHandlerContext::new(dtls_transport); let sctp_handler_context = SctpHandlerContext::new(sctp_transport); + // Listed in full rather than filled from `Default`: the ICE and DTLS handler contexts + // own a crypto provider, and deriving `Default` for them would mean resolving one + // implicitly. The provider is chosen once above and threaded from here. let pipeline_context = PipelineContext { + demuxer_handler_context: DemuxerHandlerContext::default(), ice_handler_context, dtls_handler_context, sctp_handler_context, - - ..Default::default() + datachannel_handler_context: DataChannelHandlerContext::default(), + srtp_handler_context: SrtpHandlerContext::default(), + interceptor_handler_context: InterceptorHandlerContext::default(), + endpoint_handler_context: EndpointHandlerContext::default(), + read_outs: VecDeque::new(), + write_outs: VecDeque::new(), + event_outs: VecDeque::new(), + stats: RTCStatsAccumulator::default(), }; Ok(Self { @@ -175,7 +214,7 @@ where } let dtls_fingerprints = if let Some(cert) = self.dtls_transport().certificates.first() { - cert.get_fingerprints() + cert.get_fingerprints(self.dtls_transport().crypto_provider.crypto())? } else { return Err(Error::ErrNonCertificate); }; @@ -320,7 +359,7 @@ where }; let dtls_fingerprints = if let Some(cert) = self.dtls_transport().certificates.first() { - cert.get_fingerprints() + cert.get_fingerprints(self.dtls_transport().crypto_provider.crypto())? } else { return Err(Error::ErrNonCertificate); }; diff --git a/src/peer_connection/message/internal.rs b/src/peer_connection/message/internal.rs index d8eee476..7aad2f16 100644 --- a/src/peer_connection/message/internal.rs +++ b/src/peer_connection/message/internal.rs @@ -81,6 +81,7 @@ impl RTCMessageInternal { } rtcp_packet_size } + _ => 0, // Future Packet variants: size unknown; treat as 0 for accounting. }, RTPMessage::TrackPacket(tp) => match &tp.packet { Packet::Rtp(rtp) => rtp.marshal_size(), @@ -91,6 +92,7 @@ impl RTCMessageInternal { } rtcp_packet_size } + _ => 0, }, }, } diff --git a/src/peer_connection/message/mod.rs b/src/peer_connection/message/mod.rs index 44e721bc..62f4c7bc 100644 --- a/src/peer_connection/message/mod.rs +++ b/src/peer_connection/message/mod.rs @@ -87,6 +87,8 @@ //! println!("Binary message"); //! } //! } +//! // RTCMessage is #[non_exhaustive]: a wildcard arm is required. +//! _ => {} //! } //! # } //! ``` @@ -329,6 +331,8 @@ pub(crate) mod internal; /// println!("Received data on channel {}", channel_id); /// // Process application data /// } +/// // RTCMessage is #[non_exhaustive]: a wildcard arm is required. +/// _ => {} /// } /// # } /// ``` @@ -381,6 +385,8 @@ pub(crate) mod internal; /// println!("Binary: {} bytes", msg.data.len()); /// } /// } +/// // RTCMessage is #[non_exhaustive]: a wildcard arm is required. +/// _ => {} /// } /// # } /// ``` @@ -409,6 +415,7 @@ pub(crate) mod internal; /// [RFC 3711]: https://datatracker.ietf.org/doc/html/rfc3711 /// [RFC 8831]: https://datatracker.ietf.org/doc/html/rfc8831 #[derive(Debug, Clone)] +#[non_exhaustive] pub enum RTCMessage { /// RTP packet for a specific media track. /// diff --git a/src/peer_connection/mod.rs b/src/peer_connection/mod.rs index 7a8a811b..4c3c0ee1 100644 --- a/src/peer_connection/mod.rs +++ b/src/peer_connection/mod.rs @@ -275,12 +275,12 @@ use crate::peer_connection::state::peer_connection_state::{ NegotiationNeededState, RTCPeerConnectionState, }; use crate::peer_connection::state::signaling_state::{RTCSignalingState, StateChangeOp}; -use crate::peer_connection::transport::dtls::RTCDtlsTransport; use crate::peer_connection::transport::dtls::fingerprint::RTCDtlsFingerprint; use crate::peer_connection::transport::dtls::parameters::RTCDtlsParameters; use crate::peer_connection::transport::dtls::role::{ DEFAULT_DTLS_ROLE_ANSWER, DEFAULT_DTLS_ROLE_OFFER, RTCDtlsRole, }; +use crate::peer_connection::transport::dtls::{RTCDtlsTransport, RTCDtlsTransportConfig}; use crate::peer_connection::transport::ice::RTCIceTransport; use crate::peer_connection::transport::ice::candidate::RTCIceCandidateInit; use crate::peer_connection::transport::ice::parameters::RTCIceParameters; @@ -530,18 +530,6 @@ where self } - /// Overrides the SCTP receive-buffer size (the a_rwnd flow-control window), in bytes, - /// on this builder's [`SettingEngine`]. - /// - /// Convenience for [`SettingEngine::set_sctp_max_receive_buffer_size`]; see it for the - /// throughput/memory tradeoff. Applies to whichever `SettingEngine` is currently set, - /// so call it after [`with_setting_engine`](Self::with_setting_engine) if you also - /// supply a custom engine. Leaving it unset keeps the 1 MiB default. - pub fn with_sctp_receive_buffer_size(mut self, size: u32) -> Self { - self.setting_engine.set_sctp_max_receive_buffer_size(size); - self - } - /// Configures the peer connection with an interceptor registry. /// /// Interceptors process RTP/RTCP packets as they flow through the pipeline, @@ -550,9 +538,11 @@ where /// - TWCC (Transport-Wide Congestion Control) for bandwidth estimation /// - RTCP Reports for quality statistics /// - /// This method changes the interceptor type from `NoopInterceptor` to the - /// registry's interceptor type. It must be the last builder method called - /// before `build()` when used. + /// This method replaces the builder's interceptor type — `NoopInterceptor` by default — + /// with the registry's, so it returns a `RTCPeerConnectionBuilder

` rather than + /// `Self`. Every other builder setting is carried over, and the remaining setters are + /// available on the returned builder, so this does not have to be the last call before + /// `build()`; it is simply the only one that changes the builder's type. /// /// # Type Parameters /// @@ -578,6 +568,47 @@ where /// # Ok(()) /// # } /// ``` + /// + /// # Type-erasing the chain + /// + /// A chain's type spells out its whole composition + /// (`TwccReceiverInterceptor>`), and it propagates into + /// every type that holds the peer connection. [`Registry::boxed`] erases it to + /// [`BoxedInterceptor`], so the connection has one concrete type — + /// `RTCPeerConnection` — whatever chain it was built from. That is + /// what lets a non-generic struct own one, or two connections with *different* chains + /// share a collection: + /// + /// ``` + /// use rtc::interceptor::{BoxedInterceptor, Registry}; + /// use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors; + /// use rtc::peer_connection::configuration::media_engine::MediaEngine; + /// use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; + /// + /// struct Session { + /// peer_connection: RTCPeerConnection, // no type parameter + /// } + /// + /// # fn example() -> Result<(), Box> { + /// let mut media_engine = MediaEngine::default(); + /// let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; + /// + /// let session = Session { + /// peer_connection: RTCPeerConnectionBuilder::new() + /// .with_media_engine(media_engine) + /// .with_interceptor_registry(registry.boxed()) + /// .build()?, + /// }; + /// # Ok(()) + /// # } + /// ``` + /// + /// The cost is one virtual call per chain entry point; the chain's interior stays + /// statically dispatched. Keep the generic form when the chain is fixed at compile + /// time. + /// + /// [`BoxedInterceptor`]: crate::interceptor::BoxedInterceptor + /// [`Registry::boxed`]: crate::interceptor::Registry::boxed pub fn with_interceptor_registry

( self, interceptor_registry: Registry

, @@ -1812,38 +1843,40 @@ where let mut id = self.generate_data_channel_id()?; - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #19) - if let Some(options) = options { - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #16) - if options.max_packet_life_time.is_some() && options.max_retransmits.is_some() { - return Err(Error::ErrRetransmitsOrPacketLifeTime); - } + // `None` means "the dictionary defaults", which is what `RTCDataChannelInit::default()` + // spells out. Taking that route rather than leaving `params` on its derived default + // keeps a single definition of those defaults — notably `ordered`, which is `true`. + let options = options.unwrap_or_default(); - // Ordered indicates if data is allowed to be delivered out of order. The - // default value of true, guarantees that data will be delivered in order. - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #9) - params.ordered = options.ordered; + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #16) + if options.max_packet_life_time.is_some() && options.max_retransmits.is_some() { + return Err(Error::ErrRetransmitsOrPacketLifeTime); + } - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #7) - params.max_packet_life_time = options.max_packet_life_time; + // Ordered indicates if data is allowed to be delivered out of order. The + // default value of true, guarantees that data will be delivered in order. + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #9) + params.ordered = options.ordered; - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #8) - params.max_retransmits = options.max_retransmits; + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #7) + params.max_packet_life_time = options.max_packet_life_time; - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #10) - params.protocol = options.protocol; + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #8) + params.max_retransmits = options.max_retransmits; - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #11) - if params.protocol.len() > 65535 { - return Err(Error::ErrProtocolTooLarge); - } + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #10) + params.protocol = options.protocol; + + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #11) + if params.protocol.len() > 65535 { + return Err(Error::ErrProtocolTooLarge); + } - // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #12) - params.negotiated = options.negotiated; + // https://w3c.github.io/webrtc-pc/#peer-to-peer-data-api (Step #12) + params.negotiated = options.negotiated; - if let Some(negotiated_id) = ¶ms.negotiated { - id = *negotiated_id; - } + if let Some(negotiated_id) = ¶ms.negotiated { + id = *negotiated_id; } let mut data_channel = RTCDataChannelInternal::new(id, params); @@ -2289,7 +2322,10 @@ mod tests { #[test] fn with_sctp_receive_buffer_size_sets_and_clamps() { - let builder = RTCPeerConnectionBuilder::new().with_sctp_receive_buffer_size(200_000); + let mut setting_engine = SettingEngine::default(); + setting_engine.set_sctp_max_receive_buffer_size(200_000); + + let builder = RTCPeerConnectionBuilder::new().with_setting_engine(setting_engine); assert_eq!( builder.setting_engine.sctp_max_receive_buffer_size, Some(200_000) @@ -2298,7 +2334,9 @@ mod tests { // Values below the RFC 4960 §6 floor (1500 bytes), including 0, are clamped up so // they cannot break the SCTP handshake. for input in [0u32, 500, 1499] { - let builder = RTCPeerConnectionBuilder::new().with_sctp_receive_buffer_size(input); + let mut setting_engine = SettingEngine::default(); + setting_engine.set_sctp_max_receive_buffer_size(input); + let builder = RTCPeerConnectionBuilder::new().with_setting_engine(setting_engine); assert_eq!( builder.setting_engine.sctp_max_receive_buffer_size, Some(1500), diff --git a/src/peer_connection/sdp/sdp_type.rs b/src/peer_connection/sdp/sdp_type.rs index 1c7e71e1..f0e2bcd4 100644 --- a/src/peer_connection/sdp/sdp_type.rs +++ b/src/peer_connection/sdp/sdp_type.rs @@ -52,6 +52,8 @@ use serde::{Deserialize, Serialize}; /// RTCSdpType::Pranswer => println!("This is a provisional answer"), /// RTCSdpType::Rollback => println!("This is a rollback"), /// RTCSdpType::Unspecified => println!("Type not specified"), +/// // RTCSdpType is #[non_exhaustive]: a wildcard arm is required. +/// _ => {} /// } /// # Ok(()) /// # } @@ -85,6 +87,7 @@ use serde::{Deserialize, Serialize}; /// [MDN RTCSessionDescription.type]: https://developer.mozilla.org/en-US/docs/Web/API/RTCSessionDescription/type /// [RFC 3264]: https://datatracker.ietf.org/doc/html/rfc3264 #[derive(Default, Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCSdpType { /// Type not specified. This is the default value and should not be used /// in actual WebRTC negotiation. diff --git a/src/peer_connection/state/ice_connection_state.rs b/src/peer_connection/state/ice_connection_state.rs index a702e58e..d075fda6 100644 --- a/src/peer_connection/state/ice_connection_state.rs +++ b/src/peer_connection/state/ice_connection_state.rs @@ -105,6 +105,7 @@ use std::fmt; /// [MDN RTCPeerConnection.iceConnectionState]: https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/iceConnectionState /// [RFC 8445]: https://datatracker.ietf.org/doc/html/rfc8445 #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum RTCIceConnectionState { /// State not specified. This should not occur in normal operation. #[default] diff --git a/src/peer_connection/state/ice_gathering_state.rs b/src/peer_connection/state/ice_gathering_state.rs index 831ff874..a33e184f 100644 --- a/src/peer_connection/state/ice_gathering_state.rs +++ b/src/peer_connection/state/ice_gathering_state.rs @@ -115,6 +115,7 @@ use std::fmt; /// [RFC 8445]: https://datatracker.ietf.org/doc/html/rfc8445 /// [RFC 8838]: https://datatracker.ietf.org/doc/html/rfc8838 #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum RTCIceGatheringState { /// State not specified. This should not occur in normal operation. #[default] diff --git a/src/peer_connection/state/peer_connection_state.rs b/src/peer_connection/state/peer_connection_state.rs index 8cc9fd84..bcbf8cf8 100644 --- a/src/peer_connection/state/peer_connection_state.rs +++ b/src/peer_connection/state/peer_connection_state.rs @@ -142,6 +142,7 @@ use std::fmt; /// [W3C RTCPeerConnection.connectionState]: https://w3c.github.io/webrtc-pc/#dom-peerconnection-connection-state /// [MDN RTCPeerConnection.connectionState]: https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/connectionState #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum RTCPeerConnectionState { /// State not specified. This should not occur in normal operation. #[default] diff --git a/src/peer_connection/state/signaling_state.rs b/src/peer_connection/state/signaling_state.rs index 3f75ee1e..e1c85bde 100644 --- a/src/peer_connection/state/signaling_state.rs +++ b/src/peer_connection/state/signaling_state.rs @@ -184,6 +184,7 @@ impl fmt::Display for StateChangeOp { /// [MDN RTCPeerConnection.signalingState]: https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/signalingState /// [RFC 3264]: https://datatracker.ietf.org/doc/html/rfc3264 #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum RTCSignalingState { /// State not specified. This should not occur in normal operation. Unspecified = 0, diff --git a/src/peer_connection/transport/dtls/mod.rs b/src/peer_connection/transport/dtls/mod.rs index 0831ca1e..0315c13b 100644 --- a/src/peer_connection/transport/dtls/mod.rs +++ b/src/peer_connection/transport/dtls/mod.rs @@ -4,11 +4,12 @@ use crate::peer_connection::transport::dtls::parameters::RTCDtlsParameters; use crate::peer_connection::transport::dtls::role::{DEFAULT_DTLS_ROLE_ANSWER, RTCDtlsRole}; use crate::peer_connection::transport::dtls::state::RTCDtlsTransportState; use crate::peer_connection::transport::ice::role::RTCIceRole; +use crypto::{HashAlgorithm, RTCCryptoProvider}; +use dtls::cipher_suite::CipherSuiteId; use dtls::config::{ClientAuthType, VerifyPeerCertificateFn}; use dtls::extension::extension_use_srtp::SrtpProtectionProfile; -use rcgen::KeyPair; +use rcgen::CertificateParams; use rustls::pki_types::CertificateDer; -use sha2::{Digest, Sha256}; use shared::error::{Error, Result}; use shared::{TransportContext, TransportProtocol}; use std::sync::Arc; @@ -32,8 +33,8 @@ pub(crate) fn default_srtp_protection_profiles() -> Vec { /// transport over which RTP and RTCP packets are sent and received by /// RTPSender and RTPReceiver, as well other data such as SCTP packets sent /// and received by data channels. -#[derive(Default)] pub(crate) struct RTCDtlsTransport { + pub(crate) crypto_provider: Arc, pub(crate) dtls_role: RTCDtlsRole, pub(crate) dtls_handshake_config: Option>, pub(crate) dtls_endpoint: Option<::dtls::endpoint::Endpoint>, @@ -44,18 +45,37 @@ pub(crate) struct RTCDtlsTransport { // From SettingEngine pub(crate) answering_dtls_role: RTCDtlsRole, pub(crate) srtp_protection_profiles: Vec, + /// Empty means "use the `dtls` crate's default set" (see + /// [`SettingEngine::set_dtls_cipher_suites`](crate::peer_connection::configuration::setting_engine::SettingEngine::set_dtls_cipher_suites)). + pub(crate) dtls_cipher_suites: Vec, pub(crate) allow_insecure_verification_algorithm: bool, + pub(crate) disable_certificate_fingerprint_verification: bool, pub(crate) replay_protection: ReplayProtection, } +pub(crate) struct RTCDtlsTransportConfig { + pub(crate) certificates: Vec, + pub(crate) answering_dtls_role: RTCDtlsRole, + pub(crate) srtp_protection_profiles: Vec, + pub(crate) dtls_cipher_suites: Vec, + pub(crate) allow_insecure_verification_algorithm: bool, + pub(crate) disable_certificate_fingerprint_verification: bool, + pub(crate) replay_protection: ReplayProtection, + pub(crate) crypto_provider: Arc, +} + impl RTCDtlsTransport { - pub(crate) fn new( - mut certificates: Vec, - answering_dtls_role: RTCDtlsRole, - srtp_protection_profiles: Vec, - allow_insecure_verification_algorithm: bool, - replay_protection: ReplayProtection, - ) -> Result { + pub(crate) fn new(config: RTCDtlsTransportConfig) -> Result { + let RTCDtlsTransportConfig { + mut certificates, + answering_dtls_role, + srtp_protection_profiles, + dtls_cipher_suites, + allow_insecure_verification_algorithm, + disable_certificate_fingerprint_verification, + replay_protection, + crypto_provider, + } = config; if !certificates.is_empty() { let now = SystemTime::now(); for cert in &certificates { @@ -64,8 +84,13 @@ impl RTCDtlsTransport { .map_err(|_| Error::ErrCertificateExpired)?; } } else { - let kp = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?; - let cert = RTCCertificate::from_key_pair(kp)?; + let params = CertificateParams::new(vec![shared::util::math_rand_alpha(16)]) + .map_err(|error| Error::Other(error.to_string()))?; + let cert = RTCCertificate::generate( + crypto_provider.crypto(), + crypto::SignatureScheme::EcdsaP256Sha256, + params, + )?; certificates = vec![cert]; }; @@ -78,8 +103,11 @@ impl RTCDtlsTransport { answering_dtls_role, srtp_protection_profiles, + dtls_cipher_suites, allow_insecure_verification_algorithm, + disable_certificate_fingerprint_verification, replay_protection, + crypto_provider, }) } @@ -119,41 +147,52 @@ impl RTCDtlsTransport { return Err(Error::ErrInvalidDTLSStart); } - // The `webrtc` interop tests uses `ring` for its crypto provider, - // so we need to avoid conflicts with `aws-lc-rs`. - // We need a feature gate because - // `#[cfg(test)]` is not propagated to this crate in integration tests. - #[cfg(all(any(feature = "__testing", test), feature = "aws-lc-rs"))] - let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); - self.dtls_role = self.derive_role(ice_role, remote_dtls_parameters.role); let remote_fingerprints = remote_dtls_parameters.fingerprints; - let verify_peer_certificate: VerifyPeerCertificateFn = Arc::new( - move |certs: &[Vec], _chains: &[CertificateDer<'static>]| -> Result<()> { - if certs.is_empty() { - return Err(Error::ErrNonCertificate); - } - - for fp in &remote_fingerprints { - if fp.algorithm != "sha-256" { - return Err(Error::ErrUnsupportedFingerprintAlgorithm); - } - - let mut h = Sha256::new(); - h.update(&certs[0]); - let hashed = h.finalize(); - let values: Vec = hashed.iter().map(|x| format! {"{x:02x}"}).collect(); - let remote_value = values.join(":").to_lowercase(); - - if remote_value == fp.value.to_lowercase() { - return Ok(()); - } - } - - Err(Error::ErrNoMatchingCertificateFingerprint) - }, - ); + // Leaving the callback out is what disables the check: `insecure_skip_verify` is + // already true, so this comparison is the only thing standing between the peer's + // certificate and acceptance. Dropping it does not accept a peer that presents no + // certificate at all — `client_auth` is `RequireAnyClientCert`, which the DTLS layer + // enforces on its own. + // + // Protocols where the answerer cannot know the offerer's fingerprint ahead of time + // need this. libp2p's WebRTC-Direct is the canonical case: the server synthesizes the + // client's offer locally with a placeholder fingerprint and authenticates the peer + // afterwards with a Noise handshake over the data channel. + let verify_peer_certificate: Option = + if !self.disable_certificate_fingerprint_verification { + let fingerprint_crypto = self.crypto_provider.clone(); + Some(Arc::new( + move |certs: &[Vec], _chains: &[CertificateDer<'static>]| -> Result<()> { + if certs.is_empty() { + return Err(Error::ErrNonCertificate); + } + + for fp in &remote_fingerprints { + if fp.algorithm != "sha-256" { + return Err(Error::ErrUnsupportedFingerprintAlgorithm); + } + + let hashed = fingerprint_crypto + .crypto() + .hash(HashAlgorithm::Sha256, &certs[0]) + .map_err(|error| Error::Crypto(error.to_string()))?; + let values: Vec = + hashed.iter().map(|x| format! {"{x:02x}"}).collect(); + let remote_value = values.join(":").to_lowercase(); + + if remote_value == fp.value.to_lowercase() { + return Ok(()); + } + } + + Err(Error::ErrNoMatchingCertificateFingerprint) + }, + )) + } else { + None + }; let certificate = if let Some(cert) = self.certificates.first() { cert.dtls_certificate.clone() @@ -162,24 +201,50 @@ impl RTCDtlsTransport { }; self.state_change(RTCDtlsTransportState::Connecting); + let profiles = self.supported_srtp_protection_profiles()?; + Ok(Arc::new( ::dtls::config::ConfigBuilder::default() + .with_crypto_provider(self.crypto_provider.clone()) .with_certificates(vec![certificate]) - .with_srtp_protection_profiles(if !self.srtp_protection_profiles.is_empty() { - self.srtp_protection_profiles.clone() - } else { - default_srtp_protection_profiles() - }) + .with_srtp_protection_profiles(profiles) + // Empty leaves `dtls`'s default set in place; a non-empty list replaces it. + .with_cipher_suites(self.dtls_cipher_suites.clone()) .with_client_auth(ClientAuthType::RequireAnyClientCert) .with_insecure_skip_verify(true) .with_insecure_verification(self.allow_insecure_verification_algorithm) - .with_verify_peer_certificate(Some(verify_peer_certificate)) + .with_verify_peer_certificate(verify_peer_certificate) .with_extended_master_secret(::dtls::config::ExtendedMasterSecretType::Require) .with_replay_protection_window(self.replay_protection.dtls) .build(self.dtls_role == RTCDtlsRole::Client, None)?, )) } + fn supported_srtp_protection_profiles(&self) -> Result> { + let configured = if self.srtp_protection_profiles.is_empty() { + default_srtp_protection_profiles() + } else { + self.srtp_protection_profiles.clone() + }; + let supported: Vec<_> = configured + .into_iter() + .filter(|profile| { + srtp_profile(*profile).is_some_and(|profile| { + profile + .ensure_crypto_supported(self.crypto_provider.crypto()) + .is_ok() + }) + }) + .collect(); + if supported.is_empty() { + return Err(Error::Crypto(format!( + "crypto provider {} supports none of the configured SRTP protection profiles", + self.crypto_provider.name() + ))); + } + Ok(supported) + } + pub(crate) fn role(&self) -> RTCDtlsRole { self.dtls_role } @@ -215,3 +280,163 @@ impl RTCDtlsTransport { Ok(()) } } + +fn srtp_profile( + profile: SrtpProtectionProfile, +) -> Option { + use srtp::protection_profile::ProtectionProfile; + match profile { + SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm => Some(ProtectionProfile::AeadAes128Gcm), + SrtpProtectionProfile::Srtp_Aead_Aes_256_Gcm => Some(ProtectionProfile::AeadAes256Gcm), + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80 => { + Some(ProtectionProfile::Aes128CmHmacSha1_80) + } + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_32 => { + Some(ProtectionProfile::Aes128CmHmacSha1_32) + } + _ => None, + } +} + +#[cfg(test)] +mod tests { + //! Cipher-suite plumbing for issue #808. + //! + //! `HandshakeConfig::local_cipher_suites` is `pub(crate)` to `rtc-dtls`, so these assert + //! the setting reached the config *behaviourally*: a list that cannot be satisfied is + //! rejected, which can only happen if it was applied rather than ignored. + + use super::*; + use crate::peer_connection::configuration::setting_engine::ReplayProtection; + use crypto::{AeadAlgorithm, CryptoAlgorithm, RTCCrypto, RTCRandom}; + + struct MissingAes128Gcm; + + impl RTCCrypto for MissingAes128Gcm { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + algorithm != CryptoAlgorithm::Aead(AeadAlgorithm::Aes128Gcm) + } + } + + struct ProfileFilteringProvider { + random_provider: Arc, + crypto: MissingAes128Gcm, + } + + impl RTCCryptoProvider for ProfileFilteringProvider { + fn name(&self) -> &'static str { + "missing-aes-128-gcm" + } + + fn crypto(&self) -> &dyn RTCCrypto { + &self.crypto + } + + fn random(&self) -> &dyn RTCRandom { + self.random_provider.random() + } + } + + fn transport(dtls_cipher_suites: Vec) -> RTCDtlsTransport { + RTCDtlsTransport::new(RTCDtlsTransportConfig { + certificates: vec![], + answering_dtls_role: DEFAULT_DTLS_ROLE_ANSWER, + srtp_protection_profiles: vec![], + dtls_cipher_suites, + allow_insecure_verification_algorithm: false, + disable_certificate_fingerprint_verification: false, + replay_protection: ReplayProtection::default(), + crypto_provider: crypto::default_provider().expect("test crypto provider"), + }) + .expect("a self-signed ECDSA certificate is generated when none is supplied") + } + + fn remote_params() -> RTCDtlsParameters { + RTCDtlsParameters { + role: RTCDtlsRole::Client, + fingerprints: vec![], + } + } + + #[test] + fn empty_cipher_suites_keeps_the_dtls_defaults() { + // The pre-#808 behaviour, and what every existing caller gets. + assert!( + transport(vec![]) + .prepare_transport(RTCIceRole::Controlling, remote_params()) + .is_ok() + ); + } + + #[test] + fn ecdsa_only_cipher_suites_are_accepted() { + // The fix for #808: pin the suites an ECDSA certificate can actually satisfy, so a + // peer cannot select an ECDHE_RSA suite and stall the handshake. + assert!( + transport(vec![ + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_128_Gcm_Sha256, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_Aes_256_Cbc_Sha, + CipherSuiteId::Tls_Ecdhe_Ecdsa_With_ChaCha20_Poly1305_Sha256, + ]) + .prepare_transport(RTCIceRole::Controlling, remote_params()) + .is_ok() + ); + } + + #[test] + fn unsatisfiable_cipher_suites_are_rejected_rather_than_ignored() { + // This is the assertion that proves plumbing. PSK suites are filtered out when no + // PSK is configured, leaving nothing usable. If `set_dtls_cipher_suites` were + // dropped on the floor, the default set would be used and this would succeed. + let err = transport(vec![CipherSuiteId::Tls_Psk_With_Aes_128_Ccm]) + .prepare_transport(RTCIceRole::Controlling, remote_params()) + .expect_err("a PSK-only list with no PSK leaves no usable suite"); + assert!( + err.to_string().contains("CipherSuite"), + "expected a cipher-suite error, got: {err}" + ); + } + + #[test] + fn filters_srtp_profiles_by_provider_capabilities() -> Result<()> { + let default_provider = crypto::default_provider().expect("test crypto provider"); + let certificate = RTCCertificate::generate( + default_provider.crypto(), + crypto::SignatureScheme::EcdsaP256Sha256, + CertificateParams::new(vec!["webrtc.rs".to_owned()]) + .map_err(|e| Error::Other(e.to_string()))?, + )?; + let provider: Arc = Arc::new(ProfileFilteringProvider { + random_provider: default_provider, + crypto: MissingAes128Gcm, + }); + + let build = |profiles| { + RTCDtlsTransport::new(RTCDtlsTransportConfig { + certificates: vec![certificate.clone()], + answering_dtls_role: DEFAULT_DTLS_ROLE_ANSWER, + srtp_protection_profiles: profiles, + dtls_cipher_suites: vec![], + allow_insecure_verification_algorithm: false, + disable_certificate_fingerprint_verification: false, + replay_protection: ReplayProtection::default(), + crypto_provider: provider.clone(), + }) + }; + + let error = build(vec![SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm])? + .prepare_transport(RTCIceRole::Controlling, remote_params()) + .expect_err("the provider's unsupported profile must not be advertised"); + assert!(error.to_string().contains("supports none")); + + assert!( + build(vec![ + SrtpProtectionProfile::Srtp_Aead_Aes_128_Gcm, + SrtpProtectionProfile::Srtp_Aes128_Cm_Hmac_Sha1_80, + ])? + .prepare_transport(RTCIceRole::Controlling, remote_params()) + .is_ok() + ); + Ok(()) + } +} diff --git a/src/peer_connection/transport/dtls/role.rs b/src/peer_connection/transport/dtls/role.rs index de82f6d4..cab32a27 100644 --- a/src/peer_connection/transport/dtls/role.rs +++ b/src/peer_connection/transport/dtls/role.rs @@ -76,6 +76,7 @@ use serde::{Deserialize, Serialize}; /// [RFC 5763]: https://datatracker.ietf.org/doc/html/rfc5763 /// [RFC 8122]: https://datatracker.ietf.org/doc/html/rfc8122 #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCDtlsRole { /// Role not specified. This should not occur in normal operation. #[default] diff --git a/src/peer_connection/transport/dtls/state.rs b/src/peer_connection/transport/dtls/state.rs index a2233395..4783f142 100644 --- a/src/peer_connection/transport/dtls/state.rs +++ b/src/peer_connection/transport/dtls/state.rs @@ -85,6 +85,7 @@ use std::fmt; /// [MDN RTCDtlsTransport.state]: https://developer.mozilla.org/en-US/docs/Web/API/RTCDtlsTransport/state /// [RFC 6347]: https://datatracker.ietf.org/doc/html/rfc6347 #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCDtlsTransportState { /// State not specified. This should not occur in normal operation. #[default] diff --git a/src/peer_connection/transport/ice/candidate.rs b/src/peer_connection/transport/ice/candidate.rs index 4cd2c97d..6d5ea21a 100644 --- a/src/peer_connection/transport/ice/candidate.rs +++ b/src/peer_connection/transport/ice/candidate.rs @@ -13,6 +13,7 @@ pub use ice::candidate::{ }; #[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceTcpCandidateType { #[default] Unspecified, @@ -34,6 +35,7 @@ impl From for RTCIceTcpCandidateType { TcpType::Active => RTCIceTcpCandidateType::Active, TcpType::Passive => RTCIceTcpCandidateType::Passive, TcpType::SimultaneousOpen => RTCIceTcpCandidateType::SimultaneousOpen, + _ => RTCIceTcpCandidateType::Unspecified, } } } @@ -50,6 +52,7 @@ impl RTCIceTcpCandidateType { } #[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceServerTransportProtocol { #[default] Unspecified, diff --git a/src/peer_connection/transport/ice/candidate_type.rs b/src/peer_connection/transport/ice/candidate_type.rs index d8a0a6bb..84737e22 100644 --- a/src/peer_connection/transport/ice/candidate_type.rs +++ b/src/peer_connection/transport/ice/candidate_type.rs @@ -105,6 +105,7 @@ use serde::{Deserialize, Serialize}; /// [W3C RTCIceCandidateStats.candidateType]: https://w3c.github.io/webrtc-stats/#dom-rtcicecandidatestats-candidatetype /// [MDN RTCIceCandidateStats.candidateType]: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidateStats/candidateType #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceCandidateType { /// Type not specified. This should not occur in normal operation. #[default] diff --git a/src/peer_connection/transport/ice/mod.rs b/src/peer_connection/transport/ice/mod.rs index 5a9c29e0..2da4e713 100644 --- a/src/peer_connection/transport/ice/mod.rs +++ b/src/peer_connection/transport/ice/mod.rs @@ -4,6 +4,7 @@ use crate::peer_connection::transport::ice::candidate::RTCIceCandidate; use crate::peer_connection::transport::ice::parameters::RTCIceParameters; use crate::peer_connection::transport::ice::role::RTCIceRole; use crate::peer_connection::transport::ice::state::RTCIceTransportState; +use crypto::RTCCryptoProvider; use ice::candidate::Candidate; use ice::tcp_type::TcpType; use ice::{Agent, AgentConfig}; @@ -21,7 +22,6 @@ pub(crate) mod state; /// ICETransport allows an application access to information about the ICE /// transport over which packets are sent and received. -#[derive(Default)] pub(crate) struct RTCIceTransport { pub(crate) agent: Agent, @@ -31,13 +31,16 @@ pub(crate) struct RTCIceTransport { impl RTCIceTransport { /// creates a new RTCIceTransport - pub(crate) fn new(agent_config: AgentConfig) -> Result { - let agent = Agent::new(Arc::new(agent_config))?; + pub(crate) fn new( + agent_config: AgentConfig, + crypto_provider: Arc, + ) -> Result { + let agent = Agent::new(Arc::new(agent_config), crypto_provider)?; Ok(RTCIceTransport { agent, ice_gathering_state: RTCIceGatheringState::New, - ..Default::default() + ice_connection_state: RTCIceConnectionState::default(), }) } diff --git a/src/peer_connection/transport/ice/protocol.rs b/src/peer_connection/transport/ice/protocol.rs index 9740524b..b4fdea0e 100644 --- a/src/peer_connection/transport/ice/protocol.rs +++ b/src/peer_connection/transport/ice/protocol.rs @@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize}; /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate/protocol /// [W3C]: https://w3c.github.io/webrtc-pc/#rtciceprotocol-enum #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceProtocol { /// The protocol is unspecified. #[default] diff --git a/src/peer_connection/transport/ice/role.rs b/src/peer_connection/transport/ice/role.rs index 410d168f..58d8567d 100644 --- a/src/peer_connection/transport/ice/role.rs +++ b/src/peer_connection/transport/ice/role.rs @@ -12,6 +12,7 @@ use std::fmt; /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceTransport/role /// [W3C]: https://w3c.github.io/webrtc-pc/#dom-rtcicerole #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceRole { /// The ICE role is unspecified. #[default] diff --git a/src/peer_connection/transport/ice/state.rs b/src/peer_connection/transport/ice/state.rs index 3cb55f12..7cb26246 100644 --- a/src/peer_connection/transport/ice/state.rs +++ b/src/peer_connection/transport/ice/state.rs @@ -12,6 +12,7 @@ use std::fmt; /// [MDN]: https://developer.mozilla.org/en-US/docs/Web/API/RTCIceTransport/state /// [W3C]: https://w3c.github.io/webrtc-pc/#dom-rtcicetransportstate #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCIceTransportState { /// The transport state is unspecified. #[default] diff --git a/src/peer_connection/transport/sctp/state.rs b/src/peer_connection/transport/sctp/state.rs index fb323a9b..cfad9fd1 100644 --- a/src/peer_connection/transport/sctp/state.rs +++ b/src/peer_connection/transport/sctp/state.rs @@ -9,6 +9,7 @@ use std::fmt; /// [W3C]: https://w3c.github.io/webrtc-pc/#rtcsctptransportstate #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] #[repr(u8)] +#[non_exhaustive] pub enum RTCSctpTransportState { /// The transport state is unspecified. #[default] diff --git a/src/rtp_transceiver/direction.rs b/src/rtp_transceiver/direction.rs index c61422fb..95fa9c95 100644 --- a/src/rtp_transceiver/direction.rs +++ b/src/rtp_transceiver/direction.rs @@ -14,6 +14,7 @@ use std::fmt; /// /// See [RTCRtpTransceiver.direction](https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpTransceiver/direction). #[derive(Default, Debug, Copy, Clone, PartialEq, Eq)] +#[non_exhaustive] pub enum RTCRtpTransceiverDirection { /// Direction is not specified (internal use only). #[default] diff --git a/src/rtp_transceiver/rtp_receiver/mod.rs b/src/rtp_transceiver/rtp_receiver/mod.rs index d3fd9b42..97137d98 100644 --- a/src/rtp_transceiver/rtp_receiver/mod.rs +++ b/src/rtp_transceiver/rtp_receiver/mod.rs @@ -317,15 +317,26 @@ where /// /// # Example /// - /// ```ignore + /// ``` /// // Send a Picture Loss Indication to request a keyframe - /// use rtcp::picture_loss_indication::PictureLossIndication; + /// use rtc::peer_connection::RTCPeerConnection; + /// use rtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; + /// use rtc::rtp_transceiver::RTCRtpReceiverId; /// - /// let pli = PictureLossIndication { - /// sender_ssrc: 0, - /// media_ssrc: remote_ssrc, - /// }; - /// receiver.write_rtcp(vec![Box::new(pli)])?; + /// # fn example( + /// # peer_connection: &mut RTCPeerConnection, + /// # receiver_id: RTCRtpReceiverId, + /// # remote_ssrc: u32, + /// # ) -> Result<(), Box> { + /// if let Some(mut receiver) = peer_connection.rtp_receiver(receiver_id) { + /// let pli = PictureLossIndication { + /// sender_ssrc: 0, + /// media_ssrc: remote_ssrc, + /// }; + /// receiver.write_rtcp(vec![Box::new(pli)])?; + /// } + /// # Ok(()) + /// # } /// ``` pub fn write_rtcp(&mut self, packets: Vec>) -> Result<()> { // peer_connection is mutable borrow, its rtp_transceivers won't be resized and diff --git a/src/rtp_transceiver/rtp_sender/rtp_codec.rs b/src/rtp_transceiver/rtp_sender/rtp_codec.rs index 0519b21c..c5fe1c22 100644 --- a/src/rtp_transceiver/rtp_sender/rtp_codec.rs +++ b/src/rtp_transceiver/rtp_sender/rtp_codec.rs @@ -10,6 +10,7 @@ use std::fmt; /// Codec kind identifying the media type. #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[non_exhaustive] pub enum RtpCodecKind { /// Unspecified or unknown codec type #[default] diff --git a/src/statistics/accumulator/codec.rs b/src/statistics/accumulator/codec.rs index 4a9d4ee4..9fe0b640 100644 --- a/src/statistics/accumulator/codec.rs +++ b/src/statistics/accumulator/codec.rs @@ -9,6 +9,7 @@ use std::time::Instant; /// Direction qualifier for codec stats IDs. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] pub enum CodecDirection { /// Codec used for sending (encoding). Send, diff --git a/src/statistics/accumulator/mod.rs b/src/statistics/accumulator/mod.rs index 803e4bbb..f1663628 100644 --- a/src/statistics/accumulator/mod.rs +++ b/src/statistics/accumulator/mod.rs @@ -702,8 +702,9 @@ impl RTCStatsAccumulator { /// /// # Arguments /// - /// * `pair_id` - The ID of the candidate pair to sync - /// * `cp_stats.` - CandidatePairStats + /// * `local_id` - The local candidate ID of the pair to sync + /// * `remote_id` - The remote candidate ID of the pair to sync + /// * `cp_stats` - The ice agent's stats for that candidate pair pub(crate) fn update_ice_agent_stats( &mut self, local_id: &str, diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs index 4cbba968..0cd5988e 100644 --- a/src/statistics/mod.rs +++ b/src/statistics/mod.rs @@ -12,14 +12,19 @@ //! //! # Example //! -//! ```ignore +//! ``` +//! use rtc::peer_connection::RTCPeerConnection; +//! use rtc::rtp_transceiver::RTCRtpSenderId; //! use rtc::statistics::StatsSelector; +//! use std::time::Instant; //! +//! # fn example(pc: &mut RTCPeerConnection, sender_id: RTCRtpSenderId) { //! // Get all stats //! let all_stats = pc.get_stats(Instant::now(), StatsSelector::None); //! //! // Get stats for a specific sender //! let sender_stats = pc.get_stats(Instant::now(), StatsSelector::Sender(sender_id)); +//! # } //! ``` use crate::rtp_transceiver::{RTCRtpReceiverId, RTCRtpSenderId}; @@ -46,6 +51,7 @@ pub mod stats; /// - `None` - Return all statistics for the entire connection /// - `Sender` - Return statistics for a specific RTP sender and referenced objects /// - `Receiver` - Return statistics for a specific RTP receiver and referenced objects +#[non_exhaustive] pub enum StatsSelector { /// Gather stats for the whole connection. /// diff --git a/src/statistics/report.rs b/src/statistics/report.rs index 42f0c530..0cee62d8 100644 --- a/src/statistics/report.rs +++ b/src/statistics/report.rs @@ -24,6 +24,7 @@ use std::collections::HashMap; /// /// Each variant corresponds to a different W3C WebRTC stats dictionary type. #[derive(Debug)] +#[non_exhaustive] pub enum RTCStatsReportEntry { /// Peer connection level statistics. PeerConnection(RTCPeerConnectionStats), @@ -116,9 +117,13 @@ impl RTCStatsReportEntry { /// /// # Example /// -/// ```ignore +/// ``` +/// use rtc::peer_connection::RTCPeerConnection; /// use rtc::statistics::StatsSelector; +/// use rtc::statistics::stats::RTCStatsType; +/// use std::time::Instant; /// +/// # fn example(peer_connection: &mut RTCPeerConnection) { /// let report = peer_connection.get_stats(Instant::now(), StatsSelector::None); /// /// // Iterate over all stats @@ -135,6 +140,7 @@ impl RTCStatsReportEntry { /// for inbound in report.iter_by_type(RTCStatsType::InboundRTP) { /// println!("Inbound RTP: {:?}", inbound.id()); /// } +/// # } /// ``` #[derive(Debug, Default)] pub struct RTCStatsReport { diff --git a/src/statistics/stats/ice_candidate_pair.rs b/src/statistics/stats/ice_candidate_pair.rs index c279e31e..b6a8fb07 100644 --- a/src/statistics/stats/ice_candidate_pair.rs +++ b/src/statistics/stats/ice_candidate_pair.rs @@ -14,6 +14,7 @@ use std::time::Instant; /// This enum represents the current state of a candidate pair /// in the ICE connectivity check process. #[derive(Default, PartialEq, Eq, Debug, Copy, Clone, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCStatsIceCandidatePairState { /// State has not been set. #[default] @@ -48,6 +49,7 @@ impl From for RTCStatsIceCandidatePairState { CandidatePairState::InProgress => RTCStatsIceCandidatePairState::InProgress, CandidatePairState::Failed => RTCStatsIceCandidatePairState::Failed, CandidatePairState::Succeeded => RTCStatsIceCandidatePairState::Succeeded, + _ => RTCStatsIceCandidatePairState::Unspecified, } } } diff --git a/src/statistics/stats/mod.rs b/src/statistics/stats/mod.rs index 8692acdc..58caa66a 100644 --- a/src/statistics/stats/mod.rs +++ b/src/statistics/stats/mod.rs @@ -14,10 +14,12 @@ //! //! # Example //! -//! ```ignore -//! use std::time::Instant; +//! ``` +//! use rtc::peer_connection::RTCPeerConnection; //! use rtc::statistics::StatsSelector; +//! use std::time::Instant; //! +//! # fn example(peer_connection: &mut RTCPeerConnection) { //! let report = peer_connection.get_stats(Instant::now(), StatsSelector::None); //! //! // Access transport statistics @@ -30,6 +32,7 @@ //! for dc in report.data_channels() { //! println!("Channel '{}': {} messages sent", dc.label, dc.messages_sent); //! } +//! # } //! ``` use ::serde::{Deserialize, Serialize}; @@ -56,6 +59,7 @@ pub mod transport; /// Values are serialized using the W3C-specified lowercase hyphenated format /// (e.g., `InboundRTP` serializes to `"inbound-rtp"`). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCStatsType { /// Statistics for a media codec. #[serde(rename = "codec")] @@ -139,6 +143,7 @@ pub struct RTCStats { /// This enum indicates why the video encoder may have reduced quality /// (resolution, frame rate, or bitrate) during encoding. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[non_exhaustive] pub enum RTCQualityLimitationReason { /// No quality limitation is active. #[default] diff --git a/tests/README.md b/tests/README.md index 5af95620..b73b412f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -64,6 +64,7 @@ Consider adding tests for: | 13 | `simulcast_rtc_to_webrtc_interop.rs` | Simulcast: rtc→webrtc (IGNORED) | | 14 | `offer_answer_rtc2rtc.rs` | **Pure rtc ↔ rtc (no webrtc)** | | 15 | `interceptor_rtcp_reports_interop.rs` | **RTCP report interceptor integration** | +| 16 | `rtcp_processing_boxed_interop.rs` | **Type-erased (`BoxedInterceptor`) chain** | --- @@ -741,3 +742,49 @@ Run these with: ```bash cargo test --package rtc-interceptor --test rtcp_report_integration ``` + +--- + +## Test 16: Type-Erased Interceptor Chain (`BoxedInterceptor`) + +**File:** `rtcp_processing_boxed_interop.rs` + +**Purpose:** The `RTCPeerConnection` counterpart of Test 15's `rtcp_processing_interop.rs`. It +installs the same custom `RtcpForwarderInterceptor`, but erases the chain's type so that every peer connection has the +one concrete type `RTCPeerConnection`. + +### Why + +`RTCPeerConnection` is generic over its interceptor chain. Returning `RTCPeerConnection` (what +Test 15 does) works while the value flows straight into a local, but the opaque type cannot be stored in a non-generic +struct, put in a collection next to a peer with a different chain, or produced by two branches of an `if`. Erasing the +chain with `Registry::boxed()` removes all three limits, at the cost of one virtual call per chain entry point. + +The file demonstrates both halves of that: `build_boxed_rtc_peer(forward_rtcp, is_answerer)` picks its chain at +runtime (the branches unify only after `.boxed()`), and `struct RtcpPeer` — which owns a peer connection, a socket and +the test's counters — has **no type parameter**. + +### Tests + +| Test | Description | +|----------------------------------------------------------|-------------------------------------------------------------------| +| `test_boxed_rtcp_processing_webrtc_offerer_rtc_answerer` | webrtc sends video; the boxed chain still surfaces RTCP via `poll_read()` | +| `test_boxed_rtcp_processing_rtc_sender_receives_feedback` | the boxed-chain RTC sender receives RTCP feedback about its own stream | +| `test_boxed_rtc_to_rtc_heterogeneous_chains` | two rtc peers with **different** chains driven out of a single `Vec` | + +The third test is the one the non-erased form cannot express. The offerer streams RTP (so its +`SenderReportInterceptor` emits periodic SRs) and is built **without** the forwarder; the answerer is built **with** +it. Both live in one `Vec` and are driven by the same non-generic loop, and the assertion is that their behaviour +differs accordingly: the answerer surfaces the SRs to the application, while the offerer — same type, different chain +— never surfaces a single RTCP packet. + +### Running the Tests + +```bash +cargo test --test rtcp_processing_boxed_interop -- --nocapture +``` + +### Related + +The `rtcp-processing-boxed` example (`examples/rtcp-processing-boxed/`) is the browser-driven version of the same +pattern, with a `--no-rtcp-forwarding` flag that switches the chain at runtime. diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 00000000..c5abc899 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,21 @@ +//! Shared helpers for the interop tests. + +/// Installs a process-wide rustls `CryptoProvider` if one is not already installed. +/// +/// The interop tests drive the published `webrtc` crate alongside this one, and its `dtls 0.13` +/// dependency asks rustls to infer the provider from crate features. That inference only works +/// when exactly one of rustls' `ring`/`aws-lc-rs` features is enabled — but in a test build both +/// are: `dtls 0.13` turns on `rustls/ring` while `rtc` turns on whichever its own feature selects. +/// Cargo unifies the two, rustls finds an ambiguity, and the old crate panics. +/// +/// Naming a provider here resolves it for that crate. `rtc` itself no longer consults this +/// default — it passes its provider explicitly (see `rtc_dtls::config`) — so which one we install +/// only has to satisfy the old code. `ring` is always available in a test build: either `rtc` +/// selected it, or `dtls 0.13` pulled it in. +/// +/// Idempotent, and safe to call from every test: `install_default` returns `Err` once a provider +/// is set, which is ignored here, so the first caller wins and an application that installed its +/// own is unaffected. +pub fn install_crypto_provider() { + let _ = rustls::crypto::ring::default_provider().install_default(); +} diff --git a/tests/crypto_provider_peer_connections.rs b/tests/crypto_provider_peer_connections.rs new file mode 100644 index 00000000..a606415b --- /dev/null +++ b/tests/crypto_provider_peer_connections.rs @@ -0,0 +1,585 @@ +#![cfg(all(feature = "crypto-ring", feature = "crypto-aws-lc-rs"))] + +use std::net::SocketAddr; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::{Duration, Instant}; + +use anyhow::{Result, ensure}; +use bytes::{Bytes, BytesMut}; +use rtc::crypto::providers::{AwsLcRsProvider, RingProvider}; +use rtc::crypto::{ + ActiveKeyExchange, AeadAlgorithm, AeadCipher, BlockCipherAlgorithm, CbcAlgorithm, CbcCipher, + CryptoAlgorithm, CryptoError, HashAlgorithm, HmacAlgorithm, KeyExchangeAlgorithm, PublicKey, + RTCCrypto, RTCCryptoProvider, RTCRandom, SignatureScheme, SigningKey, StreamCipher, + StreamCipherAlgorithm, +}; +use rtc::media_stream::MediaStreamTrack; +use rtc::peer_connection::configuration::media_engine::{MIME_TYPE_VP8, MediaEngine}; +use rtc::peer_connection::configuration::setting_engine::SettingEngine; +use rtc::peer_connection::event::{RTCPeerConnectionEvent, RTCTrackEvent}; +use rtc::peer_connection::message::RTCMessage; +use rtc::peer_connection::state::RTCPeerConnectionState; +use rtc::peer_connection::transport::{CandidateConfig, CandidateHostConfig, RTCIceCandidate}; +use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; +use rtc::rtcp::payload_feedbacks::picture_loss_indication::PictureLossIndication; +use rtc::rtp; +use rtc::rtp_transceiver::rtp_sender::{ + RTCRtpCodec, RTCRtpCodecParameters, RTCRtpCodingParameters, RTCRtpEncodingParameters, + RtpCodecKind, +}; +use rtc::rtp_transceiver::{RTCRtpReceiverId, RTCRtpSenderId}; +use rtc::sansio::Protocol; +use rtc::shared::{TaggedBytesMut, TransportContext, TransportProtocol}; +use tokio::net::UdpSocket; + +const SESSION_TIMEOUT: Duration = Duration::from_secs(10); +const MEDIA_SSRC: u32 = 0x1122_3344; + +#[derive(Default)] +struct Calls { + random: AtomicUsize, + hash: AtomicUsize, + hmac: AtomicUsize, + aead: AtomicUsize, + aead_seal: AtomicUsize, + aead_open: AtomicUsize, + key_exchange: AtomicUsize, + signing: AtomicUsize, + verification: AtomicUsize, +} + +struct RecordingProvider { + inner: Arc, + calls: Arc, +} + +impl RecordingProvider { + fn wrap(inner: impl RTCCryptoProvider + 'static) -> (Arc, Arc) { + let calls = Arc::new(Calls::default()); + let provider: Arc = Arc::new(Self { + inner: Arc::new(inner), + calls: calls.clone(), + }); + (provider, calls) + } +} + +impl RTCCryptoProvider for RecordingProvider { + fn name(&self) -> &'static str { + self.inner.name() + } + + fn crypto(&self) -> &dyn RTCCrypto { + self + } + + fn random(&self) -> &dyn RTCRandom { + self + } +} + +impl RTCRandom for RecordingProvider { + fn fill(&self, output: &mut [u8]) -> Result<(), CryptoError> { + self.calls.random.fetch_add(1, Ordering::Relaxed); + self.inner.random().fill(output) + } +} + +/// Counts every tag computation, so the test can assert the provider is actually used. The count +/// now happens per `sign`/`verify` rather than per one-shot `hmac` call, since keyed MACs are +/// created once per context. +struct CountingMac { + inner: Box, + calls: Arc, +} + +impl rtc::crypto::Mac for CountingMac { + fn output_len(&self) -> usize { + self.inner.output_len() + } + + fn sign(&mut self, input: &[&[u8]], output: &mut [u8]) -> Result<(), CryptoError> { + self.calls.hmac.fetch_add(1, Ordering::Relaxed); + self.inner.sign(input, output) + } + + fn verify(&mut self, input: &[&[u8]], expected: &[u8]) -> Result<(), CryptoError> { + self.calls.hmac.fetch_add(1, Ordering::Relaxed); + self.inner.verify(input, expected) + } +} + +impl RTCCrypto for RecordingProvider { + fn supports(&self, algorithm: CryptoAlgorithm) -> bool { + self.inner.crypto().supports(algorithm) + } + + fn hash(&self, algorithm: HashAlgorithm, data: &[u8]) -> Result, CryptoError> { + self.calls.hash.fetch_add(1, Ordering::Relaxed); + self.inner.crypto().hash(algorithm, data) + } + + fn new_hmac( + &self, + algorithm: HmacAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + Ok(Box::new(CountingMac { + inner: self.inner.crypto().new_hmac(algorithm, key)?, + calls: Arc::clone(&self.calls), + })) + } + + fn block_encrypt( + &self, + algorithm: BlockCipherAlgorithm, + key: &[u8], + block: &mut [u8], + ) -> Result<(), CryptoError> { + self.inner.crypto().block_encrypt(algorithm, key, block) + } + + fn new_stream_cipher( + &self, + algorithm: StreamCipherAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + self.inner.crypto().new_stream_cipher(algorithm, key) + } + + fn new_aead( + &self, + algorithm: AeadAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + self.calls.aead.fetch_add(1, Ordering::Relaxed); + Ok(Box::new(RecordingAead { + inner: self.inner.crypto().new_aead(algorithm, key)?, + calls: self.calls.clone(), + })) + } + + fn new_cbc( + &self, + algorithm: CbcAlgorithm, + key: &[u8], + ) -> Result, CryptoError> { + self.inner.crypto().new_cbc(algorithm, key) + } + + fn start_key_exchange( + &self, + algorithm: KeyExchangeAlgorithm, + ) -> Result, CryptoError> { + self.calls.key_exchange.fetch_add(1, Ordering::Relaxed); + self.inner.crypto().start_key_exchange(algorithm) + } + + fn generate_signing_key( + &self, + scheme: SignatureScheme, + ) -> Result, CryptoError> { + self.calls.signing.fetch_add(1, Ordering::Relaxed); + self.inner.crypto().generate_signing_key(scheme) + } + + fn import_signing_key( + &self, + scheme: SignatureScheme, + pkcs8_der: &[u8], + ) -> Result, CryptoError> { + self.calls.signing.fetch_add(1, Ordering::Relaxed); + self.inner.crypto().import_signing_key(scheme, pkcs8_der) + } + + fn verify_signature( + &self, + scheme: SignatureScheme, + public_key: PublicKey<'_>, + message: &[u8], + signature: &[u8], + ) -> Result<(), CryptoError> { + self.calls.verification.fetch_add(1, Ordering::Relaxed); + self.inner + .crypto() + .verify_signature(scheme, public_key, message, signature) + } +} + +struct RecordingAead { + inner: Box, + calls: Arc, +} + +impl AeadCipher for RecordingAead { + fn tag_len(&self) -> usize { + self.inner.tag_len() + } + + fn seal_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + plaintext_and_ciphertext: &mut [u8], + tag_out: &mut [u8], + ) -> Result<(), CryptoError> { + self.calls.aead_seal.fetch_add(1, Ordering::Relaxed); + self.inner + .seal_in_place(nonce, aad, plaintext_and_ciphertext, tag_out) + } + + fn open_in_place( + &mut self, + nonce: &[u8], + aad: &[u8], + ciphertext_and_plaintext: &mut [u8], + tag: &[u8], + ) -> Result<(), CryptoError> { + self.calls.aead_open.fetch_add(1, Ordering::Relaxed); + self.inner + .open_in_place(nonce, aad, ciphertext_and_plaintext, tag) + } +} + +struct Peer { + pc: RTCPeerConnection, + socket: UdpSocket, + local_addr: SocketAddr, +} + +impl Peer { + async fn new( + provider: Arc, + send_media: bool, + ) -> Result<(Self, Option)> { + let socket = UdpSocket::bind("127.0.0.1:0").await?; + let local_addr = socket.local_addr()?; + let codec = RTCRtpCodecParameters { + rtp_codec: RTCRtpCodec { + mime_type: MIME_TYPE_VP8.to_owned(), + clock_rate: 90_000, + ..Default::default() + }, + payload_type: 96, + }; + let mut media_engine = MediaEngine::default(); + media_engine.register_codec(codec.clone(), RtpCodecKind::Video)?; + let mut setting_engine = SettingEngine::default(); + setting_engine.set_crypto_provider(provider); + let mut pc = RTCPeerConnectionBuilder::new() + .with_setting_engine(setting_engine) + .with_media_engine(media_engine) + .build()?; + + let sender_id = if send_media { + Some(pc.add_track(MediaStreamTrack::new( + "provider-test-stream".to_owned(), + "provider-test-video".to_owned(), + "provider test video".to_owned(), + RtpCodecKind::Video, + vec![RTCRtpEncodingParameters { + rtp_coding_parameters: RTCRtpCodingParameters { + ssrc: Some(MEDIA_SSRC), + ..Default::default() + }, + codec: codec.rtp_codec, + ..Default::default() + }], + ))?) + } else { + None + }; + + let candidate = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: local_addr.ip().to_string(), + port: local_addr.port(), + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + pc.add_local_candidate(RTCIceCandidate::from(&candidate).to_json()?)?; + Ok(( + Self { + pc, + socket, + local_addr, + }, + sender_id, + )) + } + + async fn send_pending(&mut self) -> Result<()> { + while let Some(message) = self.pc.poll_write() { + self.socket + .send_to(&message.message, message.transport.peer_addr) + .await?; + } + Ok(()) + } + + fn receive(&mut self, data: &[u8], peer_addr: SocketAddr) -> Result<()> { + Ok(self.pc.handle_read(TaggedBytesMut { + now: Instant::now(), + transport: TransportContext { + local_addr: self.local_addr, + peer_addr, + ecn: None, + transport_protocol: TransportProtocol::UDP, + }, + message: BytesMut::from(data), + })?) + } +} + +async fn exercise_pair( + offer_provider: Arc, + offer_calls: Arc, + answer_provider: Arc, + answer_calls: Arc, +) -> Result<()> { + let (mut offer, sender_id) = Peer::new(offer_provider, true).await?; + let sender_id = sender_id.expect("offer peer has a media sender"); + let (mut answer, _) = Peer::new(answer_provider, false).await?; + + let description = offer.pc.create_offer(None)?; + offer.pc.set_local_description(description.clone())?; + answer.pc.set_remote_description(description)?; + let description = answer.pc.create_answer(None)?; + answer.pc.set_local_description(description.clone())?; + offer.pc.set_remote_description(description)?; + + let mut offer_connected = false; + let mut answer_connected = false; + let mut receiver_id: Option = None; + let mut sent_rtp = 0_u16; + let mut received_rtp = false; + let mut sent_rtcp = false; + let mut received_rtcp = false; + let mut rtcp_aead_baseline = None; + let mut sequence_number = 0; + let mut offer_buffer = vec![0; 2048]; + let mut answer_buffer = vec![0; 2048]; + let started = Instant::now(); + + while started.elapsed() < SESSION_TIMEOUT && !received_rtcp { + offer.send_pending().await?; + answer.send_pending().await?; + + while let Some(event) = offer.pc.poll_event() { + if matches!( + event, + RTCPeerConnectionEvent::OnConnectionStateChangeEvent( + RTCPeerConnectionState::Connected + ) + ) { + offer_connected = true; + } + } + while let Some(event) = answer.pc.poll_event() { + match event { + RTCPeerConnectionEvent::OnConnectionStateChangeEvent( + RTCPeerConnectionState::Connected, + ) => answer_connected = true, + RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(init)) => { + receiver_id = Some(init.receiver_id) + } + _ => {} + } + } + + while let Some(message) = answer.pc.poll_read() { + if matches!(message, RTCMessage::RtpPacket(_, _)) { + received_rtp = true; + } + } + while let Some(message) = offer.pc.poll_read() { + if matches!(message, RTCMessage::RtcpPacket(_, _)) { + received_rtcp = true; + } + } + if let Some((offer_open, answer_seal)) = rtcp_aead_baseline { + received_rtcp = offer_calls.aead_open.load(Ordering::Relaxed) > offer_open + && answer_calls.aead_seal.load(Ordering::Relaxed) > answer_seal; + } + + if offer_connected && answer_connected && !received_rtp && sent_rtp < 30 { + sequence_number += 1; + let payload_type = offer + .pc + .rtp_sender(sender_id) + .and_then(|mut sender| { + sender + .get_parameters() + .rtp_parameters + .codecs + .first() + .map(|codec| codec.payload_type) + }) + .unwrap_or(96); + offer + .pc + .rtp_sender(sender_id) + .expect("media sender exists") + .write_rtp(rtp::packet::Packet { + header: rtp::header::Header { + version: 2, + marker: true, + payload_type, + sequence_number, + timestamp: 90_000, + ssrc: MEDIA_SSRC, + ..Default::default() + }, + payload: Bytes::from_static(b"provider-isolation"), + })?; + sent_rtp += 1; + } + if received_rtp && !sent_rtcp { + rtcp_aead_baseline = Some(( + offer_calls.aead_open.load(Ordering::Relaxed), + answer_calls.aead_seal.load(Ordering::Relaxed), + )); + answer + .pc + .rtp_receiver(receiver_id.expect("receiver opened before RTP arrived")) + .expect("media receiver exists") + .write_rtcp(vec![Box::new(PictureLossIndication { + sender_ssrc: 0, + media_ssrc: MEDIA_SSRC, + })])?; + sent_rtcp = true; + } + + let next_timeout = offer + .pc + .poll_timeout() + .unwrap_or_else(|| Instant::now() + SESSION_TIMEOUT) + .min( + answer + .pc + .poll_timeout() + .unwrap_or_else(|| Instant::now() + SESSION_TIMEOUT), + ); + let delay = next_timeout + .saturating_duration_since(Instant::now()) + .min(Duration::from_millis(10)); + if delay.is_zero() { + offer.pc.handle_timeout(Instant::now())?; + answer.pc.handle_timeout(Instant::now())?; + continue; + } + + tokio::select! { + _ = tokio::time::sleep(delay) => { + offer.pc.handle_timeout(Instant::now())?; + answer.pc.handle_timeout(Instant::now())?; + } + result = offer.socket.recv_from(&mut offer_buffer) => { + let (length, peer_addr) = result?; + offer.receive(&offer_buffer[..length], peer_addr)?; + } + result = answer.socket.recv_from(&mut answer_buffer) => { + let (length, peer_addr) = result?; + answer.receive(&answer_buffer[..length], peer_addr)?; + } + } + } + + ensure!( + offer_connected && answer_connected, + "peer connection did not reach Connected" + ); + ensure!( + received_rtp, + "answerer did not receive provider-encrypted SRTP" + ); + ensure!( + received_rtcp, + "offerer did not receive provider-encrypted SRTCP" + ); + offer.pc.close()?; + answer.pc.close()?; + Ok(()) +} + +fn assert_calls(calls: &Calls) { + assert!( + calls.random.load(Ordering::Relaxed) > 0, + "provider randomness was bypassed" + ); + assert!( + calls.hash.load(Ordering::Relaxed) > 0, + "provider fingerprint hashing was bypassed" + ); + assert!( + calls.hmac.load(Ordering::Relaxed) > 0, + "provider STUN HMAC was bypassed" + ); + assert!( + calls.aead.load(Ordering::Relaxed) > 0, + "provider DTLS/SRTP AEAD was bypassed" + ); + assert!( + calls.key_exchange.load(Ordering::Relaxed) > 0, + "provider key exchange was bypassed" + ); + assert!( + calls.signing.load(Ordering::Relaxed) > 0, + "provider signing-key generation was bypassed" + ); + assert!( + calls.verification.load(Ordering::Relaxed) > 0, + "provider signature verification was bypassed" + ); +} + +#[tokio::test] +async fn isolates_crypto_providers_across_simultaneous_peer_connections() -> Result<()> { + env_logger::builder().is_test(true).try_init().ok(); + let (ring_a, ring_a_calls) = RecordingProvider::wrap(RingProvider::new()); + let (ring_b, ring_b_calls) = RecordingProvider::wrap(RingProvider::new()); + let (aws_a, aws_a_calls) = RecordingProvider::wrap(AwsLcRsProvider::new()); + let (aws_b, aws_b_calls) = RecordingProvider::wrap(AwsLcRsProvider::new()); + exercise_pair(ring_a, ring_a_calls.clone(), ring_b, ring_b_calls.clone()).await?; + exercise_pair(aws_a, aws_a_calls.clone(), aws_b, aws_b_calls.clone()).await?; + + let (ring_offer, ring_offer_calls) = RecordingProvider::wrap(RingProvider::new()); + let (aws_answer, aws_answer_calls) = RecordingProvider::wrap(AwsLcRsProvider::new()); + let (aws_offer, aws_offer_calls) = RecordingProvider::wrap(AwsLcRsProvider::new()); + let (ring_answer, ring_answer_calls) = RecordingProvider::wrap(RingProvider::new()); + let (ring_to_aws, aws_to_ring) = tokio::join!( + exercise_pair( + ring_offer, + ring_offer_calls.clone(), + aws_answer, + aws_answer_calls.clone(), + ), + exercise_pair( + aws_offer, + aws_offer_calls.clone(), + ring_answer, + ring_answer_calls.clone(), + ), + ); + ring_to_aws?; + aws_to_ring?; + + for calls in [ + ring_a_calls, + ring_b_calls, + aws_a_calls, + aws_b_calls, + ring_offer_calls, + aws_answer_calls, + aws_offer_calls, + ring_answer_calls, + ] { + assert_calls(&calls); + } + Ok(()) +} diff --git a/tests/data_channel_init_defaults.rs b/tests/data_channel_init_defaults.rs new file mode 100644 index 00000000..49c22c52 --- /dev/null +++ b/tests/data_channel_init_defaults.rs @@ -0,0 +1,61 @@ +//! `RTCDataChannelInit`'s defaults must match what its documentation promises. +//! +//! `ordered` is the one that matters. A derived `Default` makes it `false`, so +//! `create_data_channel(label, None)` — the natural way to ask for a plain data channel — +//! would hand back an *unordered* one, contradicting both the field's own doc comment and +//! [W3C `RTCDataChannelInit`], where `ordered` is defined as `= true`. +//! +//! The consequence is not merely out-of-order application messages. Unordered chunks bypass +//! SCTP's ordered-delivery queue, so a first message can overtake the `DATA_CHANNEL_OPEN` +//! sent on the same stream; the peer then receives user data on a stream id it has not +//! accepted yet and drops it. +//! +//! [W3C `RTCDataChannelInit`]: https://www.w3.org/TR/webrtc/#dom-rtcdatachannelinit + +use anyhow::Result; +use rtc::data_channel::RTCDataChannelInit; +use rtc::peer_connection::RTCPeerConnectionBuilder; + +#[test] +fn default_init_is_ordered() { + assert!( + RTCDataChannelInit::default().ordered, + "the documented default for `ordered` is true" + ); +} + +/// The default reaches the channel: passing `None` must not quietly opt out of ordering. +#[test] +fn channel_created_without_options_is_ordered() -> Result<()> { + let mut pc = RTCPeerConnectionBuilder::new().build()?; + + let dc = pc.create_data_channel("plain", None)?; + + assert!( + dc.ordered(), + "a channel created without options must be ordered" + ); + + Ok(()) +} + +/// Opting out still works — this is a default, not a policy. +#[test] +fn unordered_can_still_be_requested() -> Result<()> { + let mut pc = RTCPeerConnectionBuilder::new().build()?; + + let dc = pc.create_data_channel( + "unordered", + Some(RTCDataChannelInit { + ordered: false, + ..Default::default() + }), + )?; + + assert!( + !dc.ordered(), + "an explicit `ordered: false` must be honored" + ); + + Ok(()) +} diff --git a/tests/data_channel_send_before_open.rs b/tests/data_channel_send_before_open.rs new file mode 100644 index 00000000..547c10ff --- /dev/null +++ b/tests/data_channel_send_before_open.rs @@ -0,0 +1,84 @@ +//! `send` must not report success for data it cannot carry. +//! +//! Until the SCTP stream backing a channel exists, the write path cannot deliver anything. That +//! failure used to be invisible: `send` checked only that the channel was *registered*, so it +//! returned `Ok(())`, and the real rejection happened later inside +//! `DataChannelHandler::handle_write` — on the pipeline's write pass, where an `Err` is logged +//! and discarded rather than returned to anyone. +//! +//! The caller was therefore told its message went out while it was dropped on the floor. That is +//! the worst of the three possible contracts: buffering (what [W3C `send()`] prescribes for a +//! `connecting` channel) and erroring are both recoverable, silence is not. +//! +//! [W3C `send()`]: https://www.w3.org/TR/webrtc/#dom-rtcdatachannel-send + +use anyhow::Result; +use bytes::BytesMut; +use rtc::data_channel::{RTCDataChannelInit, RTCDataChannelState}; +use rtc::peer_connection::RTCPeerConnectionBuilder; +use rtc::shared::error::Error; + +#[test] +fn send_before_the_stream_exists_is_rejected() -> Result<()> { + let mut pc = RTCPeerConnectionBuilder::new().build()?; + + let mut dc = pc.create_data_channel("probe", Some(RTCDataChannelInit::default()))?; + assert_eq!( + dc.ready_state(), + RTCDataChannelState::Connecting, + "a freshly created channel has no stream yet" + ); + + assert_eq!( + dc.send(BytesMut::from(&b"dropped"[..])), + Err(Error::ErrDataChannelNotOpen), + "send must not claim success before the channel opens" + ); + assert_eq!( + dc.send_text("dropped"), + Err(Error::ErrDataChannelNotOpen), + "send_text must not claim success before the channel opens" + ); + + Ok(()) +} + +/// A rejected send must not be counted against the back-pressure budget: those bytes never +/// entered the SCTP pipeline, so nothing will ever release them. Leaking the counter would +/// permanently shrink the channel's send window. +#[test] +fn rejected_send_does_not_charge_outstanding_bytes() -> Result<()> { + let mut pc = RTCPeerConnectionBuilder::new().build()?; + + let mut dc = pc.create_data_channel("probe", Some(RTCDataChannelInit::default()))?; + + let _ = dc.send(BytesMut::from(&b"dropped"[..])); + + assert_eq!( + dc.outstanding_bytes(), + 0, + "a send that never reached SCTP must not be charged" + ); + + Ok(()) +} + +/// Sending on an id that was never registered is a different failure from sending too early, +/// and the two should stay distinguishable — a caller can retry the second but not the first. +#[test] +fn send_on_a_closed_channel_reports_closed() -> Result<()> { + let mut pc = RTCPeerConnectionBuilder::new().build()?; + + let mut dc = pc.create_data_channel("probe", Some(RTCDataChannelInit::default()))?; + let id = dc.id(); + dc.close()?; + + let mut dc = pc.data_channel(id).expect("handle survives close"); + assert_ne!( + dc.send(BytesMut::from(&b"dropped"[..])), + Ok(()), + "send on a closed channel must fail" + ); + + Ok(()) +} diff --git a/tests/data_channels_close_by_rtc_interop.rs b/tests/data_channels_close_by_rtc_interop.rs index 5f5b4e5c..561b2824 100644 --- a/tests/data_channels_close_by_rtc_interop.rs +++ b/tests/data_channels_close_by_rtc_interop.rs @@ -39,11 +39,14 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test data channel close behavior with RTC sending periodic messages and closing #[tokio::test] async fn test_data_channel_close_interop() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/data_channels_close_by_webrtc_interop.rs b/tests/data_channels_close_by_webrtc_interop.rs index 5d4ad6f8..cc0ffe3e 100644 --- a/tests/data_channels_close_by_webrtc_interop.rs +++ b/tests/data_channels_close_by_webrtc_interop.rs @@ -40,11 +40,14 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test data channel close behavior with WebRTC sending periodic messages and closing #[tokio::test] async fn test_data_channel_close_by_webrtc_interop() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -271,6 +274,7 @@ async fn test_data_channel_close_by_webrtc_interop() -> Result<()> { log::info!("RTC received message on channel {}: '{}'", channel_id, data); rtc_received_messages.push(data); } + _ => {} } } diff --git a/tests/data_channels_create_interop.rs b/tests/data_channels_create_interop.rs index 825e6cd4..6cbd0ee1 100644 --- a/tests/data_channels_create_interop.rs +++ b/tests/data_channels_create_interop.rs @@ -41,11 +41,14 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test data channel creation where RTC creates the channel, sends messages, and receives echoes #[tokio::test] async fn test_data_channel_create_rtc_to_webrtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -270,6 +273,7 @@ async fn test_data_channel_create_rtc_to_webrtc() -> Result<()> { let mut rtc_msgs = rtc_received_messages.lock().await; rtc_msgs.push(msg_str.clone()); } + _ => {} } } diff --git a/tests/data_channels_interop.rs b/tests/data_channels_interop.rs index d663d460..dd2e0dec 100644 --- a/tests/data_channels_interop.rs +++ b/tests/data_channels_interop.rs @@ -35,11 +35,14 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test data channel communication between rtc (sansio) and webrtc (async) implementations #[tokio::test] async fn test_data_channel_rtc_to_webrtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -257,6 +260,7 @@ async fn test_data_channel_rtc_to_webrtc() -> Result<()> { log::info!("RTC echoing message back: '{}'", msg_str); dc.send_text(msg_str)?; } + _ => {} } } diff --git a/tests/default_codecs_no_ulpfec.rs b/tests/default_codecs_no_ulpfec.rs new file mode 100644 index 00000000..8b0ac17b --- /dev/null +++ b/tests/default_codecs_no_ulpfec.rs @@ -0,0 +1,41 @@ +//! Issue #837 end-to-end: the generated SDP must carry no ULPFEC rtpmap. +use rtc::peer_connection::RTCPeerConnectionBuilder; +use rtc::peer_connection::configuration::RTCConfigurationBuilder; +use rtc::peer_connection::configuration::media_engine::MediaEngine; +use rtc::rtp_transceiver::rtp_sender::RtpCodecKind; +use rtc::rtp_transceiver::{RTCRtpTransceiverDirection, RTCRtpTransceiverInit}; + +#[test] +fn default_offer_sdp_contains_no_ulpfec() { + let mut me = MediaEngine::default(); + me.register_default_codecs() + .expect("register default codecs"); + + let mut pc = RTCPeerConnectionBuilder::new() + .with_configuration(RTCConfigurationBuilder::new().build()) + .with_media_engine(me) + .build() + .expect("build peer connection"); + pc.add_transceiver_from_kind( + RtpCodecKind::Video, + Some(RTCRtpTransceiverInit { + direction: RTCRtpTransceiverDirection::Recvonly, + ..Default::default() + }), + ) + .expect("add video transceiver"); + + let offer = pc.create_offer(None).expect("create offer"); + let lower = offer.sdp.to_lowercase(); + assert!( + !lower.contains("ulpfec"), + "issue #837: the default offer still advertises ULPFEC:\n{}", + offer.sdp + ); + // sanity: the offer really does carry video codecs, so the assertion above is meaningful + assert!( + lower.contains("vp8"), + "expected a video offer, got:\n{}", + offer.sdp + ); +} diff --git a/tests/dtls_common/mod.rs b/tests/dtls_common/mod.rs new file mode 100644 index 00000000..3bfc3926 --- /dev/null +++ b/tests/dtls_common/mod.rs @@ -0,0 +1,131 @@ +//! Shared helpers for DTLS related integration tests. + +use std::{ + net::SocketAddr, + time::{Duration, Instant}, +}; + +use anyhow::Result; +use bytes::BytesMut; +use rtc::peer_connection::{ + RTCPeerConnection, event::RTCPeerConnectionEvent, state::RTCPeerConnectionState, +}; +use sansio::Protocol; +use shared::{TaggedBytesMut, TransportContext, TransportProtocol}; +use tokio::net::UdpSocket; + +/// Connection timeout used during [`TestPeer::connect`], long enough for a DTLS handshake over +/// loopback, short enough to keep the negative test from dominating the suite. +const CONNECT_TIMEOUT: Duration = Duration::from_secs(10); + +pub trait TestPeer { + /// Mutable reference to the underlying [`RTCPeerConnection`]. + fn pc(&mut self) -> &mut RTCPeerConnection; + /// Reference to the underlying UDP socket. + fn socket(&self) -> &UdpSocket; + /// The local socket address. + fn local_addr(&self) -> SocketAddr; + + /// Drives both peers until each reports `Connected`, or the timeout expires. + /// + /// Returns whether the DTLS handshake completed on both ends. + async fn connect(&mut self, answer: &mut impl TestPeer) -> Result { + let (mut offer_connected, mut answer_connected) = (false, false); + let mut offer_buf = vec![0u8; 2000]; + let mut answer_buf = vec![0u8; 2000]; + let start = Instant::now(); + + while start.elapsed() < CONNECT_TIMEOUT && !(offer_connected && answer_connected) { + while let Some(msg) = self.pc().poll_write() { + self.socket() + .send_to(&msg.message, msg.transport.peer_addr) + .await?; + } + while let Some(event) = self.pc().poll_event() { + if matches!( + event, + RTCPeerConnectionEvent::OnConnectionStateChangeEvent( + RTCPeerConnectionState::Connected + ) + ) { + offer_connected = true; + } + } + + while let Some(msg) = answer.pc().poll_write() { + answer + .socket() + .send_to(&msg.message, msg.transport.peer_addr) + .await?; + } + while let Some(event) = answer.pc().poll_event() { + if matches!( + event, + RTCPeerConnectionEvent::OnConnectionStateChangeEvent( + RTCPeerConnectionState::Connected + ) + ) { + answer_connected = true; + } + } + + let next_timeout = self + .pc() + .poll_timeout() + .unwrap_or_else(|| Instant::now() + CONNECT_TIMEOUT) + .min( + answer + .pc() + .poll_timeout() + .unwrap_or_else(|| Instant::now() + CONNECT_TIMEOUT), + ); + let delay = next_timeout + .saturating_duration_since(Instant::now()) + .min(Duration::from_millis(10)); + + if delay.is_zero() { + self.pc().handle_timeout(Instant::now()).ok(); + answer.pc().handle_timeout(Instant::now()).ok(); + continue; + } + + let sleep = tokio::time::sleep(delay); + tokio::pin!(sleep); + tokio::select! { + _ = sleep => { + self.pc().handle_timeout(Instant::now()).ok(); + answer.pc().handle_timeout(Instant::now()).ok(); + } + Ok((n, peer_addr)) = self.socket().recv_from(&mut offer_buf) => { + let local_addr = self.local_addr(); + self.pc().handle_read(TaggedBytesMut { + now: Instant::now(), + transport: TransportContext { + local_addr, + peer_addr, + ecn: None, + transport_protocol: TransportProtocol::UDP, + }, + message: BytesMut::from(&offer_buf[..n]), + }).ok(); + } + + Ok((n, peer_addr)) = answer.socket().recv_from(&mut answer_buf) => { + let local_addr = answer.local_addr(); + answer.pc().handle_read(TaggedBytesMut { + now: Instant::now(), + transport: TransportContext { + local_addr, + peer_addr, + ecn: None, + transport_protocol: TransportProtocol::UDP, + }, + message: BytesMut::from(&answer_buf[..n]), + }).ok(); + } + } + } + + Ok(offer_connected && answer_connected) + } +} diff --git a/tests/dtls_disable_fingerprint_verification.rs b/tests/dtls_disable_fingerprint_verification.rs new file mode 100644 index 00000000..c51cd17f --- /dev/null +++ b/tests/dtls_disable_fingerprint_verification.rs @@ -0,0 +1,164 @@ +//! Regression test for `SettingEngine::disable_certificate_fingerprint_verification`. +//! +//! The setter existed but the flag was never plumbed into `RTCDtlsTransport`, so the +//! DTLS handshake always installed the fingerprint-matching `verify_peer_certificate` +//! callback and enabling the option had no effect at all. +//! +//! This matters for protocols where the answerer *cannot* know the offerer's +//! fingerprint ahead of time. libp2p's WebRTC-Direct is the canonical example: the +//! server never receives a real offer — it synthesizes one locally from the incoming +//! STUN binding request, filling the `a=fingerprint` line with a placeholder, and +//! authenticates the peer afterwards with a Noise handshake over the data channel. +//! With the flag ignored, that handshake fails with `ErrNoMatchingCertificateFingerprint`. +//! +//! The two tests below pin both directions: with the option enabled a mismatched +//! fingerprint connects, and with it left at the default the same setup still fails. + +use anyhow::Result; +use rtc::peer_connection::configuration::RTCConfigurationBuilder; +use rtc::peer_connection::configuration::setting_engine::SettingEngine; +use rtc::peer_connection::transport::{ + CandidateConfig, CandidateHostConfig, RTCDtlsRole, RTCIceCandidate, +}; +use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; +use rtc::sansio::Protocol; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::net::UdpSocket; + +use crate::dtls_common::TestPeer; + +mod dtls_common; + +/// A fingerprint that matches no certificate, standing in for the placeholder a +/// WebRTC-Direct server puts in the offer it synthesizes for the client. +const PLACEHOLDER_FINGERPRINT: &str = "a=fingerprint:sha-256 \ +FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:\ +FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF:FF"; + +struct Peer { + pc: RTCPeerConnection, + socket: Arc, + local_addr: SocketAddr, +} + +impl Peer { + async fn new(setting_engine: SettingEngine) -> Result { + let socket = UdpSocket::bind("127.0.0.1:0").await?; + let local_addr = socket.local_addr()?; + + let mut pc = RTCPeerConnectionBuilder::new() + .with_configuration(RTCConfigurationBuilder::new().build()) + .with_setting_engine(setting_engine) + .build()?; + + // Host candidate only: the peers talk over loopback, so no STUN is needed. + let candidate = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: local_addr.ip().to_string(), + port: local_addr.port(), + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + pc.add_local_candidate(RTCIceCandidate::from(&candidate).to_json()?)?; + + Ok(Self { + pc, + socket: Arc::new(socket), + local_addr, + }) + } +} + +impl TestPeer for Peer { + fn pc(&mut self) -> &mut RTCPeerConnection { + &mut self.pc + } + + fn socket(&self) -> &UdpSocket { + &self.socket + } + + fn local_addr(&self) -> SocketAddr { + self.local_addr + } +} + +/// Replaces the `a=fingerprint` line so the answerer is told to expect a +/// certificate the offerer will never present. +fn with_placeholder_fingerprint(sdp: &str) -> String { + sdp.lines() + .map(|line| { + if line.starts_with("a=fingerprint:") { + PLACEHOLDER_FINGERPRINT + } else { + line + } + }) + .collect::>() + .join("\r\n") + + "\r\n" +} + +/// Runs the handshake with the answerer given a fingerprint that cannot match. +/// +/// `disable_verification` selects whether the answerer opts out of fingerprint +/// checking; everything else is identical between the two tests. +async fn handshake_with_mismatched_fingerprint(disable_verification: bool) -> Result { + let mut offer_setting_engine = SettingEngine::default(); + offer_setting_engine.set_answering_dtls_role(RTCDtlsRole::Client)?; + let mut offer = Peer::new(offer_setting_engine).await?; + + // The answerer takes the DTLS server role, mirroring a WebRTC-Direct listener. + let mut answer_setting_engine = SettingEngine::default(); + answer_setting_engine.set_answering_dtls_role(RTCDtlsRole::Server)?; + answer_setting_engine.disable_certificate_fingerprint_verification(disable_verification); + let mut answer = Peer::new(answer_setting_engine).await?; + + // A data channel is needed for the m-line that carries the DTLS parameters. + offer.pc.create_data_channel("test", None)?; + + let local_offer = offer.pc.create_offer(None)?; + offer.pc.set_local_description(local_offer.clone())?; + + // The answerer is handed an offer whose fingerprint the offerer cannot satisfy. + let mut tampered = local_offer; + tampered.sdp = with_placeholder_fingerprint(&tampered.sdp); + answer.pc.set_remote_description(tampered)?; + + let local_answer = answer.pc.create_answer(None)?; + answer.pc.set_local_description(local_answer.clone())?; + offer.pc.set_remote_description(local_answer)?; + + let connected = offer.connect(&mut answer).await?; + + offer.pc.close().ok(); + answer.pc.close().ok(); + + Ok(connected) +} + +/// With verification disabled, a mismatched fingerprint must not block the handshake. +#[tokio::test] +async fn disabled_verification_accepts_mismatched_fingerprint() -> Result<()> { + assert!( + handshake_with_mismatched_fingerprint(true).await?, + "DTLS should complete when disable_certificate_fingerprint_verification is set" + ); + Ok(()) +} + +/// The same setup must still fail by default — otherwise the test above would pass +/// even if the option were ignored again. +#[tokio::test] +async fn default_verification_rejects_mismatched_fingerprint() -> Result<()> { + assert!( + !handshake_with_mismatched_fingerprint(false).await?, + "DTLS must reject a certificate that does not match the signaled fingerprint" + ); + Ok(()) +} diff --git a/tests/dtls_rsa_certificate.rs b/tests/dtls_rsa_certificate.rs new file mode 100644 index 00000000..2bd5616e --- /dev/null +++ b/tests/dtls_rsa_certificate.rs @@ -0,0 +1,168 @@ +//! Integration coverage for RSA certificates over DTLS. +//! +//! `ConfigBuilder::validate` used to reject every private key that was not Ed25519 or ECDSA +//! P-256. This restriction has been removed with this test validating expected behavior with +//! RSA keys. +//! +//! The keys are fixtures rather than freshly generated because rcgen cannot generate RSA keys +//! under the `ring` backend. + +use anyhow::Result; +use rtc::crypto::{self, SignatureScheme}; +use rtc::peer_connection::certificate::{CertificateParams, RTCCertificate}; +use rtc::peer_connection::configuration::RTCConfigurationBuilder; +use rtc::peer_connection::transport::{CandidateConfig, CandidateHostConfig, RTCIceCandidate}; +use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; +use rtc::sansio::Protocol; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::net::UdpSocket; + +use crate::dtls_common::TestPeer; + +mod dtls_common; + +const RSA_OFFERER_KEY: &str = include_str!("testdata/rsa_2048_offerer_key.pem"); +const RSA_ANSWERER_KEY: &str = include_str!("testdata/rsa_2048_answerer_key.pem"); + +/// The key type a peer authenticates with. +#[derive(Copy, Clone, Debug)] +enum KeyType { + Rsa2048(&'static str), + EcdsaP256, +} + +impl KeyType { + fn certificate(self) -> Result { + let provider = crypto::default_provider()?; + let params = CertificateParams::new(vec!["webrtc.rs".to_owned()])?; + + Ok(match self { + // The RSA key is a fixture, so it is imported rather than generated and then + // wrapped in a fresh self-signed certificate. + KeyType::Rsa2048(pem) => { + let der = pem::parse(pem)?.into_contents(); + let signing_key = provider + .crypto() + .import_signing_key(SignatureScheme::RsaPkcs1Sha256, &der)?; + RTCCertificate::generate_from_signing_key( + params, + SignatureScheme::RsaPkcs1Sha256, + signing_key, + )? + } + KeyType::EcdsaP256 => RTCCertificate::generate( + provider.crypto(), + SignatureScheme::EcdsaP256Sha256, + params, + )?, + }) + } +} + +struct Peer { + pc: RTCPeerConnection, + socket: Arc, + local_addr: SocketAddr, +} + +impl TestPeer for Peer { + fn pc(&mut self) -> &mut RTCPeerConnection { + &mut self.pc + } + + fn socket(&self) -> &UdpSocket { + &self.socket + } + + fn local_addr(&self) -> SocketAddr { + self.local_addr + } +} + +impl Peer { + async fn new(key_type: KeyType) -> Result { + let socket = UdpSocket::bind("127.0.0.1:0").await?; + let local_addr = socket.local_addr()?; + + let mut pc = RTCPeerConnectionBuilder::new() + .with_configuration( + RTCConfigurationBuilder::new() + .with_certificates(vec![key_type.certificate()?]) + .build(), + ) + .build()?; + + // Host candidate only: the peers talk over loopback, so no STUN is needed. + let candidate = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: local_addr.ip().to_string(), + port: local_addr.port(), + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + pc.add_local_candidate(RTCIceCandidate::from(&candidate).to_json()?)?; + + Ok(Self { + pc, + socket: Arc::new(socket), + local_addr, + }) + } +} + +/// Negotiates a connection between two peers with the given certificate key types. +async fn handshake_between(offer_key: KeyType, answer_key: KeyType) -> Result { + let mut offer = Peer::new(offer_key).await?; + let mut answer = Peer::new(answer_key).await?; + + // A data channel is needed for the m-line that carries the DTLS parameters. + offer.pc.create_data_channel("test", None)?; + + let local_offer = offer.pc.create_offer(None)?; + offer.pc.set_local_description(local_offer.clone())?; + answer.pc.set_remote_description(local_offer)?; + + let local_answer = answer.pc.create_answer(None)?; + offer.pc.set_remote_description(local_answer.clone())?; + answer.pc.set_local_description(local_answer)?; + + let connected = offer.connect(&mut answer).await?; + + offer.pc.close().ok(); + answer.pc.close().ok(); + + Ok(connected) +} + +/// An RSA certificate on both ends must complete the handshake. +#[tokio::test] +async fn rsa_certificates_complete_dtls_handshake() -> Result<()> { + assert!( + handshake_between( + KeyType::Rsa2048(RSA_OFFERER_KEY), + KeyType::Rsa2048(RSA_ANSWERER_KEY) + ) + .await?, + "DTLS should complete when both peers authenticate with an RSA certificate" + ); + Ok(()) +} + +/// Mixed private key case must complete the handshake. +#[tokio::test] +async fn rsa_certificate_interoperates_with_ecdsa_peer() -> Result<()> { + assert!( + handshake_between(KeyType::Rsa2048(RSA_OFFERER_KEY), KeyType::EcdsaP256).await?, + "DTLS should complete with an RSA offerer and an ECDSA answerer" + ); + assert!( + handshake_between(KeyType::EcdsaP256, KeyType::Rsa2048(RSA_ANSWERER_KEY)).await?, + "DTLS should complete with an ECDSA offerer and an RSA answerer" + ); + Ok(()) +} diff --git a/tests/ice_restart_by_rtc_interop.rs b/tests/ice_restart_by_rtc_interop.rs index 851bf9a2..d6502734 100644 --- a/tests/ice_restart_by_rtc_interop.rs +++ b/tests/ice_restart_by_rtc_interop.rs @@ -33,12 +33,15 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); const TEST_MESSAGE: &str = "Hello before restart!"; const TEST_MESSAGE_AFTER_RESTART: &str = "Hello after restart!"; #[tokio::test] async fn test_ice_restart_by_rtc_interop() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/ice_restart_by_webrtc_interop.rs b/tests/ice_restart_by_webrtc_interop.rs index 102cd9f5..f4834f47 100644 --- a/tests/ice_restart_by_webrtc_interop.rs +++ b/tests/ice_restart_by_webrtc_interop.rs @@ -35,6 +35,8 @@ use webrtc::peer_connection::offer_answer_options::RTCOfferOptions; use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); const TEST_MESSAGE_1: &str = "Hello before restart!"; const TEST_MESSAGE_2: &str = "Hello after restart!"; @@ -69,6 +71,7 @@ async fn create_webrtc_peer() -> Result> { /// Test ICE restart between webrtc (offerer) and rtc (answerer) #[tokio::test] async fn test_ice_restart_interop() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -240,15 +243,12 @@ async fn test_ice_restart_interop() -> Result<()> { rtc_connected = true; } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - if let Some(dc) = rtc_pc.data_channel(channel_id) { - log::info!("RTC data channel '{}'-'{}' opened", dc.label(), dc.id()); - rtc_dc_id = Some(channel_id); - } + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { + if let Some(dc) = rtc_pc.data_channel(channel_id) { + log::info!("RTC data channel '{}'-'{}' opened", dc.label(), dc.id()); + rtc_dc_id = Some(channel_id); } - _ => {} - }, + } _ => {} } } @@ -262,6 +262,7 @@ async fn test_ice_restart_interop() -> Result<()> { log::info!("RTC received message: {}", msg_str); rtc_received_messages.lock().await.push(msg_str.to_string()); } + _ => {} } } @@ -389,6 +390,7 @@ async fn test_ice_restart_interop() -> Result<()> { log::info!("RTC received message: {}", msg_str); rtc_received_messages.lock().await.push(msg_str.to_string()); } + _ => {} } } @@ -549,6 +551,7 @@ async fn test_ice_restart_interop() -> Result<()> { log::info!("RTC received message after restart: {}", msg_str); rtc_received_messages.lock().await.push(msg_str.to_string()); } + _ => {} } } @@ -621,6 +624,7 @@ async fn test_ice_restart_interop() -> Result<()> { log::info!("RTC received message after restart: {}", msg_str); rtc_received_messages.lock().await.push(msg_str.to_string()); } + _ => {} } } diff --git a/tests/interceptor_rtcp_reports_interop.rs b/tests/interceptor_rtcp_reports_interop.rs index 4d6810d6..25c39c86 100644 --- a/tests/interceptor_rtcp_reports_interop.rs +++ b/tests/interceptor_rtcp_reports_interop.rs @@ -49,12 +49,15 @@ use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; use webrtc::track::track_remote::TrackRemote; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test that custom interceptor registry with SenderReportBuilder and ReceiverReportBuilder /// can be used with RTCConfigurationBuilder. #[tokio::test] async fn test_custom_interceptor_registry_with_rtcp_reports() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -386,6 +389,7 @@ async fn test_custom_interceptor_registry_with_rtcp_reports() -> Result<()> { /// This test monitors the outgoing packets to verify SR generation. #[tokio::test] async fn test_sender_report_generation_on_rtp_send() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Debug) .is_test(true) @@ -672,6 +676,7 @@ async fn test_sender_report_generation_on_rtp_send() -> Result<()> { /// Test that using register_default_interceptors helper function works correctly. #[tokio::test] async fn test_register_default_interceptors_helper() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/mdns_query_and_gather_interop.rs b/tests/mdns_query_and_gather_interop.rs index 6eec2377..03403393 100644 --- a/tests/mdns_query_and_gather_interop.rs +++ b/tests/mdns_query_and_gather_interop.rs @@ -46,6 +46,8 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); const MDNS_LOCAL_NAME: &str = "webrtc-rs-test-mdns.local"; const WEBRTC_MDNS_LOCAL_NAME: &str = "webrtc-peer-mdns.local"; @@ -287,6 +289,7 @@ async fn run_rtc_event_loop( /// advertise its own IP via mDNS. #[tokio::test] async fn test_mdns_query_only_webrtc_offerer_rtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -452,6 +455,7 @@ async fn test_mdns_query_only_webrtc_offerer_rtc_answerer() -> Result<()> { /// advertises its own IP via an mDNS name. #[tokio::test] async fn test_mdns_query_and_gather_webrtc_offerer_rtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -605,6 +609,7 @@ async fn test_mdns_query_and_gather_webrtc_offerer_rtc_answerer() -> Result<()> /// Test mDNS QueryOnly mode: sansio RTC as offerer, webrtc as answerer #[tokio::test] async fn test_mdns_query_only_rtc_offerer_webrtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -779,6 +784,7 @@ async fn test_mdns_query_only_rtc_offerer_webrtc_answerer() -> Result<()> { /// Test mDNS QueryAndGather mode: sansio RTC as offerer, webrtc as answerer #[tokio::test] async fn test_mdns_query_and_gather_rtc_offerer_webrtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/media_only_negotiation_no_sctp.rs b/tests/media_only_negotiation_no_sctp.rs index 961845ba..feb8669e 100644 --- a/tests/media_only_negotiation_no_sctp.rs +++ b/tests/media_only_negotiation_no_sctp.rs @@ -180,10 +180,10 @@ async fn test_media_only_negotiation_does_not_start_sctp() -> Result<()> { while let Some(event) = offerer_pc.poll_event() { match event { - RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) => { - if state == RTCIceConnectionState::Failed { - return Err(anyhow::anyhow!("offerer ICE connection failed")); - } + RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) + if state == RTCIceConnectionState::Failed => + { + return Err(anyhow::anyhow!("offerer ICE connection failed")); } RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { if state == RTCPeerConnectionState::Failed { @@ -202,10 +202,10 @@ async fn test_media_only_negotiation_does_not_start_sctp() -> Result<()> { while let Some(event) = answerer_pc.poll_event() { match event { - RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) => { - if state == RTCIceConnectionState::Failed { - return Err(anyhow::anyhow!("answerer ICE connection failed")); - } + RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) + if state == RTCIceConnectionState::Failed => + { + return Err(anyhow::anyhow!("answerer ICE connection failed")); } RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { if state == RTCPeerConnectionState::Failed { @@ -240,6 +240,7 @@ async fn test_media_only_negotiation_does_not_start_sctp() -> Result<()> { unexpected_data_channel_messages += 1; } RTCMessage::RtcpPacket(_, _) => {} + _ => {} } } diff --git a/tests/media_rejection_interop.rs b/tests/media_rejection_interop.rs index 3b5d3715..c59a9d72 100644 --- a/tests/media_rejection_interop.rs +++ b/tests/media_rejection_interop.rs @@ -51,6 +51,8 @@ use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); // ============================================================================ @@ -128,6 +130,7 @@ fn create_rtc_peer_config_video_only() /// - Video RTP packets are received successfully #[tokio::test] async fn test_video_only_webrtc_offerer_rtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -415,6 +418,7 @@ async fn test_video_only_webrtc_offerer_rtc_answerer() -> Result<()> { /// sansio RTC correctly rejects audio (port=0) while accepting video. #[tokio::test] async fn test_sdp_answer_rejects_audio_correctly() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/no_builtin_crypto_provider.rs b/tests/no_builtin_crypto_provider.rs new file mode 100644 index 00000000..8bd6593f --- /dev/null +++ b/tests/no_builtin_crypto_provider.rs @@ -0,0 +1,20 @@ +#![cfg(not(any(feature = "crypto-ring", feature = "crypto-aws-lc-rs")))] + +use rtc::peer_connection::RTCPeerConnectionBuilder; + +#[test] +fn peer_connection_without_a_provider_returns_actionable_error() { + let error = match RTCPeerConnectionBuilder::new().build() { + Ok(_) => panic!("a no-built-in build must require an application crypto provider"), + Err(error) => error, + }; + let message = error.to_string(); + assert!( + message.contains("crypto provider"), + "unexpected error: {message}" + ); + assert!( + message.contains("SettingEngine::set_crypto_provider"), + "error must explain how to configure a provider: {message}" + ); +} diff --git a/tests/offer_answer_rtc2rtc.rs b/tests/offer_answer_rtc2rtc.rs index 7dd99d29..f4126c37 100644 --- a/tests/offer_answer_rtc2rtc.rs +++ b/tests/offer_answer_rtc2rtc.rs @@ -215,13 +215,10 @@ async fn test_offer_answer_rtc_to_rtc() -> Result<()> { offer_connected = true; } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - log::info!("Offer data channel {} opened", channel_id); - offer_dc_id = Some(channel_id); - } - _ => {} - }, + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { + log::info!("Offer data channel {} opened", channel_id); + offer_dc_id = Some(channel_id); + } _ => {} } } @@ -236,6 +233,7 @@ async fn test_offer_answer_rtc_to_rtc() -> Result<()> { let mut msgs = offer_received_messages.lock().await; msgs.push(msg_str); } + _ => {} } } @@ -272,12 +270,9 @@ async fn test_offer_answer_rtc_to_rtc() -> Result<()> { answer_connected = true; } } - RTCPeerConnectionEvent::OnDataChannel(dc_event) => match dc_event { - RTCDataChannelEvent::OnOpen(channel_id) => { - log::info!("Answer data channel {} opened", channel_id); - } - _ => {} - }, + RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { + log::info!("Answer data channel {} opened", channel_id); + } _ => {} } } @@ -301,6 +296,7 @@ async fn test_offer_answer_rtc_to_rtc() -> Result<()> { } } } + _ => {} } } diff --git a/tests/one_media_section_rtc_to_rtc_simulcast.rs b/tests/one_media_section_rtc_to_rtc_simulcast.rs index b5355cc4..25e859c6 100644 --- a/tests/one_media_section_rtc_to_rtc_simulcast.rs +++ b/tests/one_media_section_rtc_to_rtc_simulcast.rs @@ -402,6 +402,7 @@ async fn test_one_media_section_rtc_to_rtc_simulcast() -> Result<()> { // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/one_media_section_rtc_to_rtc_unicast.rs b/tests/one_media_section_rtc_to_rtc_unicast.rs index 7f699ebc..961373fd 100644 --- a/tests/one_media_section_rtc_to_rtc_unicast.rs +++ b/tests/one_media_section_rtc_to_rtc_unicast.rs @@ -342,6 +342,7 @@ async fn test_one_media_section_rtc_to_rtc_unicast() -> Result<()> { // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/play_from_disk_rtc_set_remote_before_add_track_interop.rs b/tests/play_from_disk_rtc_set_remote_before_add_track_interop.rs index d56d7570..3504eb33 100644 --- a/tests/play_from_disk_rtc_set_remote_before_add_track_interop.rs +++ b/tests/play_from_disk_rtc_set_remote_before_add_track_interop.rs @@ -53,6 +53,8 @@ use webrtc::rtp_transceiver::rtp_codec::RTPCodecType; use webrtc::rtp_transceiver::rtp_transceiver_direction::RTCRtpTransceiverDirection; use webrtc::track::track_remote::TrackRemote; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); const OGG_PAGE_DURATION: Duration = Duration::from_millis(20); const RTP_OUTBOUND_MTU: usize = 1200; @@ -60,6 +62,7 @@ const RTP_OUTBOUND_MTU: usize = 1200; /// Test webrtc as RecvOnly offerer, rtc as answerer streaming from disk #[tokio::test] async fn test_play_from_disk_rtc_set_remote_before_add_track() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/play_from_disk_vpx_interop.rs b/tests/play_from_disk_vpx_interop.rs index e4f75eea..5285249e 100644 --- a/tests/play_from_disk_vpx_interop.rs +++ b/tests/play_from_disk_vpx_interop.rs @@ -55,6 +55,8 @@ use webrtc::rtp_transceiver::rtp_codec::RTPCodecType; use webrtc::rtp_transceiver::rtp_transceiver_direction::RTCRtpTransceiverDirection; use webrtc::track::track_remote::TrackRemote; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); const OGG_PAGE_DURATION: Duration = Duration::from_millis(20); const RTP_OUTBOUND_MTU: usize = 1200; @@ -62,6 +64,7 @@ const RTP_OUTBOUND_MTU: usize = 1200; /// Test streaming media from disk: rtc streams from disk -> webrtc receives #[tokio::test] async fn test_play_from_disk_vpx_rtc_to_webrtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/reflect_rtc_to_webrtc_interop.rs b/tests/reflect_rtc_to_webrtc_interop.rs index b9d5e875..a95be8d1 100644 --- a/tests/reflect_rtc_to_webrtc_interop.rs +++ b/tests/reflect_rtc_to_webrtc_interop.rs @@ -51,11 +51,14 @@ use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; use webrtc::track::track_remote::TrackRemote; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test reflect functionality: rtc sends RTP -> webrtc reflects -> rtc receives #[tokio::test] async fn test_reflect_rtc_to_webrtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -320,6 +323,7 @@ async fn test_reflect_rtc_to_webrtc() -> Result<()> { // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/reflect_webrtc_to_rtc_interop.rs b/tests/reflect_webrtc_to_rtc_interop.rs index fc656609..07fb6e4d 100644 --- a/tests/reflect_webrtc_to_rtc_interop.rs +++ b/tests/reflect_webrtc_to_rtc_interop.rs @@ -45,11 +45,14 @@ use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; use webrtc::track::track_remote::TrackRemote; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test reflect functionality: webrtc sends RTP -> rtc reflects -> webrtc receives #[tokio::test] async fn test_reflect_webrtc_to_rtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -375,6 +378,7 @@ async fn test_reflect_webrtc_to_rtc() -> Result<()> { // like NACK this needs to be called. } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/rtcp_processing_boxed_interop.rs b/tests/rtcp_processing_boxed_interop.rs new file mode 100644 index 00000000..f48592b2 --- /dev/null +++ b/tests/rtcp_processing_boxed_interop.rs @@ -0,0 +1,841 @@ +//! Integration tests for RTCP packet processing with a **type-erased** interceptor chain. +//! +//! This is the `RTCPeerConnection` counterpart of +//! `rtcp_processing_interop.rs`. Both files install the same custom +//! `RtcpForwarderInterceptor`; the difference is how the resulting peer connection is +//! *typed*, and therefore what the application can do with it. +//! +//! `rtcp_processing_interop.rs` returns `RTCPeerConnection`. That works +//! as long as the chain flows straight into a local variable, but the opaque type is +//! chosen once per return site, so it cannot: +//! +//! - be stored in a plain (non-generic) application struct, +//! - be put in a `Vec`/`HashMap` alongside peers built with a *different* chain, +//! - be produced by two different branches of an `if` in the same function. +//! +//! Erasing the chain to [`BoxedInterceptor`] removes all three limits: every peer +//! connection has the one concrete type `RTCPeerConnection` regardless +//! of how its chain was assembled at runtime. That is what lets [`RtcpPeer`] below — an +//! ordinary struct with no type parameters — own a peer connection, and what lets +//! `test_boxed_rtc_to_rtc_heterogeneous_chains` drive two peers with *different* chains +//! out of a single `Vec`. +//! +//! Test scenarios: +//! 1. webrtc (offerer sending video) + boxed-chain RTC (answerer receiving RTCP) +//! 2. boxed-chain RTC (sender) receives RTCP feedback about its own stream from webrtc +//! 3. two boxed-chain RTC peers with *different* chains, driven from one `Vec` + +use anyhow::Result; +use bytes::BytesMut; +use sansio::Protocol; +use shared::{TaggedBytesMut, TransportContext, TransportProtocol}; +use std::collections::VecDeque; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::net::UdpSocket; +use tokio::time::timeout; + +use rtc::interceptor::{ + BoxedInterceptor, Interceptor, Packet, Registry, StreamInfo, TaggedPacket, interceptor, +}; +use rtc::media_stream::MediaStreamTrack; +use rtc::peer_connection::configuration::RTCConfigurationBuilder; +use rtc::peer_connection::configuration::interceptor_registry::register_default_interceptors; +use rtc::peer_connection::configuration::media_engine::{MIME_TYPE_VP8, MediaEngine}; +use rtc::peer_connection::configuration::setting_engine::SettingEngine; +use rtc::peer_connection::event::{RTCPeerConnectionEvent, RTCTrackEvent}; +use rtc::peer_connection::message::RTCMessage; +use rtc::peer_connection::state::{RTCIceConnectionState, RTCPeerConnectionState}; +use rtc::peer_connection::transport::{ + CandidateConfig, CandidateHostConfig, RTCDtlsRole, RTCIceCandidate, RTCIceServer, +}; +use rtc::peer_connection::{RTCPeerConnection, RTCPeerConnectionBuilder}; +use rtc::rtp_transceiver::rtp_sender::{ + RTCRtpCodec, RTCRtpCodecParameters, RTCRtpCodingParameters, RTCRtpEncodingParameters, + RtpCodecKind, +}; +use rtc::shared::error::Error; + +use webrtc::api::APIBuilder; +use webrtc::api::interceptor_registry::register_default_interceptors as webrtc_register_default_interceptors; +use webrtc::api::media_engine::MediaEngine as WebrtcMediaEngine; +use webrtc::ice_transport::ice_server::RTCIceServer as WebrtcIceServer; +use webrtc::interceptor::registry::Registry as WebrtcRegistry; +use webrtc::peer_connection::RTCPeerConnection as WebrtcPeerConnection; +use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfiguration; +use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; +use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; +use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; +use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; + +mod common; + +const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); + +// ============================================================================ +// RTCP Forwarder Interceptor +// ============================================================================ + +/// Builder for the RtcpForwarderInterceptor. +pub struct RtcpForwarderBuilder

{ + _phantom: std::marker::PhantomData

, +} + +impl

Default for RtcpForwarderBuilder

{ + fn default() -> Self { + Self { + _phantom: std::marker::PhantomData, + } + } +} + +impl

RtcpForwarderBuilder

{ + pub fn new() -> Self { + Self::default() + } + + pub fn build(self) -> impl FnOnce(P) -> RtcpForwarderInterceptor

{ + move |inner| RtcpForwarderInterceptor::new(inner) + } +} + +/// Interceptor that forwards RTCP packets to the application via poll_read(). +#[derive(Interceptor)] +pub struct RtcpForwarderInterceptor

{ + #[next] + next: P, + read_queue: VecDeque, +} + +impl

RtcpForwarderInterceptor

{ + fn new(next: P) -> Self { + Self { + next, + read_queue: VecDeque::new(), + } + } +} + +#[interceptor] +impl RtcpForwarderInterceptor

{ + #[overrides] + fn handle_read(&mut self, msg: TaggedPacket) -> Result<(), Self::Error> { + // If this is an RTCP packet, queue a copy for the application + if let Packet::Rtcp(rtcp_packets) = &msg.message { + self.read_queue.push_back(TaggedPacket { + now: msg.now, + transport: msg.transport, + message: Packet::Rtcp(rtcp_packets.clone()), + }); + } + // Always pass to next interceptor for normal processing + self.next.handle_read(msg) + } + + #[overrides] + fn poll_read(&mut self) -> Option { + // First return any queued RTCP packets + if let Some(pkt) = self.read_queue.pop_front() { + return Some(pkt); + } + // Then check next interceptor + self.next.poll_read() + } + + #[overrides] + fn close(&mut self) -> Result<(), Self::Error> { + self.read_queue.clear(); + self.next.close() + } +} + +// ============================================================================ +// Building a peer connection whose interceptor chain is chosen at runtime +// ============================================================================ + +/// Build a peer connection whose chain is decided at *runtime* by `forward_rtcp`. +/// +/// This is the function the `impl Interceptor` form cannot express. The two branches +/// build chains of different Rust types — +/// `RtcpForwarderInterceptor>>` versus +/// `TwccReceiver>` — so there is no single `impl Interceptor` they can +/// both satisfy, and `if`/`else` arms must agree on a type. [`Registry::boxed`] erases +/// both to [`BoxedInterceptor`], after which the branches unify and the function has one +/// ordinary return type. +fn build_boxed_rtc_peer( + forward_rtcp: bool, + is_answerer: bool, +) -> Result> { + let mut setting_engine = SettingEngine::default(); + if is_answerer { + setting_engine.set_answering_dtls_role(RTCDtlsRole::Client)?; + } + + let mut media_engine = MediaEngine::default(); + let video_codec = RTCRtpCodecParameters { + rtp_codec: RTCRtpCodec { + mime_type: MIME_TYPE_VP8.to_owned(), + clock_rate: 90000, + channels: 0, + sdp_fmtp_line: "".to_owned(), + rtcp_feedback: vec![], + }, + payload_type: 96, + }; + media_engine.register_codec(video_codec, RtpCodecKind::Video)?; + + // Default interceptors (NACK, RTCP reports, TWCC receiver) in both cases. + let registry = register_default_interceptors(Registry::new(), &mut media_engine)?; + + // The RTCP forwarder is layered on only for peers that want to see RTCP in + // `poll_read`. Both arms are erased to the same `Registry`. + let registry = if forward_rtcp { + registry.with(RtcpForwarderBuilder::new().build()).boxed() + } else { + registry.boxed() + }; + + let config = RTCConfigurationBuilder::new() + .with_ice_servers(vec![RTCIceServer { + urls: vec!["stun:stun.l.google.com:19302".to_owned()], + ..Default::default() + }]) + .build(); + + let pc = RTCPeerConnectionBuilder::new() + .with_configuration(config) + .with_setting_engine(setting_engine) + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .build()?; + Ok(pc) +} + +// ============================================================================ +// A non-generic peer holder +// ============================================================================ + +/// An application-level peer: socket, peer connection, and the counters the tests assert +/// on. +/// +/// Note that this struct has **no type parameter**. With `RTCPeerConnection` it would +/// have needed one (`RtcpPeer`), and that parameter would then infect +/// every function, collection, and struct that touches an `RtcpPeer` — which is exactly +/// the boilerplate erasure removes. +struct RtcpPeer { + name: &'static str, + pc: RTCPeerConnection, + socket: Arc, + local_addr: SocketAddr, + connected: bool, + rtp_received: u32, + rtcp_received: u32, +} + +impl RtcpPeer { + /// Bind a socket, build the peer connection, and advertise the socket as a host + /// candidate. + async fn new(name: &'static str, forward_rtcp: bool, is_answerer: bool) -> Result { + let socket = UdpSocket::bind("127.0.0.1:0").await?; + let local_addr = socket.local_addr()?; + log::info!("{} bound to {}", name, local_addr); + + let mut pc = build_boxed_rtc_peer(forward_rtcp, is_answerer)?; + + let candidate = CandidateHostConfig { + base_config: CandidateConfig { + network: "udp".to_owned(), + address: local_addr.ip().to_string(), + port: local_addr.port(), + component: 1, + ..Default::default() + }, + ..Default::default() + } + .new_candidate_host()?; + pc.add_local_candidate(RTCIceCandidate::from(&candidate).to_json()?)?; + + Ok(Self { + name, + pc, + socket: Arc::new(socket), + local_addr, + connected: false, + rtp_received: 0, + rtcp_received: 0, + }) + } + + /// Send everything the peer connection wants to put on the wire. + async fn flush_writes(&mut self) { + while let Some(msg) = self.pc.poll_write() { + // Ignore send errors - some candidate addresses may be unreachable. + let _ = self + .socket + .send_to(&msg.message, msg.transport.peer_addr) + .await; + } + } + + /// Drain connection/track events, tracking the connected state. + fn drain_events(&mut self) -> Result<()> { + while let Some(event) = self.pc.poll_event() { + match event { + RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) => { + log::info!("{} ICE state: {}", self.name, state); + if state == RTCIceConnectionState::Failed { + return Err(anyhow::anyhow!("{} ICE connection failed", self.name)); + } + } + RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { + log::info!("{} connection state: {}", self.name, state); + if state == RTCPeerConnectionState::Connected { + self.connected = true; + } + } + RTCPeerConnectionEvent::OnTrack(RTCTrackEvent::OnOpen(init)) => { + log::info!("{} track opened: {}", self.name, init.track_id); + } + _ => {} + } + } + Ok(()) + } + + /// Drain RTP/RTCP surfaced to the application, counting both. + /// + /// RTCP only ever appears here for peers built with `forward_rtcp: true` — the + /// default chain consumes it. That asymmetry is what test 3 asserts on. + fn drain_reads(&mut self) { + while let Some(message) = self.pc.poll_read() { + match message { + RTCMessage::RtpPacket(_track_id, rtp_packet) => { + self.rtp_received += 1; + if self.rtp_received.is_multiple_of(10) { + log::info!( + "{} received RTP packet #{} (seq: {})", + self.name, + self.rtp_received, + rtp_packet.header.sequence_number + ); + } + } + RTCMessage::RtcpPacket(track_id, rtcp_packets) => { + self.rtcp_received += 1; + log::info!( + "{} received RTCP packet #{} (track: {}, {} sub-packets)", + self.name, + self.rtcp_received, + track_id, + rtcp_packets.len() + ); + for (i, packet) in rtcp_packets.iter().enumerate() { + let header = packet.header(); + log::info!( + " [{}] Type: {:?}, Length: {} words", + i + 1, + header.packet_type, + header.length + ); + } + } + RTCMessage::DataChannelMessage(_, _) => {} + _ => {} + } + } + } + + /// The peer connection's next deadline, or a far-future default. + fn next_timeout(&mut self) -> Instant { + self.pc + .poll_timeout() + .unwrap_or(Instant::now() + DEFAULT_TIMEOUT_DURATION) + } + + /// Feed a datagram read off this peer's socket into the peer connection. + fn handle_datagram(&mut self, data: &[u8], peer_addr: SocketAddr) -> Result<()> { + self.pc.handle_read(TaggedBytesMut { + now: Instant::now(), + transport: TransportContext { + local_addr: self.local_addr, + peer_addr, + ecn: None, + transport_protocol: TransportProtocol::UDP, + }, + message: BytesMut::from(data), + })?; + Ok(()) + } +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/// Create a webrtc peer connection +async fn create_webrtc_peer() -> Result> { + let mut media_engine = WebrtcMediaEngine::default(); + media_engine.register_default_codecs()?; + + let mut registry = WebrtcRegistry::new(); + registry = webrtc_register_default_interceptors(registry, &mut media_engine)?; + + let api = APIBuilder::new() + .with_media_engine(media_engine) + .with_interceptor_registry(registry) + .build(); + + let config = WebrtcRTCConfiguration { + ice_servers: vec![WebrtcIceServer { + urls: vec!["stun:stun.l.google.com:19302".to_owned()], + ..Default::default() + }], + ..Default::default() + }; + + Ok(Arc::new(api.new_peer_connection(config).await?)) +} + +/// A VP8 track carrying a single known SSRC. +fn video_track(stream_id: &str, track_id: &str, ssrc: u32) -> MediaStreamTrack { + MediaStreamTrack::new( + stream_id.to_owned(), + track_id.to_owned(), + format!("{track_id}-label"), + RtpCodecKind::Video, + vec![RTCRtpEncodingParameters { + rtp_coding_parameters: RTCRtpCodingParameters { + ssrc: Some(ssrc), + ..Default::default() + }, + codec: RTCRtpCodec { + mime_type: MIME_TYPE_VP8.to_owned(), + clock_rate: 90000, + channels: 0, + sdp_fmtp_line: "".to_owned(), + rtcp_feedback: vec![], + }, + ..Default::default() + }], + ) +} + +/// A dummy VP8 RTP packet for `ssrc` with the given sequence number. +fn dummy_rtp(ssrc: u32, seq: u32, payload_type: u8) -> rtc::rtp::packet::Packet { + rtc::rtp::packet::Packet { + header: rtc::rtp::header::Header { + version: 2, + payload_type, + sequence_number: seq as u16, + timestamp: seq.wrapping_mul(3000), + ssrc, + ..Default::default() + }, + payload: bytes::Bytes::from(vec![0xAAu8; 100]), + } +} + +// ============================================================================ +// Test 1: webrtc offerer sends video, boxed-chain RTC answerer receives RTCP +// ============================================================================ + +/// The boxed counterpart of `test_rtcp_processing_webrtc_offerer_rtc_answerer`. +/// +/// Verifies that erasing the chain changes nothing observable: the custom forwarder still +/// surfaces RTCP via `poll_read()`, and the default interceptors still process RTP. +#[tokio::test] +async fn test_boxed_rtcp_processing_webrtc_offerer_rtc_answerer() -> Result<()> { + common::install_crypto_provider(); + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .is_test(true) + .try_init() + .ok(); + + log::info!("Starting boxed RTCP processing test: webrtc (offerer) -> boxed RTC (answerer)"); + + // Create webrtc peer (offerer) with video track + let webrtc_pc = create_webrtc_peer().await?; + + let track = Arc::new(TrackLocalStaticRTP::new( + RTCRtpCodecCapability { + mime_type: "video/VP8".to_owned(), + clock_rate: 90000, + channels: 0, + sdp_fmtp_line: "".to_owned(), + rtcp_feedback: vec![], + }, + "video".to_owned(), + "boxed-rtcp-test-stream".to_owned(), + )); + webrtc_pc + .add_track(Arc::clone(&track) as Arc) + .await?; + + let offer = webrtc_pc.create_offer(None).await?; + webrtc_pc.set_local_description(offer).await?; + let mut gathering_done = webrtc_pc.gathering_complete_promise().await; + let _ = timeout(Duration::from_secs(5), gathering_done.recv()).await; + let offer_with_candidates = webrtc_pc + .local_description() + .await + .expect("local description should be set"); + + // The RTC answerer: one concrete type, held by an ordinary non-generic struct. + let mut peer = RtcpPeer::new("boxed-answerer", true, true).await?; + + let rtc_offer = + rtc::peer_connection::sdp::RTCSessionDescription::offer(offer_with_candidates.sdp.clone())?; + peer.pc.set_remote_description(rtc_offer)?; + + let answer = peer.pc.create_answer(None)?; + peer.pc.set_local_description(answer.clone())?; + + let webrtc_answer = WebrtcRTCSessionDescription::answer(answer.sdp.clone())?; + webrtc_pc.set_remote_description(webrtc_answer).await?; + + // Event loop + let mut buf = vec![0u8; 2000]; + let mut webrtc_connected = false; + let mut rtp_sending_started = false; + + let start_time = Instant::now(); + let test_timeout = Duration::from_secs(30); + + while start_time.elapsed() < test_timeout { + // Start sending RTP once webrtc is connected + if webrtc_connected && !rtp_sending_started { + rtp_sending_started = true; + log::info!("WebRTC connected, starting to send RTP packets"); + let track = Arc::clone(&track); + tokio::spawn(async move { + for seq in 0u16..50 { + let rtp = webrtc::rtp::packet::Packet { + header: webrtc::rtp::header::Header { + version: 2, + payload_type: 96, + sequence_number: seq, + timestamp: seq as u32 * 3000, + ssrc: 12345, + ..Default::default() + }, + payload: bytes::Bytes::from(vec![0xAAu8; 100]), + }; + let _ = track.write_rtp(&rtp).await; + tokio::time::sleep(Duration::from_millis(20)).await; + } + }); + } + + peer.flush_writes().await; + peer.drain_events()?; + peer.drain_reads(); + + if !webrtc_connected + && webrtc_pc.connection_state() == WebrtcRTCPeerConnectionState::Connected + { + webrtc_connected = true; + log::info!("WebRTC peer connected!"); + } + + // Success: the boxed chain surfaced RTCP and passed RTP through. + if peer.rtcp_received >= 2 && peer.rtp_received >= 10 { + log::info!( + "Test passed! RTP received: {}, RTCP received: {}", + peer.rtp_received, + peer.rtcp_received + ); + peer.pc.close()?; + webrtc_pc.close().await?; + return Ok(()); + } + + let delay_from_now = peer + .next_timeout() + .checked_duration_since(Instant::now()) + .unwrap_or(Duration::from_secs(0)); + if delay_from_now.is_zero() { + peer.pc.handle_timeout(Instant::now())?; + continue; + } + + let timer = tokio::time::sleep(delay_from_now.min(Duration::from_millis(10))); + tokio::pin!(timer); + let socket = Arc::clone(&peer.socket); + + tokio::select! { + _ = timer.as_mut() => { + peer.pc.handle_timeout(Instant::now())?; + } + res = socket.recv_from(&mut buf) => { + if let Ok((n, peer_addr)) = res { + peer.handle_datagram(&buf[..n], peer_addr)?; + } + } + } + } + + Err(anyhow::anyhow!( + "Test timeout - RTP: {}, RTCP: {}", + peer.rtp_received, + peer.rtcp_received + )) +} + +// ============================================================================ +// Test 2: boxed-chain RTC sender receives RTCP feedback about its OWN stream +// ============================================================================ + +/// The boxed counterpart of `test_rtcp_processing_rtc_sender_receives_feedback`. +/// +/// The RTC peer *sends* video; webrtc receives it and reports back. The feedback's media +/// SSRC is the RTC peer's sender SSRC, so it surfaces tagged with the sender's track id — +/// exactly what an SFU needs in order to relay PLI/FIR upstream to a publisher. +#[tokio::test] +async fn test_boxed_rtcp_processing_rtc_sender_receives_feedback() -> Result<()> { + common::install_crypto_provider(); + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .is_test(true) + .try_init() + .ok(); + + const SENDER_SSRC: u32 = 0x00DE_CAFE; + const SENDER_TRACK_ID: &str = "boxed-rtcp-sender-test-track"; + + log::info!("Starting boxed RTCP processing test: boxed RTC (sender) <- webrtc feedback"); + + let mut peer = RtcpPeer::new("boxed-sender", true, false).await?; + + let sender_id = peer.pc.add_track(video_track( + "boxed-rtcp-sender-test-stream", + SENDER_TRACK_ID, + SENDER_SSRC, + ))?; + + let offer = peer.pc.create_offer(None)?; + peer.pc.set_local_description(offer.clone())?; + + let webrtc_pc = create_webrtc_peer().await?; + let webrtc_offer = WebrtcRTCSessionDescription::offer(offer.sdp.clone())?; + webrtc_pc.set_remote_description(webrtc_offer).await?; + let answer = webrtc_pc.create_answer(None).await?; + webrtc_pc.set_local_description(answer).await?; + let mut gathering_done = webrtc_pc.gathering_complete_promise().await; + let _ = timeout(Duration::from_secs(5), gathering_done.recv()).await; + let answer_with_candidates = webrtc_pc + .local_description() + .await + .expect("local description should be set"); + let rtc_answer = rtc::peer_connection::sdp::RTCSessionDescription::answer( + answer_with_candidates.sdp.clone(), + )?; + peer.pc.set_remote_description(rtc_answer)?; + + let mut buf = vec![0u8; 2000]; + let mut rtp_packets_sent = 0u32; + + let start_time = Instant::now(); + let test_timeout = Duration::from_secs(30); + + while start_time.elapsed() < test_timeout { + // Keep the webrtc receiver reporting by streaming RTP. + if peer.connected + && rtp_packets_sent < 300 + && let Some(mut sender) = peer.pc.rtp_sender(sender_id) + { + let _ = sender.write_rtp(dummy_rtp(SENDER_SSRC, rtp_packets_sent, 96)); + rtp_packets_sent += 1; + } + + peer.flush_writes().await; + peer.drain_events()?; + peer.drain_reads(); + + // Success: feedback about our SENT stream surfaced through the boxed chain. + if peer.rtcp_received >= 2 { + log::info!( + "Test passed! RTP sent: {}, RTCP received about sent stream: {}", + rtp_packets_sent, + peer.rtcp_received + ); + peer.pc.close()?; + webrtc_pc.close().await?; + return Ok(()); + } + + let delay_from_now = peer + .next_timeout() + .checked_duration_since(Instant::now()) + .unwrap_or(Duration::from_secs(0)); + if delay_from_now.is_zero() { + peer.pc.handle_timeout(Instant::now())?; + continue; + } + + let timer = tokio::time::sleep(delay_from_now.min(Duration::from_millis(10))); + tokio::pin!(timer); + let socket = Arc::clone(&peer.socket); + + tokio::select! { + _ = timer.as_mut() => { + peer.pc.handle_timeout(Instant::now())?; + } + res = socket.recv_from(&mut buf) => { + if let Ok((n, peer_addr)) = res { + peer.handle_datagram(&buf[..n], peer_addr)?; + } + } + } + } + + Err(anyhow::anyhow!( + "Test timeout - RTP sent: {}, RTCP received about sent stream: {}", + rtp_packets_sent, + peer.rtcp_received + )) +} + +// ============================================================================ +// Test 3: two RTC peers with DIFFERENT chains, driven from one Vec +// ============================================================================ + +/// The test the non-erased form cannot express. +/// +/// Two peer connections are built with genuinely different interceptor chains — the +/// answerer has the RTCP forwarder layered on, the offerer does not — and both are stored +/// in a single `Vec` and driven by the same non-generic code. With +/// `RTCPeerConnection` the two would have incompatible types and could +/// not share a collection. +/// +/// The chains' *behaviour* differs accordingly, and that is the assertion. The offerer +/// streams RTP, so its `SenderReportInterceptor` emits periodic Sender Reports; the +/// answerer receives both. The answerer, having the forwarder, surfaces those SRs to the +/// application through `poll_read()`, while the offerer — same peer connection type, +/// different chain — never surfaces a single RTCP packet, because without the forwarder +/// the default chain consumes RTCP internally. +#[tokio::test] +async fn test_boxed_rtc_to_rtc_heterogeneous_chains() -> Result<()> { + common::install_crypto_provider(); + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .is_test(true) + .try_init() + .ok(); + + const SENDER_SSRC: u32 = 0x00B0_0DED; + + log::info!("Starting boxed rtc-to-rtc test with heterogeneous interceptor chains"); + + // Same type, different chains — so they can live in one Vec. + let mut offerer = RtcpPeer::new("without-forwarder", false, false).await?; + let answerer = RtcpPeer::new("with-forwarder", true, true).await?; + + let sender_id = offerer.pc.add_track(video_track( + "boxed-heterogeneous-stream", + "boxed-heterogeneous-track", + SENDER_SSRC, + ))?; + + let mut peers: Vec = vec![offerer, answerer]; + + // Offer/answer between the two. + let offer = peers[0].pc.create_offer(None)?; + peers[0].pc.set_local_description(offer.clone())?; + peers[1].pc.set_remote_description(offer)?; + let answer = peers[1].pc.create_answer(None)?; + peers[1].pc.set_local_description(answer.clone())?; + peers[0].pc.set_remote_description(answer)?; + + let mut bufs = [vec![0u8; 2000], vec![0u8; 2000]]; + let mut rtp_packets_sent = 0u32; + + let start_time = Instant::now(); + let test_timeout = Duration::from_secs(30); + + while start_time.elapsed() < test_timeout { + // Stream RTP from the offerer once both transports are up. + if peers[0].connected + && peers[1].connected + && rtp_packets_sent < 600 + && let Some(mut sender) = peers[0].pc.rtp_sender(sender_id) + { + let _ = sender.write_rtp(dummy_rtp(SENDER_SSRC, rtp_packets_sent, 96)); + rtp_packets_sent += 1; + } + + // One non-generic loop body drives both peers, whatever chain each was built with. + for peer in &mut peers { + peer.flush_writes().await; + peer.drain_events()?; + peer.drain_reads(); + } + + // Success: RTP arrived at the answerer, whose forwarder also surfaced the + // offerer's Sender Reports about that stream. + if peers[1].rtp_received >= 10 && peers[1].rtcp_received >= 2 { + log::info!( + "Test passed! RTP sent: {}, RTP received: {}, RTCP surfaced (with-forwarder): {}, \ + RTCP surfaced (without-forwarder): {}", + rtp_packets_sent, + peers[1].rtp_received, + peers[1].rtcp_received, + peers[0].rtcp_received + ); + assert_eq!( + peers[0].rtcp_received, 0, + "a peer built without the forwarder must never surface RTCP to the application" + ); + for peer in &mut peers { + peer.pc.close()?; + } + return Ok(()); + } + + let next_timeout = peers[0].next_timeout().min(peers[1].next_timeout()); + let delay_from_now = next_timeout + .checked_duration_since(Instant::now()) + .unwrap_or(Duration::from_secs(0)); + if delay_from_now.is_zero() { + let now = Instant::now(); + for peer in &mut peers { + peer.pc.handle_timeout(now)?; + } + continue; + } + + let timer = tokio::time::sleep(delay_from_now.min(Duration::from_millis(10))); + tokio::pin!(timer); + let socket0 = Arc::clone(&peers[0].socket); + let socket1 = Arc::clone(&peers[1].socket); + let (buf0, buf1) = bufs.split_at_mut(1); + + tokio::select! { + _ = timer.as_mut() => { + let now = Instant::now(); + for peer in &mut peers { + peer.pc.handle_timeout(now)?; + } + } + res = socket0.recv_from(&mut buf0[0]) => { + if let Ok((n, peer_addr)) = res { + peers[0].handle_datagram(&buf0[0][..n], peer_addr)?; + } + } + res = socket1.recv_from(&mut buf1[0]) => { + if let Ok((n, peer_addr)) = res { + peers[1].handle_datagram(&buf1[0][..n], peer_addr)?; + } + } + } + } + + Err(anyhow::anyhow!( + "Test timeout - RTP sent: {}, RTP received: {}, RTCP surfaced: {}", + rtp_packets_sent, + peers[1].rtp_received, + peers[1].rtcp_received + )) +} diff --git a/tests/rtcp_processing_interop.rs b/tests/rtcp_processing_interop.rs index 0f900adb..ed66759f 100644 --- a/tests/rtcp_processing_interop.rs +++ b/tests/rtcp_processing_interop.rs @@ -50,6 +50,8 @@ use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; use webrtc::track::track_local::track_local_static_rtp::TrackLocalStaticRTP; use webrtc::track::track_local::{TrackLocal, TrackLocalWriter}; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); // ============================================================================ @@ -213,6 +215,7 @@ fn create_rtc_peer_config_with_rtcp_forwarder( /// - RTCP packets can be parsed and inspected #[tokio::test] async fn test_rtcp_processing_webrtc_offerer_rtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -474,6 +477,7 @@ async fn test_rtcp_processing_webrtc_offerer_rtc_answerer() -> Result<()> { /// This test verifies RTCP processing when roles are reversed. #[tokio::test] async fn test_rtcp_processing_rtc_offerer_webrtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -750,6 +754,7 @@ async fn test_rtcp_processing_rtc_offerer_webrtc_answerer() -> Result<()> { /// Asserts the RTC peer receives RTCP about its sent stream, tagged with the sender's track id. #[tokio::test] async fn test_rtcp_processing_rtc_sender_receives_feedback() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -859,10 +864,10 @@ async fn test_rtcp_processing_rtc_sender_receives_feedback() -> Result<()> { while let Some(event) = rtc_pc.poll_event() { match event { - RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) => { - if state == RTCIceConnectionState::Failed { - return Err(anyhow::anyhow!("RTC ICE connection failed")); - } + RTCPeerConnectionEvent::OnIceConnectionStateChangeEvent(state) + if state == RTCIceConnectionState::Failed => + { + return Err(anyhow::anyhow!("RTC ICE connection failed")); } RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { log::info!("RTC connection state: {}", state); diff --git a/tests/save_to_disk_vpx_interop.rs b/tests/save_to_disk_vpx_interop.rs index 163c7414..4c752ec0 100644 --- a/tests/save_to_disk_vpx_interop.rs +++ b/tests/save_to_disk_vpx_interop.rs @@ -47,11 +47,14 @@ use webrtc::rtp_transceiver::rtp_codec::RTCRtpCodecCapability; use webrtc::track::track_local::TrackLocal; use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test streaming media to disk: webrtc streams -> rtc receives #[tokio::test] async fn test_save_to_disk_vpx_webrtc_to_rtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -413,6 +416,7 @@ async fn test_save_to_disk_vpx_webrtc_to_rtc() -> Result<()> { // Process RTCP packets } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/simulcast_rtc_to_rtc_interop.rs b/tests/simulcast_rtc_to_rtc_interop.rs index ae987fe7..3eb12fd0 100644 --- a/tests/simulcast_rtc_to_rtc_interop.rs +++ b/tests/simulcast_rtc_to_rtc_interop.rs @@ -427,6 +427,7 @@ async fn test_simulcast_rtc_to_rtc() -> Result<()> { // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/simulcast_rtc_to_webrtc_interop.rs b/tests/simulcast_rtc_to_webrtc_interop.rs index ad7dd61c..05d40403 100644 --- a/tests/simulcast_rtc_to_webrtc_interop.rs +++ b/tests/simulcast_rtc_to_webrtc_interop.rs @@ -61,11 +61,14 @@ use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as use webrtc::rtp_transceiver::rtp_codec::RTPCodecType; use webrtc::track::track_remote::TrackRemote; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Test simulcast: rtc sends 3 layers with RIDs -> webrtc receives all 3 layers #[tokio::test] async fn test_simulcast_rtc_to_webrtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) diff --git a/tests/simulcast_webrtc_to_rtc_interop.rs b/tests/simulcast_webrtc_to_rtc_interop.rs index 0d450f2f..c7694ecd 100644 --- a/tests/simulcast_webrtc_to_rtc_interop.rs +++ b/tests/simulcast_webrtc_to_rtc_interop.rs @@ -56,6 +56,8 @@ use webrtc::rtp_transceiver::rtp_codec::{ use webrtc::track::track_local::TrackLocal; use webrtc::track::track_local::track_local_static_sample::TrackLocalStaticSample; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Integration test for multi-track video streaming (webrtc → rtc) @@ -92,6 +94,7 @@ const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); #[tokio::test] #[ignore] async fn test_simulcast_webrtc_to_rtc() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -499,6 +502,7 @@ async fn test_simulcast_webrtc_to_rtc() -> Result<()> { // RTCP packets are handled internally } RTCMessage::DataChannelMessage(_, _) => {} + _ => {} } } diff --git a/tests/statistics_rtc_to_rtc.rs b/tests/statistics_rtc_to_rtc.rs index 8ddbeea2..5599b6ba 100644 --- a/tests/statistics_rtc_to_rtc.rs +++ b/tests/statistics_rtc_to_rtc.rs @@ -185,10 +185,10 @@ async fn test_data_channel_statistics_collection() -> Result<()> { // Process offer peer events while let Some(event) = runner.offer_pc.poll_event() { match event { - RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { - if state == RTCPeerConnectionState::Connected { - offer_connected = true; - } + RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) + if state == RTCPeerConnectionState::Connected => + { + offer_connected = true; } RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { offer_dc_id = Some(channel_id); @@ -215,10 +215,10 @@ async fn test_data_channel_statistics_collection() -> Result<()> { // Process answer peer events while let Some(event) = runner.answer_pc.poll_event() { match event { - RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) => { - if state == RTCPeerConnectionState::Connected { - answer_connected = true; - } + RTCPeerConnectionEvent::OnConnectionStateChangeEvent(state) + if state == RTCPeerConnectionState::Connected => + { + answer_connected = true; } RTCPeerConnectionEvent::OnDataChannel(RTCDataChannelEvent::OnOpen(channel_id)) => { _answer_dc_id = Some(channel_id); @@ -891,6 +891,7 @@ async fn test_stats_json_serialization() -> Result<()> { RTCStatsReportEntry::AudioSource(s) => serde_json::to_string(s), RTCStatsReportEntry::VideoSource(s) => serde_json::to_string(s), RTCStatsReportEntry::AudioPlayout(s) => serde_json::to_string(s), + _ => continue, // Skip unknown variants for forward compatibility }; assert!( diff --git a/tests/testdata/rsa_2048_answerer_key.pem b/tests/testdata/rsa_2048_answerer_key.pem new file mode 100644 index 00000000..6d2a2d5f --- /dev/null +++ b/tests/testdata/rsa_2048_answerer_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCk0WjLWHTJi+I8 +jJbf80B+9R2FCxaHDYE9AYFxB+PPMa4qW/tj85SvgdcmBZGU8i8T0mmGTyd85AUQ +SaLvrvdaft2qyePbYZK8fxxHjuAwUR1m+FW59qSt0Qzw5cOZWqYRSYqYb4xpnl08 +5UauKO5y52tP9QUQiONMPxvTQPTRPJ7XHf4pJL50MDA8oEp/kE0oMpqmARgZtAvo +rp3/O81AY57EErhyTSmRir4VXv0/1V5zGaZO5JBvYd5RAOhCPqXRD6CdHbYtnJhL +6xBmFKDflhiZWbVt/jQe1f25aY5j9YQAwuInFgeXEC9h7NJhe6CQwpomVz4689PF +Pq7cQINfAgMBAAECggEAJyC5zrCYxTJKytIbjV4b3SgG0otv1S8NE/UUDEfxusIS +FWdR1ohAr6vU7mdbCO+34m5M9AA9oSYm15eLsXgpq9e7eyzDxdSzt8E7bveJ3J6d +KtFg1K2rqcIb3uLFHVaKX0dEumyc+oQIoYjSz0zTUv8CmHBUO/krcPH4gp4EoNlw +NpGps7TBjLoy6VKVERivBH0uphf+XVlZKuXu1d03JECpTjml4+xqEIyLNuaufEXk +0XdWD6YGeRuzuqeLmMFP+G7/hdIm2EnyEyfPaODgoDVzbhAsBio+8mF8bSrLJUZt +xU+HpR3EiAhHFuEm2bJ/wOuTfuBAYGiH1fH7NvS2KQKBgQDYtNx8Enw3JKF7ch2r +ZVKL7XS7LY/rc4Q883rhMxmdRJsFMtK4um3XqikA0n6w+h+hu4SMwLpFL6T32w34 +sP6PvEb10jseeR5ZfgURmzv+pmjrcEMzX7ckW6wpxOOfNseVjoxOSM6u2ktz8fzf +rbRu8iniuB3QmAf2vxLurelp9wKBgQDCs/mM3B2WJw9bpI0HzgWlCnZYUh5JtaZo +pp2ylTFVUXifxb02T8EkuL/RsJfk05psB2PgjPRUuWu3/zZRs9aUTeE98aGpis1V +d9IxGPLi0gh7TOo2XJglKr4bSgGnu2HToV+sGkedngLFJH3aH1cvD+9IVXDEaY5P +WvYmKgOX2QKBgBNJnaopVwbtarX7BSsBHKS5oL8cIggZwvZN0W0DyE7YEVgYGus4 +JJiiW33oPkBwMqqJbxW7/Zg00mEBDTBKSI3wevxcFsjyjMH4VmknwOI8W3BBEKoc +5ccFpcAd9whvrrnf8xrwnfywDzElo5Ug98M8KIA2VOkA7wyGGyBFOFYnAoGAHMgu +I7yGQyQTCSESGA1EmFkrlXLON2c66k13Emcrm8knY7b/eX/gtWT6Ni08xv7g3Qda +pF8x3zRp0BFSHlHPbqz0lwuZMk7qe6/yyn89qHoT9uYv2Ulm1Fe1MqOGCx2QIJDC +a9aWr54kGrCCSjRH1xUr8i1vNhnEh8aSjKG5VkECgYEAzyHUxK/PdxkGvz04LFLB +BjA8MJqyULH/WEPPl4aYvY2XyDpYNcgZzvyXigZ/RvZsXyB8Nj26zTozGeBpInOL +mVZfqhIl7i5IsbM7KWVbMVu7vgHP/7qn2ggkjIjnK1wsaXi3yYf7HTN36N+ciuf+ +5ugmEIxGUt5ZFnoTy8UTgsk= +-----END PRIVATE KEY----- diff --git a/tests/testdata/rsa_2048_offerer_key.pem b/tests/testdata/rsa_2048_offerer_key.pem new file mode 100644 index 00000000..8a1cf1b3 --- /dev/null +++ b/tests/testdata/rsa_2048_offerer_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCw+MSGjOm81NEW +MhYsA3a7Cdzi+sraonptStAbIXwKKeA2sb/QBSJU7D00k4MBnY/S1ciVrGeQvMbE +38p8JrvPVw7jvf9wgm+A6CVHdtjnO0MeK+zj19UV7dI62ojixxNqx5ZoXfikDtOM +5Z6u/CUJ4bJ3oDY5OOuOmYo/n9by7QEblp5I2J12UIkElh+Ng3VkZu/w8f1eRiHo +/wg/0Naqh7MeVgwYWhhiBZug+pX4ElrJa/x+BRw5llh9wScbs+v0kwMxTuiIzMhE +HeE6WbBkbZN137y9ZsFDU5jhZLxGcv1jou7xYqfBrH3bPpzbaM/qz/AfA2pVdHe0 +UXJZNyf1AgMBAAECggEABFM1bQW5RKh9f6mztFmqp4GN3heQ/kI69GbVVS8T+qTx +Wi+CkjvojeNuz3/Mvi0H8c17EdAH1pgL0jiXQ0IoaYsleFgRYReJpWsxq2Byhpwt +KhSavSrDhhNXhaFc97szyfKcxhTd2cHpq6eE9vPUl+bl11nurqapzcS02z81yp6z +rqbb0QfufF5TywinK78a0rKyY3mtF83z8kw9D3e1QF5r2AfrAK8vcGkZfOlK8mhk +3q3SaLKsWvB7emQ4ldigCM282jT3x+4vmPhOGbvtjsEpJEm28VTzIChqttHv2Rpg ++IVZccPVlyy4OEZoLqbnwP14rm5tX0R5cNjrxVIzQQKBgQD2yBbJ0UR/Cp3wKS3j +NTTGYDn7VYNBMlaXL4u2NKkNUGXHaUMykORx0IlQt1VpplX7qs4QHVwBekOJbgRa +aIjlT7QO8Z3EEQj/1Ba6fMmVad/fo6ToI2JAvT04gaSInYO2PkRyHNyi89+E2GpG +2yrY+2FGE2aNe+UdmjuW2zXvhQKBgQC3lRq9haGD+TvC/tGjzMgsMo0AaEaGqDm+ +k7M/+eCLsoT7xznloizM1UZ/jKg7jSOvqfzZs1Wps08DPZrCPyYJc9ak+vBimyWE +RTVHVxrmmqUpR8iehGdrTVAmbFOss2TnUK+io5+JCb3wRjt2RyQPWm5zSpHdl1wy +1oZICKNpsQKBgQCwOwlTDDd/BcTt6WpUk/1hIPynCFUYLOt7Qb/i2U5ULLLSKdCL +/r60rHgzBQlgziEe/MX06hJ3F6m9Lay8J2SDZVyvQ0on5wZnMz0b5dtK8PWnzkQI +ZqRWmQ1sGeC2ks2pSmQ0nXnOgJuBUc7rVL4Pf8zibx5QMUbX0fl17ItixQKBgHoB +oyLfg6c05Y3DUkodF8+fzOu/YVeux6mreY6EH8JX4199WTIO5N1AxLiSH2BsfZIK +VBvOvpiorVNHBuofk8Tmcnl0uHugBn/wiucdsagekLNtnJwU/LJoUGMozTdShjXg +/skFG0q06cGcu3nw77swa4U9wtFU/ZZf0iBfdVMRAoGAcZ2FcL4UsrBbIg6t8b/4 +K4wM5zNEt2o7kdVWLrrM0aBbWL1EEXFXLPXqhOEguSM4QL1ZCC8fxozYG0bNuiuj +y2R3K/AaeJDN/W9rM6x+Dbuj7QoOLDP9cSUf9iwaYtbrQcbDTk8SCzsyXle/bCli +L3Tuvt9w14f5auU05DpIfYQ= +-----END PRIVATE KEY----- diff --git a/tests/trickle_ice_interop.rs b/tests/trickle_ice_interop.rs index 6017e769..61dfda4a 100644 --- a/tests/trickle_ice_interop.rs +++ b/tests/trickle_ice_interop.rs @@ -45,6 +45,8 @@ use webrtc::peer_connection::configuration::RTCConfiguration as WebrtcRTCConfigu use webrtc::peer_connection::peer_connection_state::RTCPeerConnectionState as WebrtcRTCPeerConnectionState; use webrtc::peer_connection::sdp::session_description::RTCSessionDescription as WebrtcRTCSessionDescription; +mod common; + const DEFAULT_TIMEOUT_DURATION: Duration = Duration::from_secs(30); /// Helper function to create a webrtc peer connection (no STUN - local only) @@ -101,6 +103,7 @@ fn create_rtc_peer_config( /// - Data channel communication works with trickle ICE #[tokio::test] async fn test_trickle_ice_webrtc_offerer_rtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true) @@ -391,6 +394,7 @@ async fn test_trickle_ice_webrtc_offerer_rtc_answerer() -> Result<()> { /// - Data channel communication works with trickle ICE #[tokio::test] async fn test_trickle_ice_rtc_offerer_webrtc_answerer() -> Result<()> { + common::install_crypto_provider(); env_logger::builder() .filter_level(log::LevelFilter::Info) .is_test(true)