Translate the key wrap metadata trailer - #472
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes cross-endian interoperability for the key-wrap protocol by translating the whNvmMetadata “raw struct trailer” that follows the fixed key-wrap request/response headers. It introduces a dedicated metadata translation helper and applies it in the server handlers so policy checks and storage always operate on native-order metadata, while the wire-facing trailer is translated appropriately.
Changes:
- Add
wh_MessageNvm_TranslateMetadata()to translatewhNvmMetadatatrailers (including in-place translation support). - Update key wrap / unwrap-and-export server handlers to translate the metadata trailer on ingress/egress based on the request
magic. - Add a dedicated endianness regression test for key-wrap metadata trailers and register it in the server test list.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| wolfhsm/wh_message_nvm.h | Declares the new wh_MessageNvm_TranslateMetadata() helper for raw-struct metadata trailers. |
| src/wh_message_nvm.c | Implements wh_MessageNvm_TranslateMetadata() using existing translation macros and label passthrough. |
| src/wh_server_keystore.c | Translates key-wrap metadata trailer on request ingest and on unwrap-and-export response egress using magic. |
| test-refactor/server/wh_test_keywrap_endian.c | Adds endianness coverage: helper property checks plus wrap→unwrap/export round-trips under native/swapped magic. |
| test-refactor/wh_test_list.c | Registers the new whTest_KeyWrapEndian server test. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #472
Scan targets checked: wolfhsm-core-bugs, wolfhsm-crypto-bugs, wolfhsm-src
No new issues found in the changed files. ✅
|
|
||
| /* The policy checks above run on native metadata, so put the trailer into | ||
| * the client's byte order only once it is on its way out */ | ||
| (void)wh_MessageNvm_TranslateMetadata(magic, metadata, metadata); |
There was a problem hiding this comment.
Please check the error return from this call.
Don't we need to do the translation prior to the policy checks (right after _AesGcmKeyUnwrap)? Else the metadata coming in would still be in the native client endianess
There was a problem hiding this comment.
Error return is now checked, using the fallthrough guard so it can't mask a
prior failure:
if (ret == WH_ERROR_OK) {
ret = wh_MessageNvm_TranslateMetadata(magic, metadata, metadata);
}On the placement — I kept it on egress, because the metadata here doesn't come
off the wire. It comes out of the decrypted blob, and the blob always holds
metadata in server order:
_AesGcmKeyWrapWithKek()memcpys thewhNvmMetadatastruct into the
plaintext blob verbatim, and_HandleKeyWrapRequesttranslates
client -> native before wrapping._HandleKeyWrapExportRequestwraps
metadata read straight from the keystore, which is native by definition._HandleKeyUnwrapAndCacheRequestdoes no translation at all and stores blob
metadata directly into the keystore. That is only correct if blob metadata is
native, which is the same invariant.
So translating right after _AesGcmKeyUnwrap would byte-swap the values that
WH_KEYID_IS_UNASSIGNED(metadata->id), the WH_KEYTYPE_WRAPPED check,
metadata->flags & WH_NVM_FLAGS_NONEXPORTABLE, and the USER ownership check
all read — a swapped-endian client would then bypass or trip those checks
incorrectly. Egress-last keeps every policy check on native values.
I've expanded the comment at the call site to say this explicitly.
There was a problem hiding this comment.
Checked now, and it mattered more than expected — the check made a
previously-dead branch live:
if (ret == WH_ERROR_OK) {
ret = wh_MessageNvm_TranslateMetadata(magic, metadata, metadata);
}
if (ret != WH_ERROR_OK && respDataSz >= sizeof(*metadata)) {
resp->keySz = 0;
wh_Utils_ForceZero(metadata, scrubSz);
}I had these as if / else if first, which fails open: a translation failure
would skip the scrub and leave resp->keySz set, shipping the decrypted key
alongside an error rc. Two ifs keyed off the final ret close it.
The scrub came out of the same audit — *out_resp_size here unconditionally
counts a trailer, so a failed unwrap shipped up to 32 bytes of stale buffer.
Placement answer unchanged: egress-last, since the blob holds metadata in server
order and every policy check must run on native values.
There was a problem hiding this comment.
I would like to see an end-to-end test of this with a client and server if possible
There was a problem hiding this comment.
Added: test-refactor/client-server/wh_test_keywrap_endian_e2e.c
(whTest_KeyWrapEndianE2E, client group). Wrap -> unwrap-and-export against the
live server over the real transport, once as a same-endian peer and once as a
swapped one.
There was a problem hiding this comment.
Expanded since my last reply. whTest_KeyWrapEndianE2E now also covers a denied
export (NONEXPORTABLE) and asserts the response trailer comes back zeroed.
That case needs the real harness: the server test uses separate request and
response buffers, but wh_Server_HandleRequestMessage passes one pointer as
both. For KEYUNWRAPEXPORT both fixed structs are 8 bytes, so the blob and the
response trailer share an offset and _AesGcmKeyUnwrap writes the decrypted
trailer there before the scrub.
Byte swaps in both files are hand-rolled rather than reusing the Translate*
helpers, so a bug under test cannot cancel itself out — a flags bug injected
into wh_MessageNvm_TranslateMetadata is now caught by the round trip and the
NONEXPORTABLE negative, where it previously passed undetected.
d8f30fb to
9be189c
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #472
Scan targets checked: wolfhsm-core-bugs, wolfhsm-crypto-bugs, wolfhsm-src
No new issues found in the changed files. ✅
|
Hello @AlexLanzano , |
9be189c to
7cae950
Compare
Problem
The key wrap protocol carries a
whNvmMetadataas a raw struct trailerafter the fixed request/response headers, but the server translated only the
headers.
whNvmMetadatais fouruint16_tfields (id,access,flags,len), so every one is byte-swapped for a cross-endian client/server pair:WH_KEY_KEYWRAPrequestwhNvmMetadata+ keyid0x4025(TYPE=WRAPPED) read as0x2540(TYPE=SHE) → valid wrap rejectedWH_ERROR_BADARGSWH_KEY_KEYUNWRAPEXPORTresponsewhNvmMetadata+ keyid,access,flags,lenreturned byte-swappedEvery other NVM message flattens the struct into message fields and translates
each one; key wrap is the only place the raw struct crosses the boundary.
Addressed by f_5469.
Fix (
src/wh_server_keystore.c)Server-side only: clients always send native byte order, and the server
translates both directions from the comm-header
magic.wh_MessageNvm_TranslateMetadata()— new helper inwh_message_nvm.[ch],in-place safe, label copied verbatim.
_HandleKeyWrapRequesttranslates on ingress before any field isinspected, establishing the invariant that blobs always store metadata in
server-native order.
_HandleKeyUnwrapAndExportRequesttranslates on egress last, after everypolicy check has run on native values.
Both handlers are
staticwith one call site each, so they gain amagicparameter. No client, wire-format, or struct change.
Also fixed, found while auditing the failure paths:
*out_resp_sizeforKEYUNWRAPEXPORTunconditionally counts awhNvmMetadatatrailer, so a failedunwrap shipped up to 32 bytes of stale comm-buffer content to the client. The
handler now zeroes the trailer and any decrypted key with
wh_Utils_ForceZero()on every failure exit, and the dispatcher scrubs as a backstop. In-switch
returns becameret = X; break;so one epilogue covers them all._HandleKeyWrapRequestalso setsresp->cipherTypebefore its first failureexit — the client checks
cipherTypeahead ofrc, so an early returnpreviously surfaced as
WH_ERROR_ABORTEDinstead of the real error.Tests
Reworked on review: 1,179 test lines removed, 128 added. Both original
files were shapes a real peer cannot produce —
wh_test_keywrap_endian.c(720)drove
whServerContext*internals with synthetic magics, andwh_test_keywrap_endian_e2e.c(459) hand-built packets and pushed them at thetransport, because
wh_CommClientonly ever sends native magic. Their twowh_test_list.centries and theREADME.mdcoverage-gap bullet go with them.Replaced by
test-refactor/misc/wh_test_message_nvm.c, following #468: aloopback pair structurally cannot observe a byte-order bug, so the test calls
wh_MessageNvm_TranslateMetadata()directly withWH_COMM_MAGIC_SWAP. Nopacket crafting, no new entry point registered beyond the one parent function.
It asserts native magic is a no-op, all four scalars swap, the
labelbytearray does not move, translation is its own inverse, in-place (
src == dest)matches out-of-place, and NULL args give
WH_ERROR_BADARGS. Expected values arewritten out as literals rather than derived from
wh_Translate16(), so theoracle does not share the code under test. The helper is ungated, so the test
runs in every configuration.
The
whNvmMetadataentry added towh_test_check_struct_padding.cstays — itis what keeps the raw wire struct padding-free.
Coverage dropped with the harness, none of it reachable from a real client:
foreign-magic acceptance at the comm layer, the
req_packet/resp_packetaliasing that makes egress translation an in-place swap, wrap/unwrap policy
negatives already covered by
client-server/wh_test_keywrap.c, and themalformed-request residue check for the scrub described above. The new
"keep in step with
wh_MessageNvm_TranslateAddObjectRequest" comment likewisehas no test behind it; the twin is pre-existing code this PR does not touch.
Verification
On merge base
237bfbc, clean under-std=c90 -Werror -Wall -Wextra, zerowarnings in all four configurations:
test-refactordefaulttest-refactor DMA=1test-refactor SHE=1test-refactor NOCRYPTO=1whTest_MessageNvmTranslatepasses in all four. Two negative controls, eachrebuilt and re-run: dropping the
accessWH_T16fails the test, and droppingthe label
memcpyfails it; restoring both returns 46/26/0.