Skip to content

Support for importing external PSKs (RFC 9258)#27

Open
Frauschi wants to merge 10 commits into
masterfrom
claude/festive-albattani-q5y080
Open

Support for importing external PSKs (RFC 9258)#27
Frauschi wants to merge 10 commits into
masterfrom
claude/festive-albattani-q5y080

Conversation

@Frauschi

Copy link
Copy Markdown
Owner

Summary

Adds support for importing external PSKs for TLS 1.3 per RFC 9258, gated behind WOLFSSL_EXTERNAL_PSK_IMPORTER. This is a port of commit 3d5c9e3 from the Laboratory-for-Safe-and-Secure-Systems fork onto current master, adapted to upstream APIs and hardened.

The importer transforms an external PSK (EPSK) — external identity, optional context, base key — into the ImportedIdentity carried on the wire and the internal (imported) PSK used in the handshake.

What's included

  • New client/server importer callback interface (in addition to the existing TLS 1.3 PSK callbacks).
  • ImportedIdentity (external_identity, context, target_protocol, target_kdf) construction/serialization on the client and parsing on the server.
  • Imported-PSK derivation per RFC 9258 §3.1:
    epskx = HKDF-Extract(0, epsk) then
    ipskx = HKDF-Expand-Label(epskx, "derived psk", Hash(ImportedIdentity), L),
    with L = target_kdf output length.
  • "imp binder" binder-key label for imported PSKs (§5).
  • Pre-extracted EPSK support (wolfSSL_external_psk_pre_extracted) — skips HKDF-Extract.
  • Configurable EPSK hash: the importer hash defaults to SHA-256 (the RFC 9258 default) and is settable per-EPSK via the callbacks' int* hashAlgo argument (pre-initialized to WC_SHA256). The whole derivation — Hash(ImportedIdentity) + HKDF — honors devId/CryptoCb.
  • Build plumbing: --enable-psk-importer (autotools) and the CMake/options.h.in equivalents.

API (behind WOLFSSL_EXTERNAL_PSK_IMPORTER)

typedef int (*wc_psk_client_importer_callback)(WOLFSSL* ssl,
    unsigned char* identity, word32* identitySz,
    unsigned char* context, word32* contextSz,
    unsigned char* key, word32* keySz, int* hashAlgo);
typedef int (*wc_psk_server_importer_callback)(WOLFSSL* ssl,
    const unsigned char* identity, word32 identitySz,
    const unsigned char* context, word32 contextSz,
    unsigned char* key, word32* keySz, int* hashAlgo);

void wolfSSL_CTX_set_psk_client_importer_callback(WOLFSSL_CTX*, wc_psk_client_importer_callback);
void wolfSSL_set_psk_client_importer_callback(WOLFSSL*, wc_psk_client_importer_callback);
void wolfSSL_CTX_set_psk_server_importer_callback(WOLFSSL_CTX*, wc_psk_server_importer_callback);
void wolfSSL_set_psk_server_importer_callback(WOLFSSL*, wc_psk_server_importer_callback);
int  wolfSSL_external_psk_pre_extracted(WOLFSSL* ssl, int opt);

Identity, context and key are opaque, length-delimited buffers (each *Sz is capacity-in / length-out), so identities may contain arbitrary bytes per the RFC.

Notable adaptations vs. the source commit

  • Retargeted the derivation from the fork's streaming Hkdf struct API (not in master) to master's functional wc_HKDF_*_ex API, preserving devId support.
  • Dropped the PKCS#11 key-by-id/label external-key path (it depended on the un-upstreamed streaming HKDF + PKCS#11 V3.0 work); pre-extracted EPSK support is retained.

RFC 9258 compliance

Audited against the RFC; compliant on the cryptographic essentials — ImportedIdentity layout, the "derived psk" Expand-Label with L = target-KDF length, epskx = HKDF-Extract(0, epsk), the "imp binder" label, the KDF registry values (HKDF_SHA256/HKDF_SHA384), and the EPSK-associated importer hash (default SHA-256, now configurable). Imported PSKs are treated as external (non-resumption, obfuscated_ticket_age = 0). The server also validates target_protocol/target_kdf against what's negotiated.

DTLS 1.3 is outside the RFC's scope; the implementation is self-consistent there but uses the "dtls13" protocol label.

Hardening (from review)

  • Reject a zero-length external_identity when parsing (removes an unauthenticated server NULL-pointer write).
  • Server passes the received identity/context as opaque (ptr,len) — no buffer mutation, no heap copy.
  • Client deduplicates by target KDF (one IPSK per KDF) and skips suites whose MAC isn't a defined RFC 9258 KDF.
  • Derived-key buffers registered with WOLFSSL_CHECK_MEM_ZERO.

Tests

