From c8541aa58452d99678b2c7e5227c0ae6eb68801d Mon Sep 17 00:00:00 2001 From: maclane Date: Wed, 1 Jul 2026 16:31:13 -0500 Subject: [PATCH] hardening(frost): zeroize DKG FFI secret buffers (parity with Sign path) The native tbtc-signer Sign path scrubs its Go-heap FFI transport buffers that carry secret material (via `defer zeroBytes(...)` on the request/ response/nonces slices), but the DKG path did not, leaving long-term share and DKG secret material resident in the Go heap after use. This closes that DKG<->Sign zeroization inconsistency. The DKG engine methods build a Go-heap request payload (JSON), hand a C copy to the Rust FFI via C.CBytes, and receive the response as a fresh Go slice via C.GoBytes. callBuildTaggedTBTCSignerOperation already scrubs and frees the C-heap request copy, and the Rust side frees the C response buffer, but the Go-side request/response slices were never wiped. Mirror the Sign path exactly by deferring zeroBytes on the secret-bearing Go buffers, so a mid-function or error return still wipes: - Part1: response (round-1 secret package / private polynomial coeffs). - Part2: request (round-1 secret package) and response (round-2 secret package + per-recipient round-2 secret shares). - Part3: request (round-2 secret package + received secret shares) and response (final key package / long-term signing share). - RunDKGWithSeed: request (DKG seed that deterministically reconstructs the group secret); its response is public metadata only. Public-only buffers are left untouched (RunDKG request/response, Part1 request). The defers run after the decoders evaluate the return value, and the decoders return freshly hex-decoded copies, so wiping the transport buffers never corrupts the returned secrets. cgo-safe: the Go slices are independent of the C copies, so zeroing them after the call returns neither races the C side nor risks a double-free. Co-Authored-By: Claude Fable 5 --- ...e_tbtc_signer_registration_frost_native.go | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go index 26d2c70dfa..941881b54b 100644 --- a/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go +++ b/pkg/frost/signing/native_frost_engine_tbtc_signer_registration_frost_native.go @@ -678,6 +678,12 @@ func (bttse *buildTaggedTBTCSignerEngine) RunDKGWithSeed( if err != nil { return nil, err } + // The request embeds the DKG seed, which deterministically drives key + // generation and therefore reconstructs the group secret; scrub the + // Go-side buffer on every return path, mirroring the Sign path. The C copy + // is separately scrubbed in callBuildTaggedTBTCSignerOperation. The RunDKG + // response carries only public metadata, so it is not zeroized. + defer zeroBytes(requestPayload) responsePayload, err := callBuildTaggedTBTCSignerRunDKG(requestPayload) if err != nil { @@ -705,6 +711,11 @@ func (bttse *buildTaggedTBTCSignerEngine) Part1( if err != nil { return nil, err } + // The response carries the round-1 secret package (private polynomial + // coefficients that must never be broadcast). Scrub the Go-side transport + // buffer once decoded, mirroring the Sign path's zeroBytes hygiene; the + // decoded secret returned to the caller is a fresh, independent copy. + defer zeroBytes(responsePayload) return decodeBuildTaggedTBTCSignerDKGPart1Response(responsePayload) } @@ -720,11 +731,19 @@ func (bttse *buildTaggedTBTCSignerEngine) Part2( if err != nil { return nil, err } + // The request embeds the round-1 secret package; scrub the Go-side buffer + // on every return path (including a failed FFI call), mirroring the Sign + // path. The C copy is separately scrubbed in callBuildTaggedTBTCSignerOperation. + defer zeroBytes(requestPayload) responsePayload, err := callBuildTaggedTBTCSignerDKGPart2(requestPayload) if err != nil { return nil, err } + // The response carries the round-2 secret package and the per-recipient + // round-2 packages (secret shares). Scrub the Go-side transport buffer once + // decoded; the decoded values returned to the caller are fresh copies. + defer zeroBytes(responsePayload) return decodeBuildTaggedTBTCSignerDKGPart2Response(responsePayload) } @@ -742,11 +761,20 @@ func (bttse *buildTaggedTBTCSignerEngine) Part3( if err != nil { return nil, err } + // The request embeds the round-2 secret package and the received round-2 + // packages (incoming secret shares); scrub the Go-side buffer on every + // return path, mirroring the Sign path. The C copy is separately scrubbed + // in callBuildTaggedTBTCSignerOperation. + defer zeroBytes(requestPayload) responsePayload, err := callBuildTaggedTBTCSignerDKGPart3(requestPayload) if err != nil { return nil, err } + // The response carries the final key package (the long-term signing share). + // Scrub the Go-side transport buffer once decoded; the decoded key package + // returned to the caller is a fresh copy. + defer zeroBytes(responsePayload) return decodeBuildTaggedTBTCSignerDKGPart3Response(responsePayload) }