TLS 1.3 External PSK
View SourceThis guide covers TLS 1.3 external pre-shared key (PSK) support for QUIC connections, defined by RFC 8446 §4.2.11. External PSKs let two endpoints authenticate each other without X.509 certificates by sharing a secret out of band.
This is not RFC 9258 (the PSK Importer). The secret you pass is consumed as raw IKM by the TLS key schedule; no derivation is layered on top. If you need importer-style derivation to bind a PSK to a target protocol/KDF, do it before handing the secret to this library.
When to use it
RFC 9257 §5.1 describes the typical use cases:
- Closed deployments where the operator controls both endpoints and PKI would just add ceremony (cluster nodes, service-mesh peers).
- Device-to-device authentication with pre-provisioned secrets.
- Environments without a certificate authority (lab gear, embedded devices, internal control planes).
Secret requirements
The PSK is consumed unchanged by HKDF, so you choose the entropy.
- Minimum 128 bits of entropy. Longer is fine.
- No truncation, no derivation. What you pass is what HKDF gets.
- Generate with
crypto:strong_rand_bytes/1or your KMS. - Keep secrets in a vault or KMS. This library does not store them.
API
Client
{ok, Conn} = quic:connect(Host, Port, #{
verify => false,
alpn => [<<"echo">>],
external_psk => {<<"client-id">>, <<"32-byte-shared-secret-...">>}
}, self()).Two forms are accepted:
{Identity, Secret}: defaults to mode[psk_dhe_ke](forward-secret).{Identity, Secret, Modes}: explicit non-empty list ofpsk_dhe_ke | psk_ke. First match wins on the server.
external_psk and session_ticket are mutually exclusive. Passing
both yields {error, {bad_opts, psk_conflict}}.
Server
{ok, _} = quic:start_server(my_server, 4433, #{
alpn => [<<"echo">>],
%% Either or both:
psks => #{<<"client-id">> => <<"32-byte-shared-secret-...">>},
psk_callback => fun
(<<"client-id">>) -> {ok, <<"32-byte-shared-secret-...">>};
(_) -> not_found
end
}).Lookup order is psk_callback first, then psks map. Either or
both may be configured. They may coexist with cert/key. Without
PSK config and without cert/key, start_server/3 returns
{error, no_auth_method}.
A callback that raises is treated as not_found and logged at
warning level. It will not crash the handshake.
Modes
psk_dhe_ke(default): PSK authentication with (EC)DHE. Forward-secret. Use this unless you have a specific reason not to.psk_ke: PSK only, no DHE. No forward secrecy. Use only on endpoints that cannot do DHE or when forward secrecy is explicitly out of scope.
Downgrade protection
When a client supplies external_psk, it requires the server to
select that PSK. If the server's ServerHello does not echo the
expected selected_psk_identity, the client aborts with
{error, psk_not_selected} rather than completing a cert-based
handshake. This prevents silent fallback to an unauthenticated
cert path when verify => false is in effect.
Mixed cert + PSK servers
A server configured with both certs and PSK selects per-handshake:
- Client offers a known PSK identity, binder verifies, compatible mode → server selects PSK.
- Client offers an unknown identity OR no compatible mode → server falls through to the cert path.
- Client offers a known identity but the binder fails to verify →
fatal
decrypt_erroralert. The server does not fall through to cert in this case (silent downgrade prevention).
v1 limitations
- No 0-RTT /
early_dataon external PSK. A client offeringexternal_pskwithearly_datawill have theearly_dataextension ignored by the server. NewSessionTicketis not emitted on PSK-authenticated handshakes. External PSK clients already hold a long-lived credential.
Both items are tracked as follow-ups in docs/features.md.
Distribution
Erlang distribution over QUIC (quic_dist) supports PSK
authentication for certificate-less clustering. See
QUIC_DIST.md.