Skip to content

Commit 4825f92

Browse files
jaredwolffclaude
andcommitted
Add DTLS 1.2 PSK authentication (RFC 4279)
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, resolver } / Server { hint, resolver }) - PskResolver trait for callback-based key lookup - ConfigBuilder::with_psk_client / with_psk_server - Dtls::new_12_psk constructor for PSK-only sessions - 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 - Client handles servers that omit ServerKeyExchange when no identity hint is present (RFC 4279 §2) Config validation: - Reject PSK configs where DTLS 1.2 has no PSK suite after filtering, regardless of DTLS 1.3 state (DTLS 1.3 is not a fallback for Dtls::new_12_psk, and DTLS 1.3 PSK is a separate mechanism) - Require kx groups whenever a cert-based DTLS 1.2 suite survives the filter, even when PSK is also configured - require_client_certificate documented as cert-only 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 plus an explicit psk_valid flag 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 - Config-builder validation tests for PSK + DTLS 1.3, PSK + cert suites, and PSK-only empty-kx-groups 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> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6b60a94 commit 4825f92

27 files changed

Lines changed: 2040 additions & 200 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Unreleased
22

3+
* Add PSK (Pre-Shared Key) cipher suite for DTLS 1.2 (RFC 4279, RFC 7925)
4+
* `PSK_AES128_CCM_8` (0xC0A8)
5+
* Add `Dtls::new_12_psk()` constructor for PSK-only sessions
6+
* Add `PskResolver` trait and PSK config builder methods
7+
* Fix client to handle optional ServerKeyExchange in PSK handshakes (RFC 4279 §2)
38
* Fix DTLS 1.2 signature hash mismatch for P-384 keys #97
49

510
# 0.5.0

Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ edition = "2024"
77
license = "MIT OR Apache-2.0"
88
repository = "https://github.com/algesten/dimpl"
99
readme = "README.md"
10-
keywords = ["dtls", "tls", "webrtc"]
10+
keywords = ["dtls", "tls", "webrtc", "psk"]
1111
categories = ["network-programming", "cryptography", "security"]
1212

1313
# MSRV
@@ -17,13 +17,14 @@ rust-version = "1.85.0"
1717
default = ["aws-lc-rs", "rcgen"]
1818

1919
# Default crypto provider
20-
aws-lc-rs = ["dep:aws-lc-rs", "_crypto-common"]
20+
aws-lc-rs = ["dep:aws-lc-rs", "dep:ccm", "dep:aes", "_crypto-common"]
2121

2222
# Pure Rust crypto provider
2323
rust-crypto = [
2424
"dep:aes-gcm", "dep:chacha20poly1305", "dep:chacha20", "dep:p256",
2525
"dep:p384", "dep:x25519-dalek", "dep:sha2", "dep:hmac", "dep:hkdf",
2626
"dep:ecdsa", "dep:generic-array", "dep:rand_core",
27+
"dep:ccm", "dep:aes",
2728
"_crypto-common"
2829
]
2930

@@ -68,6 +69,8 @@ generic-array = { version = "0.14", optional = true }
6869
rand_core = { version = "0.6", optional = true }
6970
chacha20poly1305 = { version = "0.10", optional = true }
7071
chacha20 = { version = "0.9", optional = true }
72+
ccm = { version = "0.5", default-features = false, optional = true }
73+
aes = { version = "0.8", optional = true }
7174
x25519-dalek = { version = "2", optional = true, features = ["static_secrets"] }
7275

7376
# certificate generation

README.md

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ verification and SRTP key export yourself.
2222

2323
### Version selection
2424

25-
Three constructors control which DTLS version is used:
26-
- [`Dtls::new_12`][new_12] — explicit DTLS 1.2
25+
Four constructors control which DTLS version is used:
26+
- [`Dtls::new_12`][new_12] — explicit DTLS 1.2 (certificate‑based)
27+
- [`Dtls::new_12_psk`][new_12_psk] — explicit DTLS 1.2 (PSK, no certificates)
2728
- [`Dtls::new_13`][new_13] — explicit DTLS 1.3
2829
- [`Dtls::new_auto`][new_auto] — auto‑sense: the first
2930
incoming ClientHello determines the version (based on the
@@ -34,6 +35,8 @@ Three constructors control which DTLS version is used:
3435
- `ECDHE_ECDSA_AES256_GCM_SHA384`
3536
- `ECDHE_ECDSA_AES128_GCM_SHA256`
3637
- `ECDHE_ECDSA_CHACHA20_POLY1305_SHA256`
38+
- **PSK cipher suites (TLS 1.2 over DTLS)**
39+
- `PSK_AES128_CCM_8`
3740
- **Cipher suites (TLS 1.3 over DTLS)**
3841
- `TLS_AES_128_GCM_SHA256`
3942
- `TLS_AES_256_GCM_SHA384`
@@ -44,7 +47,6 @@ Three constructors control which DTLS version is used:
4447
- **DTLS‑SRTP**: Exports keying material for `SRTP_AEAD_AES_256_GCM`,
4548
`SRTP_AEAD_AES_128_GCM`, and `SRTP_AES128_CM_SHA1_80` ([RFC 5764], [RFC 7714]).
4649
- **Extended Master Secret** ([RFC 7627]) is negotiated and enforced (DTLS 1.2).
47-
- Not supported: PSK cipher suites.
4850

4951
### Certificate model
5052
During the handshake the engine emits
@@ -131,6 +133,37 @@ let dtls = mk_dtls_client();
131133
let _ = example_event_loop(dtls);
132134
```
133135

136+
## Example (PSK client)
137+
138+
```rust
139+
use std::sync::Arc;
140+
use std::time::Instant;
141+
142+
use dimpl::{Config, Dtls, PskResolver};
143+
144+
struct MyPsk;
145+
146+
impl PskResolver for MyPsk {
147+
fn resolve(&self, identity: &[u8]) -> Option<Vec<u8>> {
148+
if identity == b"device-01" {
149+
Some(b"shared-secret-key".to_vec())
150+
} else {
151+
None
152+
}
153+
}
154+
}
155+
156+
let config = Arc::new(
157+
Config::builder()
158+
.with_psk_client(b"device-01".to_vec(), Arc::new(MyPsk))
159+
.build()
160+
.unwrap(),
161+
);
162+
163+
let mut dtls = Dtls::new_12_psk(config, Instant::now());
164+
dtls.set_active(true); // client role
165+
```
166+
134167
#### MSRV
135168
Rust 1.85.0
136169

@@ -139,6 +172,7 @@ Rust 1.85.0
139172
- Renegotiation is not implemented (WebRTC does full restart).
140173

141174
[new_12]: https://docs.rs/dimpl/latest/dimpl/struct.Dtls.html#method.new_12
175+
[new_12_psk]: https://docs.rs/dimpl/latest/dimpl/struct.Dtls.html#method.new_12_psk
142176
[new_13]: https://docs.rs/dimpl/latest/dimpl/struct.Dtls.html#method.new_13
143177
[new_auto]: https://docs.rs/dimpl/latest/dimpl/struct.Dtls.html#method.new_auto
144178
[peer_cert]: https://docs.rs/dimpl/latest/dimpl/enum.Output.html#variant.PeerCert

src/auto.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ impl HybridClientHello {
105105
ch_body.push(0);
106106

107107
// cipher_suites: 1.3 suites first, then 1.2 suites (filtered by config)
108-
let mut suites: ArrayVec<u16, 8> = ArrayVec::new();
108+
let mut suites: ArrayVec<u16, 16> = ArrayVec::new();
109109
for cs in config.dtls13_cipher_suites() {
110110
suites.push(cs.suite().as_u16());
111111
}

0 commit comments

Comments
 (0)