Skip to content

Comments

[KeyManager] Implement FFIs for decap-and-seal and HPKE open operations#649

Open
NilanjanDaw wants to merge 11 commits intogoogle:mainfrom
NilanjanDaw:add-decap-seal-open-ffi
Open

[KeyManager] Implement FFIs for decap-and-seal and HPKE open operations#649
NilanjanDaw wants to merge 11 commits intogoogle:mainfrom
NilanjanDaw:add-decap-seal-open-ffi

Conversation

@NilanjanDaw
Copy link
Collaborator

@NilanjanDaw NilanjanDaw commented Feb 8, 2026

This PR implements the core FFI interfaces for the Key Protection Service (KPS) and Workload Service (WS) to support the secure secret delivery flow using the decap-and-seal and HPKE open operations. It also introduces necessary cryptographic utilities to test the end-to-end secret sharing flow.

Key Changes

km_common (Common Crypto & Utilities)

  • DHKEM Encapsulation: Added a "test-only" encap to support encapsulation operation flows with a pre-shared key. This manually implements the Encap operation as BoringSSL's current FFI lacks a direct entry point encapsulation.
  • Test Infrastructure: Introduced a test-utils feature to allow these cryptographic helpers to be shared across crates for testing purposes without exposing them in production builds.

kps_key_custody_core (Key Protection Service)

  • key_manager_decap_and_seal FFI: Implemented the decapsulate-and-seal logic. This takes a client's encapsulated key, recovers the shared secret using the KEM private key, and immediately reseals it for the target Workload Service using its binding public key.

ws_key_custody_core (Workload Service)

  • key_manager_open FFI: Implemented the final decryption step. It retrieves the Binding Private Key (isolated in memfd_secret backed storage) to decrypt the final payload delivered from the KPS.

Testing Performed

  • Ran cargo test -p km_common
  • Ran cargo test -p kps_key_custody_core
  • Ran cargo test -p ws_key_custody_core
  • All 40+ unit tests across the workspace are passing.

atulpatildbz added a commit to atulpatildbz/go-tpm-tools that referenced this pull request Feb 15, 2026
This change functions as the Go-side implementation for the Decap and Seal flow.
It is based on PR google#652 (Key Gen) and PR google#649 (FFI Decap).

Base Commit: 2030fa6 (Align Key Generation API with contract)

Squashed Commits:
- Fix compilation and tests for wsd_decaps_go (API alignment to /v1/keys:decap)
- keymanager/wsd: align decaps payloads with proto messages
- keymanager/wsd: define decaps API proto schema
- [keymanager/wsd] Add /keys:decaps endpoint with DecapAndSeal + Open orchestration

Key Features:
- endpoint: POST /v1/keys:decap
- Request: DecapsRequest (snake_case)
- Response: DecapsResponse (snake_case)
- Flows: DecapAndSeal (KPS) -> Open (WSD)
@NilanjanDaw NilanjanDaw force-pushed the add-decap-seal-open-ffi branch from 20bc382 to cb4a8ec Compare February 15, 2026 11:00
@NilanjanDaw NilanjanDaw changed the title Implement Key Manager FFIs for decap-and-seal and HPKE open operations [KeyManager] Implement Key Manager FFIs for decap-and-seal and HPKE open operations Feb 15, 2026
@NilanjanDaw NilanjanDaw changed the title [KeyManager] Implement Key Manager FFIs for decap-and-seal and HPKE open operations [KeyManager] Implement FFIs for decap-and-seal and HPKE open operations Feb 15, 2026
Introduces `key_manager_decap_and_seal`, a new FFI function in
`key_custody_core` that enables decapsulating a shared secret and
immediately resealing it with a binding public key.

Key Changes:
- New FFI: Added `key_manager_decap_and_seal` to handle the transition
of a shared secret from a client-provided encapsulation to a sealed
form bound to a specific receiver.
- Security: Integrated `zeroize` to ensure the intermediate shared
secret is wiped from memory after the resealing operation.
- Error Handling: Implemented robust error reporting for invalid
inputs, missing keys, and cryptographic failures.
- Testing: Added a comprehensive unit test `test_decap_and_seal_success`
that simulates a client-to-service flow and verifies the recovered secret.
BoringSSL does not currently expose a way to initialize a RecipientContext
directly from a pre-calculated shared secret. This change adds a
 helper to km_common that manually implements
the HPKE KeySchedule to derive the AEAD key and nonce.

Changes:
- Implement  and HPKE KDF helpers in km_common.
- Mark these helpers as available only for tests or via the new  feature.
- Enable  in kps_key_custody_core dev-dependencies.
- Update  to verify decryption using the recovered secret.
This change adds the  FFI function to the workload
service's key custody core. This function allows the Workload Service
to decrypt secrets using a previously generated binding key.

Key features:
- Look up the Binding Private Key from the registry (isolated in its own
  memfd_secret page via Vault).
- Perform an HPKE open operation to decrypt the provided payload.
- Aggressively sanitize sensitive material by zeroing out decrypted
  plaintext buffers after copying to the output.
- Add comprehensive unit tests covering success, invalid UUID, and
  insufficient buffer size scenarios.
- Fix unused import warnings in km_common related to test-only crypto helpers.
Err(e) => e,
}
}))
.unwrap_or(-1)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FFI API has a pretty opaque error code interface. Any future plans for improving this?

Once pattern I've seen before is setting a global ERRNO with an error code for the last failure, and always returning -1 in the FFI API.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me do this in a future PR. We will track this in #668

Comment on lines 10 to 12

[dev-dependencies]
km_common = { path = "../../km_common", features = ["test-utils"] }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be useful to use a "Cargo workspace" to manage all of these shared crates:

https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, updated the cargo dependencies to a workspace.

@NilanjanDaw NilanjanDaw force-pushed the add-decap-seal-open-ffi branch from b653fa6 to 036d7f6 Compare February 20, 2026 11:47
use km_common::key_types::{KeyRecord, KeyRegistry, KeySpec};
use prost::Message;
use std::slice;
use std::sync::atomic::AtomicBool;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can you format this file? i think the previous position of this import was correct

uuid: Uuid,
encapsulated_key: &[u8],
aad: &[u8],
) -> Result<(Vec<u8>, Vec<u8>), i32> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we avoid the intermediate allocation and accept &mut [u8] instead? since caller already provides out_encapsulated_key, out_ciphertext output buffers

Some(_) => 0, // Success
None => -1, // Not found
// Seal
match km_common::crypto::hpke_seal(binding_public_key, &shared_secret, aad, hpke_algo) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're using hpke_algo here. which assumes that binding key always supports the exact same cryptro params (KEM/KDF/AEAD) as the KEM key.

is your comment of the other PR #665 (comment) valid here?

None => -1, // Not found
/// Internal function to decrypt a ciphertext using a stored binding key.
fn open_internal(uuid: Uuid, enc: &[u8], ciphertext: &[u8], aad: &[u8]) -> Result<SecretBox, i32> {
let Some(record) = KEY_REGISTRY.get_key(&uuid) else {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we can use shorter idiom here:

let key_record = KEY_REGISTRY.get_key(&uuid).ok_or(-1)?;

Comment on lines +165 to +166
/// * `out_plaintext_len` - The size of `out_plaintext` buffer.
/// On success, it will be updated with the actual size of the plaintext.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the signature is :

out_plaintext_len: usize, // Passed by value!

this statement is misleading: "On success, it will be updated with the actual size of the plaintext". since it's passed by value.

check if you need to update the signature to *mut usize. or fix the docstring

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.

4 participants