Skip to content

Add PSK cipher suites for DTLS 1.2#92

Merged
algesten merged 7 commits intoalgesten:mainfrom
circuitdojo:psk-support
Apr 21, 2026
Merged

Add PSK cipher suites for DTLS 1.2#92
algesten merged 7 commits intoalgesten:mainfrom
circuitdojo:psk-support

Conversation

@jaredwolff
Copy link
Copy Markdown
Contributor

@jaredwolff jaredwolff commented Mar 10, 2026

Summary

  • Implement RFC 4279 PSK key exchange for DTLS 1.2 with TLS_PSK_WITH_AES_128_CCM_8 (0xC0A8)
    • Only mandatory PSK cipher suite per RFC 7925 (IoT TLS/DTLS profile)
  • Add PskResolver trait for callback-based key lookup and Dtls::new_12_psk() constructor
  • Model PSK config as Psk enum with Client and Server variants
  • Unify certificate/PSK auth via AuthMode enum in CryptoContext and Engine
  • Add PskError variant for PSK-specific error conditions
  • AES-128-CCM-8 cipher via ccm crate, shared across both aws-lc-rs and RustCrypto backends
  • Handle servers that omit ServerKeyExchange when no PSK identity hint is provided (RFC 4279 §2)

Motivation

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

  • Self-handshake and bidirectional data transfer tests for PSK_AES128_CCM_8
  • PSK identity validation tests (invalid identity rejected at Finished)
  • OpenSSL interop tests Ignored (OpenSSL does not support CCM-8 over DTLS)
  • All existing tests continue to pass (48 tests + doctests)
  • cargo clippy clean

@motters
Copy link
Copy Markdown

motters commented Mar 10, 2026

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.

@jaredwolff
Copy link
Copy Markdown
Contributor Author

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.

@HMBSbige
Copy link
Copy Markdown
Contributor

HMBSbige commented Mar 11, 2026

I took a quick look at the changes and noticed a potential issue: when a Config has both a certificate and a psk_resolver, new_12 (certificate mode) can still negotiate PSK suites, since is_cipher_suite_compatible() returns true for PSK (no signature_algorithm). This could let a peer downgrade the connection to PSK, skipping Certificate/CertificateRequest and bypassing require_client_certificate. Might be worth filtering out PSK suites in the certificate path, or catching this in build().

@jaredwolff
Copy link
Copy Markdown
Contributor Author

Good catch. Let me get a fix in there for that.

Copy link
Copy Markdown
Contributor

@HMBSbige HMBSbige left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also noticed a couple of minor issues:

Comment thread src/crypto/aws_lc_rs/cipher_suite.rs
Comment thread src/crypto/rust_crypto/cipher_suite.rs
Comment thread src/dtls12/server.rs Outdated
@jaredwolff
Copy link
Copy Markdown
Contributor Author

Addressing the CI issues shortly.

Copy link
Copy Markdown
Owner

@algesten algesten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/crypto/aws_lc_rs/cipher_suite.rs Outdated
Comment thread src/dtls12/client.rs
Comment thread src/dtls12/client.rs
Comment thread src/dtls12/client.rs Outdated
Comment thread src/dtls12/client.rs Outdated
Comment thread src/dtls12/engine.rs
Comment thread src/config.rs Outdated
Comment thread src/config.rs
Comment thread src/config.rs Outdated
Comment thread src/config.rs
@jaredwolff jaredwolff force-pushed the psk-support branch 2 times, most recently from 8b3f4c3 to fe24c71 Compare March 13, 2026 03:44
@jaredwolff
Copy link
Copy Markdown
Contributor Author

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>
@jaredwolff jaredwolff force-pushed the psk-support branch 2 times, most recently from 846a304 to 4825f92 Compare April 20, 2026 05:01
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>
jaredwolff and others added 4 commits April 20, 2026 01:08
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>
@jaredwolff
Copy link
Copy Markdown
Contributor Author

Rebased to main, squashed the initial work and found some edge cases that needed addressing.

@algesten
Copy link
Copy Markdown
Owner

@jaredwolff looks ready! Some lint fixes.

@jaredwolff
Copy link
Copy Markdown
Contributor Author

@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>
@algesten algesten merged commit 7068c01 into algesten:main Apr 21, 2026
46 checks passed
@algesten
Copy link
Copy Markdown
Owner

Thanks for taking it all the way!

@jaredwolff
Copy link
Copy Markdown
Contributor Author

Thanks for the feedback! I've got more coming. 😜

@jaredwolff jaredwolff deleted the psk-support branch April 21, 2026 04:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants