Support for importing external PSKs (RFC 9258)#27
Open
Frauschi wants to merge 10 commits into
Open
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 commit3d5c9e3from 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
ImportedIdentitycarried on the wire and the internal (imported) PSK used in the handshake.What's included
ImportedIdentity(external_identity,context,target_protocol,target_kdf) construction/serialization on the client and parsing on the server.epskx = HKDF-Extract(0, epsk)thenipskx = HKDF-Expand-Label(epskx, "derived psk", Hash(ImportedIdentity), L),with L = target_kdf output length.
"imp binder"binder-key label for imported PSKs (§5).wolfSSL_external_psk_pre_extracted) — skips HKDF-Extract.int* hashAlgoargument (pre-initialized toWC_SHA256). The whole derivation —Hash(ImportedIdentity)+ HKDF — honorsdevId/CryptoCb.--enable-psk-importer(autotools) and the CMake/options.h.inequivalents.API (behind
WOLFSSL_EXTERNAL_PSK_IMPORTER)Identity, context and key are opaque, length-delimited buffers (each
*Szis capacity-in / length-out), so identities may contain arbitrary bytes per the RFC.Notable adaptations vs. the source commit
Hkdfstruct API (not in master) to master's functionalwc_HKDF_*_exAPI, preservingdevIdsupport.RFC 9258 compliance
Audited against the RFC; compliant on the cryptographic essentials —
ImportedIdentitylayout, the"derived psk"Expand-Label withL= 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 validatestarget_protocol/target_kdfagainst 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)
external_identitywhen parsing (removes an unauthenticated server NULL-pointer write).(ptr,len)— no buffer mutation, no heap copy.WOLFSSL_CHECK_MEM_ZERO.Tests
tests/api.c: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.testpasses with 0 failures.🤖 Generated with Claude Code
Generated by Claude Code