Add PSK cipher suites for DTLS 1.2#92
Conversation
|
I’ve been watching this repo for like 7 months, waiting for PSK support. PSK + CIDs + session resumption would finally unlock Rust for a lot of IoT use-cases where DTLS is everywhere. |
|
Its definitely been a long time coming for the rust eco system. Excited to get this out there. I'll get working on the rebase. Looks like some conflicts. |
|
I took a quick look at the changes and noticed a potential issue: when a |
|
Good catch. Let me get a fix in there for that. |
HMBSbige
left a comment
There was a problem hiding this comment.
I also noticed a couple of minor issues:
|
Addressing the CI issues shortly. |
algesten
left a comment
There was a problem hiding this comment.
Hi there! I have read through the code. Overall looks good, I think there are a couple of improvements to do.
The ambition of dimpl was never to be a generic DTLS implementation (beyond my own needs in WebRTC), however I'm not opposed to it going that direction. Although I'm not a crypto person, I'll remain the code steward for it, for now.
The PSK stuff goes beyond what I expect we'll use dimpl for ourselves, so accepting this into the tree also means we will be relying on yourself (and other volunteers) to stay on top of that part of the code. HMBSbige has already stepped up, so maybe that won't be much of a problem.
8b3f4c3 to
fe24c71
Compare
|
Probably a good candidate for a squashed commit everything is hashed out. Thanks again all. |
Adds pre-shared key support with TLS_PSK_WITH_AES_128_CCM_8 (0xC0A8),
the cipher mandated by RFC 7925 / LwM2M for constrained IoT devices.
Public API:
- Psk enum (Client { identity, key } / Server { resolver })
- ConfigBuilder::with_psk_client / with_psk_server
- Error::PskError variant
Internals:
- AuthMode enum unifies Certificate and PSK paths in CryptoContext
- ccm_cipher module implements AES-128-CCM-8 AEAD
- ClientKeyExchange / ServerKeyExchange gain PSK variants
- PSK cipher suite wired into both aws-lc-rs and rust-crypto backends
Security:
- Certificate-mode contexts reject PSK suites, preventing downgrade
past Certificate/CertificateVerify
- PSK suites filtered from advertised list when no resolver configured
- Invalid-identity handling uses a dummy-PSK fallback to avoid a timing
oracle that would otherwise leak identity validity
- CCM_8 ordered after AEAD suites in the default list
Tests:
- tests/dtls12/psk.rs covers the PSK handshake and edge cases
- OpenSSL interop tests included but #[ignore]d (OpenSSL excludes
CCM-8 from DTLS)
Docs: README, CHANGELOG, and module docs updated.
Signed-off-by: Jared Wolff <hello@jaredwolff.com>
846a304 to
4825f92
Compare
Review follow-ups to the initial PSK commit, squashed from three in-flight fixups plus additional validation tightening. Config validation: - Reject PSK configs where DTLS 1.2 has no PSK suite after filtering, regardless of DTLS 1.3 state. The only PSK suite this crate implements is DTLS 1.2 (0xC0A8), so a surviving DTLS 1.3 suite is not a fallback for Dtls::new_12_psk; building such a config produced a runtime-only failure instead of a clear build error. - Require kx groups whenever a cert-based DTLS 1.2 suite survives the filter, even when PSK is also configured. Previously a `with_psk_*().kx_groups(&[])` config that kept ECDHE suites in the DTLS 1.2 filter built successfully and then failed in send_server_key_exchange/process_ecdh_params. - Skip the kx-group check only when the surviving DTLS 1.2 suites are exclusively PSK, instead of whenever PSK is configured. - Reject builders whose constructor validation would otherwise silently accept a PSK-suite-free DTLS 1.2 filter. Constructor: - Dtls::new_12_psk asserts the config has a PSK configured so a missing resolver fails fast at construction rather than producing zero negotiable suites. Handshake: - Omit ServerKeyExchange entirely when the server has no PSK identity hint configured (RFC 4279 §2). Docs: - Clarify that require_client_certificate applies only to certificate-authenticated cipher suites and has no effect on a negotiated PSK handshake. Tests: - Add psk_with_dtls13_but_no_psk_dtls12_suite_rejected and psk_with_cert_dtls12_and_empty_kx_groups_rejected to cover the new validation paths. - Tighten psk_client_with_empty_kx_groups_builds to an explicitly PSK-only DTLS 1.2 filter, matching the stricter semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Before: - Server used a hardcoded all-zeros dummy PSK on resolver failure, so a client whose resolver also produced zeros derived the same master secret — handshake would have completed without the psk_valid flag. - psk_valid: bool defaulted to true; a future refactor that forgot to set it in the PSK branch would silently bypass identity validation. After: - Dummy PSK is 32 random bytes from the engine RNG. Finished MAC mismatch is now cryptographically guaranteed, not merely statistical. - psk_valid: Option<bool>, initialized to None. Finished-handler requires Some(true) only when the negotiated suite is_psk() — fails closed if the PSK path ever reaches Finished without setting it. - DUMMY_PSK_LEN constant documents the chosen size and its rationale. Tests: - New test psk_mismatched_keys_fail_at_finished_via_mac: both sides return Some (different keys), forcing the failure through the MAC / record-decryption path alone. Exercises the primary crypto guarantee independently of the psk_valid flag. - Existing invalid-identity test relaxed to accept CryptoError too — with a random dummy, the server can't decrypt the client's Finished record (different derived AEAD keys), so the failure surfaces as a decryption error before reaching the MAC comparison. Both error types represent correct invalid-PSK rejection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
API stability: - Mark `Psk` enum and its `Client`/`Server` variants `#[non_exhaustive]` so future variants (DTLS 1.3 external PSK, etc.) or new fields can be added without a major version bump. Internal pattern matches already use `..` so no crate-internal churn. Documentation: - Expand the invariant on `CryptoContext::get_client_certificate` and `serialize_client_certificate` to spell out why panicking in PSK mode is correct: the state machine routes around them via `cs.is_psk()` before Certificate serialization is reached. - Add `// unwrap:` comments to cert-mode `load_private_key().expect()` calls in `Client::new` and `Server::new` to match the CLAUDE.md convention for justified unwraps. (The matching DTLS 1.3 expect is pre-existing on main and out of scope.) Minor: - Replace `pms.extend_from_slice(&vec![0u8; n])` with `pms.resize(..)` in `compute_psk_pre_master_secret` — drops a temporary heap allocation from every PSK handshake. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cover the ClientKeyExchange and ServerKeyExchange PSK variants at the parser level so future refactors don't regress the wire format: - psk_roundtrip — parse + serialize round-trip for a typical message. - psk_rejects_oversized_length / psk_rejects_oversized_hint_length — asserts the parser fails cleanly when the declared u16 length exceeds the bytes actually present (nom `take` rejects, no panic). - psk_empty_identity / psk_empty_hint — zero-length fields are wire-legal (RFC 4279 §2, §5.1) and must parse to an empty range. Cross-implementation interop (mbedtls / tinydtls for PSK_AES128_CCM_8) deferred to a separate follow-up PR — real infra work, not a unit test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tier 1 (e4d8c5d) required psk_valid == Some(true) on every PSK Finished, but abbreviated (resumption) handshakes skip ClientKeyExchange entirely — the server reuses a cached master secret and never consults the resolver, so psk_valid stays None. The overzealous check rejected legitimate PSK resumption. Relax to `psk_valid == Some(false)`. This still catches the dummy-PSK flow explicitly (the intended defense-in-depth against a future refactor that lets the dummy survive the MAC check), while allowing None for the resumption code path. Found during rebase of dtls-conn-id onto psk-support; caught by tests/dtls12/resumption.rs::psk_abbreviated_handshake. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Rebased to main, squashed the initial work and found some edge cases that needed addressing. |
|
@jaredwolff looks ready! Some lint fixes. |
Sounds good. Let me fix those shortly. |
Apply cargo fmt and split multi-line imports into single-line forms to satisfy the snowflake import-multi-line check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Thanks for taking it all the way! |
|
Thanks for the feedback! I've got more coming. 😜 |
Summary
TLS_PSK_WITH_AES_128_CCM_8(0xC0A8)PskResolvertrait for callback-based key lookup andDtls::new_12_psk()constructorPskenum withClientandServervariantsAuthModeenum inCryptoContextandEnginePskErrorvariant for PSK-specific error conditionsccmcrate, shared across both aws-lc-rs and RustCrypto backendsMotivation
PSK support is needed for IoT devices (e.g., nRF9151 modem) that use pre-shared keys
instead of certificates for DTLS 1.2 authentication.
Test plan
OpenSSL interop testsIgnored (OpenSSL does not support CCM-8 over DTLS)cargo clippyclean