tests/api.c:

  • Memio handshake covering SHA-256 and SHA-384 target KDFs, with/without context, pre-extracted, and a non-default (SHA-384-associated) EPSK hash. Runs in PSK-only / no-certificate builds too.
  • Negative binder-mismatch handshake.
  • Direct ImportedIdentity (de)serialization unit test covering empty-identity, truncated, length-overflow, and bad-KDF inputs.

Verification

Builds clean (-Werror) with: feature on, feature off (default), WOLFSSL_CHECK_MEM_ZERO + WOLFSSL_SMALL_STACK, DTLS 1.3, and a no-SHA384 build. ./tests/unit.test passes with 0 failures.

🤖 Generated with Claude Code


Generated by Claude Code

claude added 10 commits June 21, 2026 16:43
Port the external PSK importer interface from RFC 9258 to current master,
gated behind WOLFSSL_EXTERNAL_PSK_IMPORTER. Based on commit 3d5c9e3 of the
Laboratory-for-Safe-and-Secure-Systems wolfssl fork.

* New client/server PSK importer callback interface, in addition to the
  existing TLS 1.3 PSK callbacks.
* Support for the optional external PSK context.
* Derive an ImportedIdentity (external_identity, context, target_protocol,
  target_kdf) from the external data and serialize it as the on-the-wire
  PSK identity.
* Derive the internal (imported) PSK from the external one per RFC 9258
  Section 3.1: epskx = HKDF-Extract(0, epsk) followed by
  ipskx = HKDF-Expand-Label(epskx, "derived psk", Hash(ImportedIdentity), L),
  with L set to the output length of the target_kdf.
* Support both normal and pre-extracted external PSKs
  (wolfSSL_external_psk_pre_extracted).
* Update PSK binder key derivation to use the "imp binder" label for
  imported PSKs (RFC 9258 Section 5).
* Parse a received ImportedIdentity on the server side.

The imported-PSK derivation uses SHA-256 as the importer hash function,
which is the RFC 9258 default when the EPSK has no associated hash. The
HKDF operations use the existing functional wc_HKDF_*_ex API so devId /
CryptoCb offload is supported. The fork's PKCS#11 key-by-id/label path was
omitted as it depends on a streaming HKDF API not present in master.

Add an --enable-psk-importer configure option (and CMake equivalent) and a
memio-based handshake test covering the SHA-256 and SHA-384 target KDFs,
with and without a context, and the pre-extracted case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw
Address code-review feedback on the external PSK importer:

* Security: reject a zero-length external_identity when parsing a received
  ImportedIdentity. Previously such input left the id pointer NULL, which
  the server then dereferenced/wrote to (id[idSz] = '\0') -> remotely
  triggerable NULL pointer write. RFC 9258 defines
  external_identity<1..2^16-1>.
* Server side no longer mutates the received message buffer: the identity
  is copied into a heap NUL-terminated string for the callback.
* Server now validates target_protocol (in addition to target_kdf) against
  the negotiated version before treating an identity as imported.
* Client side derives one ImportedIdentity per (protocol, KDF) and skips
  cipher suites whose MAC is not a defined RFC 9258 target KDF, avoiding
  duplicate PSK identities and a BAD_FUNC_ARG on non-SHA256/384 suites.
  Consolidated the per-iteration cleanup via a single goto path and fixed
  word32->word16 narrowing.
* Register the derived-key buffers (okm/prk) with WOLFSSL_CHECK_MEM_ZERO.
* Fixed the CreateImportedIdentity doc comment (no ssl parameter) and added
  a doc comment for ParseImportedIdentity.
* Added Doxygen API documentation for the new public importer functions.
* Tests: added a direct ImportedIdentity (de)serialization unit test
  covering empty-identity, truncated, overflow and bad-KDF inputs; added a
  negative binder-mismatch handshake; broadened the test guard so it also
  runs in PSK-only (no certificate) builds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw
Harden the importer callback contract so the external identity (and
context) are opaque, length-delimited byte buffers instead of a
NUL-terminated C string recovered with strlen.

* Client callback signature is now
  (ssl, identity, *identitySz, context, *contextSz, key, *keySz):
  each *Sz holds the buffer capacity on entry and the actual length on
  exit. The strlen-based length recovery is removed, and identities may
  contain arbitrary bytes (RFC 9258 external_identity is opaque).
* Server callback signature is now
  (ssl, identity, identitySz, context, contextSz, key, *keySz):
  identity and context are passed as explicit (ptr,len) pairs aliasing
  the received message. This removes the previous heap copy and
  NUL-termination of the received identity entirely.
* Updated both client call sites, the server lookup in FindPskSuite, the
  test callbacks, and the API documentation accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw
RFC 9258 derives the imported PSK using the hash associated with the
external PSK (defaulting to SHA-256 only when none is associated). The
importer hash was previously hardcoded to SHA-256, which is non-compliant
for an EPSK associated with a different hash (e.g. SHA-384).

