Skip to content

Commit acab338

Browse files
committed
feat(keymanager): Implement Decap and Seal orchestration in Go
This change functions as the Go-side implementation for the Decap and Seal flow. It is based on PR #652 (Key Gen) and PR #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)
1 parent 2030fa6 commit acab338

File tree

16 files changed

+824
-150
lines changed

16 files changed

+824
-150
lines changed

keymanager/key_protection_service/key_custody_core/include/kps_key_custody_core.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@ int32_t key_manager_generate_kem_keypair(KmHpkeAlgorithm algo,
1818
uint8_t *out_uuid,
1919
uint8_t *out_pubkey,
2020
size_t out_pubkey_len);
21+
// key_manager_decap_and_seal decapsulates a shared secret using the stored KEM
22+
// key identified by uuid_bytes, then reseals the shared secret with the
23+
// associated binding public key via HPKE.
24+
//
25+
// uuid_bytes must point to a 16-byte KEM key UUID.
26+
// encapsulated_key is the client-provided encapsulated key.
27+
// aad is optional Additional Authenticated Data (may be NULL if aad_len == 0).
28+
// out_encapsulated_key_len and out_ciphertext_len are in/out params.
29+
//
30+
// Returns 0 on success, -1 on invalid args/key not found, -2 if output buffers
31+
// are too small, -3 if decapsulation fails, -4 if sealing fails.
32+
int32_t key_manager_decap_and_seal(
33+
const uint8_t *uuid_bytes,
34+
const uint8_t *encapsulated_key,
35+
size_t encapsulated_key_len,
36+
const uint8_t *aad,
37+
size_t aad_len,
38+
uint8_t *out_encapsulated_key,
39+
size_t *out_encapsulated_key_len,
40+
uint8_t *out_ciphertext,
41+
size_t *out_ciphertext_len);
2142

2243
#ifdef __cplusplus
2344
} // extern "C"