Expose the EPSK hash through the importer callbacks via an int* hashAlgo
argument that is pre-initialized to WC_SHA256: callers that use the default
need not touch it, and only set it for an EPSK associated with another hash.

* Both client and server importer callbacks gain a trailing int* hashAlgo.
* DeriveImportedPreSharedKey() takes the importer hash and uses it for both
  Hash(ImportedIdentity) and the HKDF-Extract/Expand, via the generic
  wc_Hash / wc_HKDF_*_ex APIs (buffers sized to WC_MAX_DIGEST_SIZE). The
  digest size is validated to fit the buffers. The target_kdf still solely
  determines the imported-PSK output length L, independent of this hash.
* The importer hash is threaded straight from each callback invocation to
  the derivation (both call sites already call the callback immediately
  before deriving), so no extra state is stored.
* Tests: callbacks advertise the hash; added a case with a SHA-384
  associated EPSK used with a SHA-256 target_kdf.
* Updated API documentation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw
Compute Hash(ImportedIdentity) with the streaming wc_HashAlg API
(wc_HashInit_ex/Update/Final/Free) instead of the one-shot wc_Hash, so the
hash can be offloaded through devId/CryptoCb like the HKDF steps already
are. The hash context is stack-allocated normally and heap-allocated under
WOLFSSL_SMALL_STACK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw
Address review feedback on PR #27.

* Refactor the imported-PSK derivation into a WOLFSSL-independent core,
  DeriveImportedPsk(), exposed as WOLFSSL_LOCAL. DeriveImportedPreSharedKey()
  becomes a thin wrapper that sources its inputs from the SSL object. This
  lets the derivation be exercised directly with known-answer vectors.
* Add test_tls13_external_psk_importer_kat: known-answer tests for the
  ImportedIdentity serialization and the ipskx output. The expected values
  were computed independently of wolfSSL (Python hashlib/hmac per RFC 9258
  Section 3.1), so a symmetric derivation bug that the handshake tests would
  miss is now caught. Three vectors cover SHA-256/SHA-384 target KDFs and a
  non-default (SHA-384) importer hash.
* Document the importer callback contract: callbacks must be deterministic
  (invoked more than once per connection), and the identity/context size
  caps (MAX_PSK_ID_LEN / MAX_PSK_CTX_LEN), in both ssl.h and the Doxygen
  docs.

No standalone interoperable reference implementation of the RFC 9258
importer transform exists publicly (the RFC ships no test vectors), so the
independently-computed KAT is used in place of cross-implementation interop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw
Drop wolfSSL_external_psk_pre_extracted() and the associated
psk_externalKeyPreExtracted state and derivation branch. It was a
non-standard convenience not required by RFC 9258 (the import always
performs epskx = HKDF-Extract(0, epsk)) and is not wanted for upstreaming.

* Remove the public API and its prototype/documentation.
* Remove the Arrays.psk_externalKeyPreExtracted bitfield.
* DeriveImportedPsk()/DeriveImportedPreSharedKey() always HKDF-Extract.
* Drop the pre-extracted handshake test case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw
Add a fourth known-answer vector to the importer KAT using the exact
external_identity (0xCAFECAFE), context (0xDEADBEEF) and expected
ImportedIdentity from GnuTLS's own test (tests/psk-importer.c, GnuTLS
>= 3.8.1). This confirms our ImportedIdentity serialization matches an
independent RFC 9258 implementation. The ipskx expectation is computed
independently.

The KAT helper now takes the identity and EPSK as parameters so vectors
with different inputs can be exercised.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw
Wire the importer into the example client and server so they can be used
for interoperability testing (e.g. against GnuTLS >= 3.8.1).

* Add example importer callbacks (my_psk_client_importer_cb /
  my_psk_server_importer_cb) in wolfssl/test.h: same external identity
  (kIdentityStr), no context, default SHA-256 importer hash, and a fixed
  32-byte external PSK on both sides.
* Add a --psk-importer option to examples/client and examples/server that
  selects the importer callbacks (TLS 1.3 only). Gated behind
  WOLFSSL_EXTERNAL_PSK_IMPORTER; the regular PSK paths are unchanged when
  the option/feature is absent.
* Document the option in the usage output of both apps.

Verified client<->server interop over a socket for the SHA-256 and SHA-384
target KDFs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw
Change the example RFC 9258 importer callbacks (wolfssl/test.h) to carry a
non-empty optional context ("wolfSSL importer example context") instead of
an empty one, to better illustrate the feature. The server callback now
verifies the received context matches.

Verified wolfSSL <-> GnuTLS 3.8.4 interop (both directions, TLS 1.3,
TLS_AES_128_GCM_SHA256) still succeeds with the context present, and that a
mismatched context is correctly rejected with a binder-verification
failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw
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.

2 participants