keymanager/key_protection_service/key_custody_core/kps_key_custody_core_cgo.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,47 @@ func GenerateKEMKeypair(bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, [
5757
copy(pubkey, pubkeyBuf[:pubkeyLen])
5858
return id, pubkey, nil
5959
}
60+
61+
// DecapAndSeal decapsulates a shared secret using the stored KEM key and
62+
// reseals it with the associated binding public key via Rust FFI.
63+
// Returns the new encapsulated key and sealed ciphertext.
64+
func DecapAndSeal(kemUUID uuid.UUID, encapsulatedKey, aad []byte) ([]byte, []byte, error) {
65+
if len(encapsulatedKey) == 0 {
66+
return nil, nil, fmt.Errorf("encapsulated key must not be empty")
67+
}
68+
69+
uuidBytes := kemUUID[:]
70+
71+
var outEncKey [32]byte
72+
outEncKeyLen := C.size_t(len(outEncKey))
73+
var outCT [48]byte // 32-byte secret + 16-byte GCM tag
74+
outCTLen := C.size_t(len(outCT))
75+
76+
var aadPtr *C.uint8_t
77+
aadLen := C.size_t(0)
78+
if len(aad) > 0 {
79+
aadPtr = (*C.uint8_t)(unsafe.Pointer(&aad[0]))
80+
aadLen = C.size_t(len(aad))
81+
}
82+
83+
rc := C.key_manager_decap_and_seal(
84+
(*C.uint8_t)(unsafe.Pointer(&uuidBytes[0])),
85+
(*C.uint8_t)(unsafe.Pointer(&encapsulatedKey[0])),
86+
C.size_t(len(encapsulatedKey)),
87+
aadPtr,
88+
aadLen,
89+
(*C.uint8_t)(unsafe.Pointer(&outEncKey[0])),
90+
&outEncKeyLen,
91+
(*C.uint8_t)(unsafe.Pointer(&outCT[0])),
92+
&outCTLen,
93+
)
94+
if rc != 0 {
95+
return nil, nil, fmt.Errorf("key_manager_decap_and_seal failed with code %d", rc)
96+
}
97+
98+
sealEnc := make([]byte, outEncKeyLen)
99+
copy(sealEnc, outEncKey[:outEncKeyLen])
100+
sealedCT := make([]byte, outCTLen)
101+
copy(sealedCT, outCT[:outCTLen])
102+
return sealEnc, sealedCT, nil
103+
}

keymanager/key_protection_service/key_custody_core/src/lib.rs

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ pub unsafe extern "C" fn key_manager_decap_and_seal(
163163
};
164164

165165
let key_record = match KEY_REGISTRY.get_key(&uuid) {
166-
Some(record) => record,
167-
None => return -1,
166+
Ok(record) => record,
167+
Err(_) => return -1,
168168
};
169169

170170
let (hpke_algo, binding_public_key) = match &key_record.meta.spec {
@@ -184,9 +184,14 @@ pub unsafe extern "C" fn key_manager_decap_and_seal(
184184
let enc_key_slice = unsafe { slice::from_raw_parts(encapsulated_key, encapsulated_key_len) };
185185

186186
// Decapsulate
187-
let mut shared_secret = match km_common::crypto::decaps(&key_record.private_key, enc_key_slice)
187+
let private_key = match km_common::crypto::PrivateKey::try_from(key_record.private_key.as_bytes().to_vec()) {
188+
Ok(pk) => pk,
189+
Err(_) => return -3,
190+
};
191+
192+
let mut shared_secret = match km_common::crypto::decaps(&private_key, enc_key_slice)
188193
{
189-
Ok(s) => s,
194+
Ok(secret) => secret,
190195
Err(_) => return -3,
191196
};
192197

@@ -198,10 +203,10 @@ pub unsafe extern "C" fn key_manager_decap_and_seal(
198203

199204
// Seal
200205
let (new_enc_key, sealed_ciphertext) = match km_common::crypto::hpke_seal(
201-
binding_public_key,
206+
&binding_public_key,
202207
&shared_secret,
203208
aad_slice,
204-
hpke_algo,
209+
&hpke_algo,
205210
) {
206211
Ok(res) => res,
207212
Err(_) => {
@@ -427,21 +432,22 @@ mod tests {
427432
fn test_destroy_kem_key_success() {
428433
let binding_pubkey = [1u8; 32];
429434
let mut uuid_bytes = [0u8; 16];
430-
let algo = HpkeAlgorithm {
435+
let algo = KmHpkeAlgorithm {
431436
kem: KemAlgorithm::DhkemX25519HkdfSha256 as i32,
432437
kdf: KdfAlgorithm::HkdfSha256 as i32,
433438
aead: AeadAlgorithm::Aes256Gcm as i32,
434439
};
435440

436441
unsafe {
442+
let mut pubkey_bytes = [0u8; 32];
437443
key_manager_generate_kem_keypair(
438444
algo,
439445
binding_pubkey.as_ptr(),
440446
binding_pubkey.len(),
441447
3600,
442448
uuid_bytes.as_mut_ptr(),
443-
std::ptr::null_mut(),
444-
std::ptr::null_mut(),
449+
pubkey_bytes.as_mut_ptr(),
450+
32,
445451
);
446452
}
447453

@@ -466,6 +472,38 @@ mod tests {
466472
assert_eq!(result, -1);
467473
}
468474

475+
#[test]
476+
fn test_key_manager_generate_kem_keypair() {
477+
// 1. Setup binding key
478+
let binding_kem_algo = KemAlgorithm::DhkemX25519HkdfSha256;
479+
let (binding_pk, _) = km_common::crypto::generate_x25519_keypair(binding_kem_algo).unwrap();
480+
481+
// 2. Call generate_kem_keypair
482+
let mut uuid_bytes = [0u8; 16];
483+
let mut kem_pubkey_bytes = [0u8; 32];
484+
let kem_pubkey_len = 32;
485+
let algo = KmHpkeAlgorithm {
486+
kem: KemAlgorithm::DhkemX25519HkdfSha256 as i32,
487+
kdf: KdfAlgorithm::HkdfSha256 as i32,
488+
aead: AeadAlgorithm::Aes256Gcm as i32,
489+
};
490+
let result = unsafe {
491+
key_manager_generate_kem_keypair(
492+
algo,
493+
binding_pk.as_ptr(),
494+
binding_pk.len(),
495+
3600,
496+
uuid_bytes.as_mut_ptr(),
497+
kem_pubkey_bytes.as_mut_ptr(),
498+
kem_pubkey_len,
499+
)
500+
};
501+
502+
assert_eq!(result, 0);
503+
assert_ne!(uuid_bytes, [0u8; 16]);
504+
assert_ne!(kem_pubkey_bytes, [0u8; 32]);
505+
}
506+
469507
#[test]
470508
fn test_decap_and_seal_success() {
471509
// 1. Setup binding key (receiver for seal)
@@ -476,12 +514,13 @@ mod tests {
476514
// 2. Generate KEM key in registry
477515
let mut uuid_bytes = [0u8; 16];
478516
let mut kem_pubkey_bytes = [0u8; 32];
479-
let mut kem_pubkey_len = 32;
480-
let algo = HpkeAlgorithm {
517+
let kem_pubkey_len = 32;
518+
let algo = KmHpkeAlgorithm {
481519
kem: KemAlgorithm::DhkemX25519HkdfSha256 as i32,
482520
kdf: KdfAlgorithm::HkdfSha256 as i32,
483521
aead: AeadAlgorithm::Aes256Gcm as i32,
484522
};
523+
let hpke_algo: HpkeAlgorithm = algo.into();
485524
unsafe {
486525
key_manager_generate_kem_keypair(
487526
algo,
@@ -490,7 +529,7 @@ mod tests {
490529
3600,
491530
uuid_bytes.as_mut_ptr(),
492531
kem_pubkey_bytes.as_mut_ptr(),
493-
&mut kem_pubkey_len,
532+
kem_pubkey_len,
494533
);
495534
}
496535

@@ -499,7 +538,7 @@ mod tests {
499538
let aad = b"test_aad";
500539
// We use `hpke_seal_raw` to act as the client to generate a valid encapsulation
501540
let (client_enc, client_ct) =
502-
km_common::crypto::hpke_seal_raw(&kem_pubkey_bytes, pt, aad, &algo).unwrap();
541+
km_common::crypto::hpke_seal_raw(&kem_pubkey_bytes, pt, aad, &hpke_algo).unwrap();
503542

504543
// Step 3: Call `decap_and_seal`.
505544
let mut out_enc_key = [0u8; 32];
@@ -524,32 +563,33 @@ mod tests {
524563
assert_eq!(result, 0);
525564

526565
// 4. Verify we can decrypt the result using binding_sk
566+
let binding_sk_priv = km_common::crypto::PrivateKey::try_from(binding_sk.clone()).unwrap();
527567
let recovered_shared_secret =
528-
km_common::crypto::hpke_open(&binding_sk, &out_enc_key, &out_ct, aad, &algo)
568+
km_common::crypto::hpke_open(&binding_sk_priv, &out_enc_key, &out_ct, aad, &hpke_algo)
529569
.expect("Failed to decrypt the resealed secret");
530570

531-
assert_eq!(recovered_shared_secret.len(), 32);
571+
assert_eq!(recovered_shared_secret.as_slice().len(), 32);
532572

533573
// 5. Verify the recovered secret matches what decaps would produce
534574
let key_record = KEY_REGISTRY.get_key(&Uuid::from_bytes(uuid_bytes)).unwrap();
575+
let private_key = km_common::crypto::PrivateKey::try_from(key_record.private_key.as_bytes().to_vec()).unwrap();
535576
let expected_shared_secret = km_common::crypto::decaps(
536-
key_record.private_key.as_bytes(),
577+
&private_key,
537578
&client_enc,
538-
KemAlgorithm::DhkemX25519HkdfSha256,
539579
)
540580
.expect("decaps failed");
541581
assert_eq!(
542-
recovered_shared_secret, expected_shared_secret,
582+
recovered_shared_secret.as_slice(), expected_shared_secret.as_slice(),
543583
"Recovered secret mismatch"
544584
);
545585

546586
// 6. Verify that this secret correctly decrypts the original client ciphertext
547587
// using the shared secret directly instead of the private key.
548588
let decrypted_pt = km_common::crypto::hpke_open_with_shared_secret(
549-
&recovered_shared_secret,
589+
recovered_shared_secret.as_slice(),
550590
&client_ct,
551591
aad,
552-
&algo,
592+
&hpke_algo,
553593
)
554594
.expect("Failed to decrypt client message with shared secret");
555595

@@ -610,20 +650,21 @@ mod tests {
610650

611651
// 2. Generate KEM key
612652
let mut uuid_bytes = [0u8; 16];
613-
let algo = HpkeAlgorithm {
653+
let algo = KmHpkeAlgorithm {
614654
kem: KemAlgorithm::DhkemX25519HkdfSha256 as i32,
615655
kdf: KdfAlgorithm::HkdfSha256 as i32,
616656
aead: AeadAlgorithm::Aes256Gcm as i32,
617657
};
658+
let mut kem_pubkey_bytes = [0u8; 32];
618659
unsafe {
619660
key_manager_generate_kem_keypair(
620661
algo,
621662
binding_pk.as_ptr(),
622663
binding_pk.len(),
623664
3600,
624665
uuid_bytes.as_mut_ptr(),
625-
std::ptr::null_mut(),
626-
std::ptr::null_mut(),
666+
kem_pubkey_bytes.as_mut_ptr(),
667+
32,
627668
);
628669
}
629670

@@ -659,8 +700,14 @@ mod tests {
659700
// 2. Generate KEM key
660701
let mut uuid_bytes = [0u8; 16];
661702
let mut kem_pubkey_bytes = [0u8; 32];
703+
let mut kem_pubkey_bytes = [0u8; 32];
662704
let mut kem_pubkey_len = 32;
663-
let algo = HpkeAlgorithm {
705+
let algo = KmHpkeAlgorithm {
706+
kem: KemAlgorithm::DhkemX25519HkdfSha256 as i32,
707+
kdf: KdfAlgorithm::HkdfSha256 as i32,
708+
aead: AeadAlgorithm::Aes256Gcm as i32,
709+
};
710+
let client_algo = HpkeAlgorithm {
664711
kem: KemAlgorithm::DhkemX25519HkdfSha256 as i32,
665712
kdf: KdfAlgorithm::HkdfSha256 as i32,
666713
aead: AeadAlgorithm::Aes256Gcm as i32,
@@ -673,13 +720,13 @@ mod tests {
673720
3600,
674721
uuid_bytes.as_mut_ptr(),
675722
kem_pubkey_bytes.as_mut_ptr(),
676-
&mut kem_pubkey_len,
723+
kem_pubkey_len,
677724
);
678725
}
679726

680727
// 3. Generate valid client encapsulation
681728
let (client_enc, _) =
682-
km_common::crypto::hpke_seal_raw(&kem_pubkey_bytes, b"secret", b"", &algo).unwrap();
729+
km_common::crypto::hpke_seal_raw(&kem_pubkey_bytes, b"secret", b"", &client_algo).unwrap();
683730

684731
// 4. Call with small output buffers
685732
let mut out_enc_key = [0u8; 31]; // Small
Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Package key_protection_service implements the Key Orchestration Layer (KOL)
22
// for the Key Protection Service. It wraps the KPS Key Custody Core (KCC) FFI
3-
// to provide a Go-native interface for KEM key generation.
3+
// to provide a Go-native interface for KEM key operations.
44
package key_protection_service
55

66
import "github.com/google/uuid"
@@ -10,18 +10,36 @@ type KEMKeyGenerator interface {
1010
GenerateKEMKeypair(bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, []byte, error)
1111
}
1212

13-
// Service implements KEMKeyGenerator by delegating to the KPS KCC FFI.
13+
// DecapSealer decapsulates a shared secret and reseals it with the binding key.
14+
type DecapSealer interface {
15+
DecapAndSeal(kemUUID uuid.UUID, encapsulatedKey, aad []byte) (sealEnc []byte, sealedCT []byte, err error)
16+
}
17+
18+
// Service implements KEMKeyGenerator and DecapSealer by delegating to the KPS KCC FFI.
1419
type Service struct {
1520
generateKEMKeypairFn func(bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, []byte, error)
21+
decapAndSealFn func(kemUUID uuid.UUID, encapsulatedKey, aad []byte) ([]byte, []byte, error)
1622
}
1723

18-
// NewService creates a new KPS KOL service with the given KCC function.
19-
func NewService(generateKEMKeypairFn func(bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, []byte, error)) *Service {
20-
return &Service{generateKEMKeypairFn: generateKEMKeypairFn}
24+
// NewService creates a new KPS KOL service with the given KCC functions.
25+
func NewService(
26+
generateKEMKeypairFn func(bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, []byte, error),
27+
decapAndSealFn func(kemUUID uuid.UUID, encapsulatedKey, aad []byte) ([]byte, []byte, error),
28+
) *Service {
29+
return &Service{
30+
generateKEMKeypairFn: generateKEMKeypairFn,
31+
decapAndSealFn: decapAndSealFn,
32+
}
2133
}
2234

2335
// GenerateKEMKeypair generates a KEM keypair linked to the provided binding
2436
// public key by calling the KPS KCC FFI.
2537
func (s *Service) GenerateKEMKeypair(bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, []byte, error) {
2638
return s.generateKEMKeypairFn(bindingPubKey, lifespanSecs)
2739
}
40+
41+
// DecapAndSeal decapsulates a shared secret using the stored KEM key and
42+
// reseals it with the associated binding public key by calling the KPS KCC FFI.
43+
func (s *Service) DecapAndSeal(kemUUID uuid.UUID, encapsulatedKey, aad []byte) ([]byte, []byte, error) {
44+
return s.decapAndSealFn(kemUUID, encapsulatedKey, aad)
45+
}

0 commit comments

Comments
 (0)