diff --git a/keymanager/key_protection_service/key_custody_core/kps_key_custody_core_cgo.go b/keymanager/key_protection_service/key_custody_core/kps_key_custody_core_cgo.go index 5fdc79b35..3058f1be5 100644 --- a/keymanager/key_protection_service/key_custody_core/kps_key_custody_core_cgo.go +++ b/keymanager/key_protection_service/key_custody_core/kps_key_custody_core_cgo.go @@ -61,3 +61,47 @@ func GenerateKEMKeypair(algo *keymanager.HpkeAlgorithm, bindingPubKey []byte, li copy(pubkey, pubkeyBuf[:pubkeyLen]) return id, pubkey, nil } + +// DecapAndSeal decapsulates a shared secret using the stored KEM key and +// reseals it with the associated binding public key via Rust FFI. +// Returns the new encapsulated key and sealed ciphertext. +func DecapAndSeal(kemUUID uuid.UUID, encapsulatedKey, aad []byte) ([]byte, []byte, error) { + if len(encapsulatedKey) == 0 { + return nil, nil, fmt.Errorf("encapsulated key must not be empty") + } + + uuidBytes := kemUUID[:] + + var outEncKey [32]byte + outEncKeyLen := C.size_t(len(outEncKey)) + var outCT [48]byte // 32-byte secret + 16-byte GCM tag + outCTLen := C.size_t(len(outCT)) + + var aadPtr *C.uint8_t + aadLen := C.size_t(0) + if len(aad) > 0 { + aadPtr = (*C.uint8_t)(unsafe.Pointer(&aad[0])) + aadLen = C.size_t(len(aad)) + } + + rc := C.key_manager_decap_and_seal( + (*C.uint8_t)(unsafe.Pointer(&uuidBytes[0])), + (*C.uint8_t)(unsafe.Pointer(&encapsulatedKey[0])), + C.size_t(len(encapsulatedKey)), + aadPtr, + aadLen, + (*C.uint8_t)(unsafe.Pointer(&outEncKey[0])), + outEncKeyLen, + (*C.uint8_t)(unsafe.Pointer(&outCT[0])), + outCTLen, + ) + if rc != 0 { + return nil, nil, fmt.Errorf("key_manager_decap_and_seal failed with code %d", rc) + } + + sealEnc := make([]byte, outEncKeyLen) + copy(sealEnc, outEncKey[:outEncKeyLen]) + sealedCT := make([]byte, outCTLen) + copy(sealedCT, outCT[:outCTLen]) + return sealEnc, sealedCT, nil +} diff --git a/keymanager/key_protection_service/key_custody_core/kps_key_custody_core_stub.go b/keymanager/key_protection_service/key_custody_core/kps_key_custody_core_stub.go index a23377e03..9155f1e78 100644 --- a/keymanager/key_protection_service/key_custody_core/kps_key_custody_core_stub.go +++ b/keymanager/key_protection_service/key_custody_core/kps_key_custody_core_stub.go @@ -14,3 +14,8 @@ import ( func GenerateKEMKeypair(_ *algorithms.HpkeAlgorithm, _ []byte, _ uint64) (uuid.UUID, []byte, error) { return uuid.Nil, nil, fmt.Errorf("GenerateKEMKeypair is not supported on this architecture") } + +// DecapAndSeal is a stub for architectures where the Rust library is not supported. +func DecapAndSeal(_ uuid.UUID, _, _ []byte) ([]byte, []byte, error) { + return nil, nil, fmt.Errorf("DecapAndSeal is not supported on this architecture") +} diff --git a/keymanager/key_protection_service/service.go b/keymanager/key_protection_service/service.go index 5ddd7ec34..4b557ce13 100644 --- a/keymanager/key_protection_service/service.go +++ b/keymanager/key_protection_service/service.go @@ -1,6 +1,6 @@ // Package keyprotectionservice implements the Key Orchestration Layer (KOL) // for the Key Protection Service. It wraps the KPS Key Custody Core (KCC) FFI -// to provide a Go-native interface for KEM key generation. +// to provide a Go-native interface for KEM key operations. package keyprotectionservice import ( @@ -14,14 +14,26 @@ type KEMKeyGenerator interface { GenerateKEMKeypair(algo *keymanager.HpkeAlgorithm, bindingPubKey []byte, lifespanSecs uint64) ([]byte, []byte, error) } -// Service implements KEMKeyGenerator by delegating to the KPS KCC FFI. +// DecapSealer decapsulates a shared secret and reseals it with the binding key. +type DecapSealer interface { + DecapAndSeal(kemUUID uuid.UUID, encapsulatedKey, aad []byte) (sealEnc []byte, sealedCT []byte, err error) +} + +// Service implements KEMKeyGenerator and DecapSealer by delegating to the KPS KCC FFI. type Service struct { generateKEMKeypairFn func(algo *keymanager.HpkeAlgorithm, bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, []byte, error) + decapAndSealFn func(kemUUID uuid.UUID, encapsulatedKey, aad []byte) ([]byte, []byte, error) } -// NewService creates a new KPS KOL service with the given KCC function. -func NewService(generateKEMKeypairFn func(algo *keymanager.HpkeAlgorithm, bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, []byte, error)) *Service { - return &Service{generateKEMKeypairFn: generateKEMKeypairFn} +// NewService creates a new KPS KOL service with the given KCC functions. +func NewService( + generateKEMKeypairFn func(algo *keymanager.HpkeAlgorithm, bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, []byte, error), + decapAndSealFn func(kemUUID uuid.UUID, encapsulatedKey, aad []byte) ([]byte, []byte, error), +) *Service { + return &Service{ + generateKEMKeypairFn: generateKEMKeypairFn, + decapAndSealFn: decapAndSealFn, + } } // GenerateKEMKeypair generates a KEM keypair linked to the provided binding @@ -29,3 +41,9 @@ func NewService(generateKEMKeypairFn func(algo *keymanager.HpkeAlgorithm, bindin func (s *Service) GenerateKEMKeypair(algo *keymanager.HpkeAlgorithm, bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, []byte, error) { return s.generateKEMKeypairFn(algo, bindingPubKey, lifespanSecs) } + +// DecapAndSeal decapsulates a shared secret using the stored KEM key and +// reseals it with the associated binding public key by calling the KPS KCC FFI. +func (s *Service) DecapAndSeal(kemUUID uuid.UUID, encapsulatedKey, aad []byte) ([]byte, []byte, error) { + return s.decapAndSealFn(kemUUID, encapsulatedKey, aad) +} diff --git a/keymanager/key_protection_service/service_test.go b/keymanager/key_protection_service/service_test.go index 542feb4f4..25eeaace2 100644 --- a/keymanager/key_protection_service/service_test.go +++ b/keymanager/key_protection_service/service_test.go @@ -9,6 +9,11 @@ import ( keymanager "github.com/google/go-tpm-tools/keymanager/km_common/proto" ) +// noopDecapAndSeal is a placeholder for tests that don't exercise DecapAndSeal. +func noopDecapAndSeal(_ uuid.UUID, _, _ []byte) ([]byte, []byte, error) { + return nil, nil, nil +} + func TestServiceGenerateKEMKeypairSuccess(t *testing.T) { expectedUUID := uuid.New() expectedPubKey := make([]byte, 32) @@ -24,7 +29,7 @@ func TestServiceGenerateKEMKeypairSuccess(t *testing.T) { t.Fatalf("expected lifespanSecs 7200, got %d", lifespanSecs) } return expectedUUID, expectedPubKey, nil - }) + }, noopDecapAndSeal) id, pubKey, err := svc.GenerateKEMKeypair(&keymanager.HpkeAlgorithm{}, make([]byte, 32), 7200) if err != nil { @@ -41,10 +46,45 @@ func TestServiceGenerateKEMKeypairSuccess(t *testing.T) { func TestServiceGenerateKEMKeypairError(t *testing.T) { svc := NewService(func(_ *keymanager.HpkeAlgorithm, _ []byte, _ uint64) (uuid.UUID, []byte, error) { return uuid.Nil, nil, fmt.Errorf("FFI error") - }) + }, noopDecapAndSeal) _, _, err := svc.GenerateKEMKeypair(&keymanager.HpkeAlgorithm{}, make([]byte, 32), 3600) if err == nil { t.Fatal("expected error, got nil") } } + +func TestServiceDecapAndSealSuccess(t *testing.T) { + kemUUID := uuid.New() + expectedSealEnc := []byte("seal-enc-key") + expectedSealedCT := []byte("sealed-ciphertext") + + svc := NewService(nil, func(id uuid.UUID, _, _ []byte) ([]byte, []byte, error) { + if id != kemUUID { + t.Fatalf("expected KEM UUID %s, got %s", kemUUID, id) + } + return expectedSealEnc, expectedSealedCT, nil + }) + + sealEnc, sealedCT, err := svc.DecapAndSeal(kemUUID, []byte("enc-key"), []byte("aad")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if string(sealEnc) != string(expectedSealEnc) { + t.Fatalf("expected seal enc %q, got %q", expectedSealEnc, sealEnc) + } + if string(sealedCT) != string(expectedSealedCT) { + t.Fatalf("expected sealed CT %q, got %q", expectedSealedCT, sealedCT) + } +} + +func TestServiceDecapAndSealError(t *testing.T) { + svc := NewService(nil, func(_ uuid.UUID, _, _ []byte) ([]byte, []byte, error) { + return nil, nil, fmt.Errorf("decap FFI error") + }) + + _, _, err := svc.DecapAndSeal(uuid.New(), []byte("enc-key"), nil) + if err == nil { + t.Fatal("expected error, got nil") + } +} diff --git a/keymanager/workload_service/integration_test.go b/keymanager/workload_service/integration_test.go index 89978b628..f442d6d73 100644 --- a/keymanager/workload_service/integration_test.go +++ b/keymanager/workload_service/integration_test.go @@ -24,10 +24,17 @@ func (r *realWorkloadService) GenerateBindingKeypair(algo *keymanager.HpkeAlgori return wskcc.GenerateBindingKeypair(algo, lifespanSecs) } +// realOpener wraps the actual WSD KCC FFI for Open. +type realOpener struct{} + +func (r *realOpener) Open(bindingUUID uuid.UUID, enc, ciphertext, aad []byte) ([]byte, error) { + return wskcc.Open(bindingUUID, enc, ciphertext, aad) +} + func TestIntegrationGenerateKeysEndToEnd(t *testing.T) { // Wire up real FFI calls: WSD KCC for binding, KPS KCC (via KPS KOL) for KEM. - kpsSvc := kps.NewService(kpskcc.GenerateKEMKeypair) - srv := NewServer(kpsSvc, &realWorkloadService{}) + kpsSvc := kps.NewService(kpskcc.GenerateKEMKeypair, kpskcc.DecapAndSeal) + srv, _ := NewServer(kpsSvc, &realWorkloadService{}, "test.sock") reqBody, err := json.Marshal(GenerateKemRequest{ Algorithm: KemAlgorithmDHKEMX25519HKDFSHA256, @@ -72,8 +79,8 @@ func TestIntegrationGenerateKeysEndToEnd(t *testing.T) { } func TestIntegrationGenerateKeysUniqueMappings(t *testing.T) { - kpsSvc := kps.NewService(kpskcc.GenerateKEMKeypair) - srv := NewServer(kpsSvc, &realWorkloadService{}) + kpsSvc := kps.NewService(kpskcc.GenerateKEMKeypair, kpskcc.DecapAndSeal) + srv, _ := NewServer(kpsSvc, &realWorkloadService{}, "test.sock") // Generate two key sets. var kemUUIDs [2]uuid.UUID diff --git a/keymanager/workload_service/key_custody_core/ws_key_custody_core_cgo.go b/keymanager/workload_service/key_custody_core/ws_key_custody_core_cgo.go index 9a14f1652..8e5bdd20c 100644 --- a/keymanager/workload_service/key_custody_core/ws_key_custody_core_cgo.go +++ b/keymanager/workload_service/key_custody_core/ws_key_custody_core_cgo.go @@ -54,3 +54,49 @@ func GenerateBindingKeypair(algo *keymanager.HpkeAlgorithm, lifespanSecs uint64) copy(pubkey, pubkeyBuf[:pubkeyLen]) return id, pubkey, nil } + +// Open decrypts a sealed ciphertext using the binding key identified by +// bindingUUID via Rust FFI (HPKE Open). +// Returns the decrypted plaintext (shared secret). +func Open(bindingUUID uuid.UUID, enc, ciphertext, aad []byte) ([]byte, error) { + if len(enc) == 0 { + return nil, fmt.Errorf("enc must not be empty") + } + if len(ciphertext) == 0 { + return nil, fmt.Errorf("ciphertext must not be empty") + } + + uuidBytes := bindingUUID[:] + + var outPT [32]byte + outPTLen := C.size_t(len(outPT)) + + // Rust key_manager_open requires non-null aad pointer. + // Use a sentinel byte so the pointer is always valid. + var aadSentinel [1]byte + aadPtr := (*C.uint8_t)(unsafe.Pointer(&aadSentinel[0])) + aadLen := C.size_t(0) + if len(aad) > 0 { + aadPtr = (*C.uint8_t)(unsafe.Pointer(&aad[0])) + aadLen = C.size_t(len(aad)) + } + + rc := C.key_manager_open( + (*C.uint8_t)(unsafe.Pointer(&uuidBytes[0])), + (*C.uint8_t)(unsafe.Pointer(&enc[0])), + C.size_t(len(enc)), + (*C.uint8_t)(unsafe.Pointer(&ciphertext[0])), + C.size_t(len(ciphertext)), + aadPtr, + aadLen, + (*C.uint8_t)(unsafe.Pointer(&outPT[0])), + outPTLen, + ) + if rc != 0 { + return nil, fmt.Errorf("key_manager_open failed with code %d", rc) + } + + plaintext := make([]byte, outPTLen) + copy(plaintext, outPT[:outPTLen]) + return plaintext, nil +} diff --git a/keymanager/workload_service/key_custody_core/ws_key_custody_core_stub.go b/keymanager/workload_service/key_custody_core/ws_key_custody_core_stub.go index b13e21be3..199bce9e2 100644 --- a/keymanager/workload_service/key_custody_core/ws_key_custody_core_stub.go +++ b/keymanager/workload_service/key_custody_core/ws_key_custody_core_stub.go @@ -14,3 +14,8 @@ import ( func GenerateBindingKeypair(_ *algorithms.HpkeAlgorithm, _ uint64) (uuid.UUID, []byte, error) { return uuid.Nil, nil, fmt.Errorf("GenerateBindingKeypair is not supported on this architecture") } + +// Open is a stub for architectures where the Rust library is not supported. +func Open(_ uuid.UUID, _, _, _ []byte) ([]byte, error) { + return nil, fmt.Errorf("Open is not supported on this architecture") +} diff --git a/keymanager/workload_service/server.go b/keymanager/workload_service/server.go index ae976e29c..c3d7de7c6 100644 --- a/keymanager/workload_service/server.go +++ b/keymanager/workload_service/server.go @@ -1,10 +1,11 @@ // Package workloadservice implements the Key Orchestration Layer (KOL) for the // Workload Service Daemon (WSD). It provides an HTTP server on a unix socket -// exposing key generation endpoints. +// exposing key management endpoints. package workloadservice import ( "context" + "encoding/base64" "encoding/json" "fmt" "math" @@ -23,12 +24,14 @@ import ( // WorkloadService defines the interface for generating binding keypairs. type WorkloadService interface { GenerateBindingKeypair(algo *keymanager.HpkeAlgorithm, lifespanSecs uint64) (uuid.UUID, []byte, error) + Open(bindingUUID uuid.UUID, enc, ciphertext, aad []byte) ([]byte, error) } type keyProtectionService struct{} // KeyProtectionService defines the interface for generating KEM keypairs. type KeyProtectionService interface { GenerateKEMKeypair(algo *keymanager.HpkeAlgorithm, bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, []byte, error) + DecapAndSeal(kemUUID uuid.UUID, encapsulatedKey, aad []byte) (sealEnc []byte, sealedCT []byte, err error) } type workloadService struct{} @@ -36,10 +39,18 @@ func (r *workloadService) GenerateBindingKeypair(algo *keymanager.HpkeAlgorithm, return wskcc.GenerateBindingKeypair(algo, lifespanSecs) } +func (r *workloadService) Open(bindingUUID uuid.UUID, enc, ciphertext, aad []byte) ([]byte, error) { + return wskcc.Open(bindingUUID, enc, ciphertext, aad) +} + func (r *keyProtectionService) GenerateKEMKeypair(algo *keymanager.HpkeAlgorithm, bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, []byte, error) { return kpscc.GenerateKEMKeypair(algo, bindingPubKey, lifespanSecs) } +func (r *keyProtectionService) DecapAndSeal(kemUUID uuid.UUID, encapsulatedKey, aad []byte) (sealEnc []byte, sealedCT []byte, err error) { + return kpscc.DecapAndSeal(kemUUID, encapsulatedKey, aad) +} + // KeyHandle represents a key handle returned from the API. type KeyHandle struct { Handle string `json:"handle"` @@ -101,6 +112,29 @@ type GetCapabilitiesResponse struct { SupportedAlgorithms []SupportedAlgorithm `json:"supported_algorithms"` } +// KemCiphertext carries raw encapsulated key bytes and the KEM algorithm. +type KemCiphertext struct { + Algorithm KemAlgorithm `json:"algorithm"` + Ciphertext string `json:"ciphertext"` // base64-encoded raw bytes +} + +// DecapsRequest is the JSON body for POST /keys:decap. +type DecapsRequest struct { + KeyHandle KeyHandle `json:"key_handle"` + Ciphertext KemCiphertext `json:"ciphertext"` +} + +// KemSharedSecret is the Decaps result payload. +type KemSharedSecret struct { + Algorithm KemAlgorithm `json:"algorithm"` + Secret string `json:"secret"` // base64-encoded raw bytes +} + +// DecapsResponse is returned by POST /v1/keys:decap. +type DecapsResponse struct { + SharedSecret KemSharedSecret `json:"shared_secret"` +} + // Server is the WSD HTTP server. type Server struct { keyProtectionService KeyProtectionService @@ -129,9 +163,9 @@ func NewServer(keyProtectionService KeyProtectionService, workloadService Worklo } mux := http.NewServeMux() + mux.HandleFunc("/v1/keys:decap", s.handleDecaps) mux.HandleFunc("POST /v1/keys:generate_kem", s.handleGenerateKem) mux.HandleFunc("GET /v1/capabilities", s.handleGetCapabilities) - s.httpServer = &http.Server{Handler: mux} _ = os.Remove(socketPath) @@ -166,6 +200,80 @@ func (s *Server) LookupBindingUUID(kemUUID uuid.UUID) (uuid.UUID, bool) { return id, ok } +func decapsAADContext(kemUUID uuid.UUID, algorithm KemAlgorithm) []byte { + // Bind the KPS->WSD transport ciphertext to this decapsulation context. + // Note: The AAD context string retains `decaps` as it is part of the internal binding protocol + // and changing it might affect backward compatibility if keys were already persisted (though lifespan is short). + // For API alignment, we only change the external endpoint and JSON. + return []byte(fmt.Sprintf("wsd:keys:decaps:v1:%d:%s", algorithm, kemUUID)) +} + +func (s *Server) handleDecaps(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + var req DecapsRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, fmt.Sprintf("invalid request body: %v", err), http.StatusBadRequest) + return + } + + if req.Ciphertext.Algorithm != KemAlgorithmDHKEMX25519HKDFSHA256 { + http.Error(w, fmt.Sprintf("unsupported ciphertext algorithm: %d", req.Ciphertext.Algorithm), http.StatusBadRequest) + return + } + + kemUUID, err := uuid.Parse(req.KeyHandle.Handle) + if err != nil { + http.Error(w, fmt.Sprintf("invalid key_handle.handle: %v", err), http.StatusBadRequest) + return + } + + encapsulatedKey, err := base64.StdEncoding.DecodeString(req.Ciphertext.Ciphertext) + if err != nil { + http.Error(w, fmt.Sprintf("invalid ciphertext.ciphertext base64: %v", err), http.StatusBadRequest) + return + } + if len(encapsulatedKey) == 0 { + http.Error(w, "ciphertext.ciphertext must not be empty", http.StatusBadRequest) + return + } + aad := decapsAADContext(kemUUID, req.Ciphertext.Algorithm) + + // Step 1: Look up the binding UUID for this KEM key. + bindingUUID, ok := s.LookupBindingUUID(kemUUID) + if !ok { + http.Error(w, fmt.Sprintf("KEM key handle not found: %s", kemUUID), http.StatusNotFound) + return + } + + // Step 2: Decapsulate and reseal via KPS. + sealEnc, sealedCT, err := s.keyProtectionService.DecapAndSeal(kemUUID, encapsulatedKey, aad) + if err != nil { + http.Error(w, fmt.Sprintf("failed to decap and seal: %v", err), http.StatusInternalServerError) + return + } + + // Step 3: Open the sealed secret using the binding key via WSD KCC. + plaintext, err := s.workloadService.Open(bindingUUID, sealEnc, sealedCT, aad) + if err != nil { + http.Error(w, fmt.Sprintf("failed to open sealed secret: %v", err), http.StatusInternalServerError) + return + } + + // Step 4: Return the shared secret. + resp := DecapsResponse{ + SharedSecret: KemSharedSecret{ + Algorithm: req.Ciphertext.Algorithm, + Secret: base64.StdEncoding.EncodeToString(plaintext), + }, + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(resp) +} + func (s *Server) handleGenerateKem(w http.ResponseWriter, r *http.Request) { var req GenerateKemRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -255,7 +363,7 @@ func writeJSON(w http.ResponseWriter, v any, code int) { w.Header().Set("Content-Type", "application/json") w.Header().Set("X-Content-Type-Options", "nosniff") w.WriteHeader(code) - json.NewEncoder(w).Encode(v) + _ = json.NewEncoder(w).Encode(v) } // writeError writes a JSON error response. diff --git a/keymanager/workload_service/server_test.go b/keymanager/workload_service/server_test.go index f6ff7db3e..c11d9baf2 100644 --- a/keymanager/workload_service/server_test.go +++ b/keymanager/workload_service/server_test.go @@ -2,6 +2,7 @@ package workloadservice import ( "bytes" + "encoding/base64" "encoding/json" "fmt" "net/http" @@ -25,15 +26,28 @@ func newTestServer(t *testing.T, kemGen KeyProtectionService, bindingGen Workloa // mockWorkloadService implements WorkloadService for testing. type mockWorkloadService struct { - uuid uuid.UUID - pubKey []byte - err error + uuid uuid.UUID + pubKey []byte + err error + plaintext []byte + receivedUUID uuid.UUID + receivedEnc []byte + receivedCT []byte + receivedAAD []byte } func (m *mockWorkloadService) GenerateBindingKeypair(_ *keymanager.HpkeAlgorithm, _ uint64) (uuid.UUID, []byte, error) { return m.uuid, m.pubKey, m.err } +func (m *mockWorkloadService) Open(bindingUUID uuid.UUID, enc, ciphertext, aad []byte) ([]byte, error) { + m.receivedUUID = bindingUUID + m.receivedEnc = enc + m.receivedCT = ciphertext + m.receivedAAD = aad + return m.plaintext, m.err +} + // mockKeyProtectionService implements KeyProtectionService for testing. type mockKeyProtectionService struct { uuid uuid.UUID @@ -41,6 +55,11 @@ type mockKeyProtectionService struct { err error receivedPubKey []byte receivedLifespan uint64 + sealEnc []byte + sealedCT []byte + receivedKEMUUID uuid.UUID + receivedEncKey []byte + receivedAAD []byte } func (m *mockKeyProtectionService) GenerateKEMKeypair(_ *keymanager.HpkeAlgorithm, bindingPubKey []byte, lifespanSecs uint64) (uuid.UUID, []byte, error) { @@ -49,6 +68,13 @@ func (m *mockKeyProtectionService) GenerateKEMKeypair(_ *keymanager.HpkeAlgorith return m.uuid, m.pubKey, m.err } +func (m *mockKeyProtectionService) DecapAndSeal(kemUUID uuid.UUID, encapsulatedKey, aad []byte) ([]byte, []byte, error) { + m.receivedKEMUUID = kemUUID + m.receivedEncKey = encapsulatedKey + m.receivedAAD = aad + return m.sealEnc, m.sealedCT, m.err +} + func validGenerateBody() []byte { body, _ := json.Marshal(GenerateKemRequest{ Algorithm: KemAlgorithmDHKEMX25519HKDFSHA256, @@ -447,3 +473,189 @@ func TestHandleGetCapabilitiesInvalidMethod(t *testing.T) { t.Fatalf("expected status 405, got %d", w.Code) } } + +// --- /keys:decap tests --- + +// newDecapsTestServer creates a server pre-populated with a KEM→Binding mapping. +func newDecapsTestServer(t *testing.T, kemUUID, bindingUUID uuid.UUID, ds *mockKeyProtectionService, op *mockWorkloadService) *Server { + srv := newTestServer(t, ds, op) + srv.mu.Lock() + srv.kemToBindingMap[kemUUID] = bindingUUID + srv.mu.Unlock() + return srv +} + +func decapsRequestBody(kemUUID uuid.UUID, algo KemAlgorithm, encKey []byte) string { + return fmt.Sprintf( + `{"key_handle":{"handle":"%s"},"ciphertext":{"algorithm":"%s","ciphertext":"%s"}}`, + kemUUID.String(), + algo, + base64.StdEncoding.EncodeToString(encKey), + ) +} + +func TestHandleDecapsSuccess(t *testing.T) { + kemUUID := uuid.New() + bindingUUID := uuid.New() + encKey := []byte("test-encapsulated-key-32-bytes!!") + sealEnc := []byte("seal-encapsulated-key-32-bytes!!") + sealedCT := []byte("sealed-ciphertext-48-bytes-with-tag!!!!!!!!!!!!!!") + plaintext := []byte("shared-secret-32-bytes-value!!!!") // 32 bytes + expectedAAD := decapsAADContext(kemUUID, KemAlgorithmDHKEMX25519HKDFSHA256) + + ds := &mockKeyProtectionService{sealEnc: sealEnc, sealedCT: sealedCT} + op := &mockWorkloadService{plaintext: plaintext} + srv := newDecapsTestServer(t, kemUUID, bindingUUID, ds, op) + + body := decapsRequestBody(kemUUID, KemAlgorithmDHKEMX25519HKDFSHA256, encKey) + req := httptest.NewRequest(http.MethodPost, "/v1/keys:decap", strings.NewReader(body)) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String()) + } + + var resp DecapsResponse + if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + if resp.SharedSecret.Algorithm != KemAlgorithmDHKEMX25519HKDFSHA256 { + t.Fatalf("expected shared_secret.algorithm=%d, got %d", KemAlgorithmDHKEMX25519HKDFSHA256, resp.SharedSecret.Algorithm) + } + + decoded, err := base64.StdEncoding.DecodeString(resp.SharedSecret.Secret) + if err != nil { + t.Fatalf("failed to base64-decode shared secret: %v", err) + } + if string(decoded) != string(plaintext) { + t.Fatalf("expected plaintext %q, got %q", plaintext, decoded) + } + + // Verify DecapSealer received correct args. + if ds.receivedKEMUUID != kemUUID { + t.Fatalf("expected DecapSealer to receive KEM UUID %s, got %s", kemUUID, ds.receivedKEMUUID) + } + if string(ds.receivedEncKey) != string(encKey) { + t.Fatalf("expected DecapSealer to receive enc key %q, got %q", encKey, ds.receivedEncKey) + } + if string(ds.receivedAAD) != string(expectedAAD) { + t.Fatalf("expected DecapSealer to receive AAD %q, got %q", expectedAAD, ds.receivedAAD) + } + + // Verify Opener received correct args. + if op.receivedUUID != bindingUUID { + t.Fatalf("expected Opener to receive binding UUID %s, got %s", bindingUUID, op.receivedUUID) + } + if string(op.receivedEnc) != string(sealEnc) { + t.Fatalf("expected Opener to receive enc %q, got %q", sealEnc, op.receivedEnc) + } + if string(op.receivedCT) != string(sealedCT) { + t.Fatalf("expected Opener to receive CT %q, got %q", sealedCT, op.receivedCT) + } + if string(op.receivedAAD) != string(expectedAAD) { + t.Fatalf("expected Opener to receive AAD %q, got %q", expectedAAD, op.receivedAAD) + } +} + +func TestHandleDecapsMethodNotAllowed(t *testing.T) { + srv := newTestServer(t, &mockKeyProtectionService{}, &mockWorkloadService{}) + + req := httptest.NewRequest(http.MethodGet, "/v1/keys:decap", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusMethodNotAllowed { + t.Fatalf("expected status 405, got %d", w.Code) + } +} + +func TestHandleDecapsBadRequestBody(t *testing.T) { + srv := newTestServer(t, &mockKeyProtectionService{}, &mockWorkloadService{}) + + req := httptest.NewRequest(http.MethodPost, "/v1/keys:decap", strings.NewReader("not-json")) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected status 400, got %d", w.Code) + } +} + +func TestHandleDecapsInvalidKEMUUID(t *testing.T) { + srv := newTestServer(t, &mockKeyProtectionService{}, &mockWorkloadService{}) + + body := `{"key_handle":{"handle":"not-a-uuid"},"ciphertext":{"algorithm":1,"ciphertext":"AAAA"}}` + req := httptest.NewRequest(http.MethodPost, "/v1/keys:decap", strings.NewReader(body)) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected status 400, got %d", w.Code) + } +} + +func TestHandleDecapsKEMKeyNotFound(t *testing.T) { + kemUUID := uuid.New() + srv := newTestServer(t, &mockKeyProtectionService{}, &mockWorkloadService{}) + // Don't populate kemToBindingMap. + + body := decapsRequestBody(kemUUID, KemAlgorithmDHKEMX25519HKDFSHA256, []byte("enc-key")) + req := httptest.NewRequest(http.MethodPost, "/v1/keys:decap", strings.NewReader(body)) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Fatalf("expected status 404, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestHandleDecapsDecapSealError(t *testing.T) { + kemUUID := uuid.New() + bindingUUID := uuid.New() + + ds := &mockKeyProtectionService{err: fmt.Errorf("decap FFI error")} + srv := newDecapsTestServer(t, kemUUID, bindingUUID, ds, &mockWorkloadService{}) + + body := decapsRequestBody(kemUUID, KemAlgorithmDHKEMX25519HKDFSHA256, []byte("enc-key")) + req := httptest.NewRequest(http.MethodPost, "/v1/keys:decap", strings.NewReader(body)) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Fatalf("expected status 500, got %d", w.Code) + } +} + +func TestHandleDecapsOpenError(t *testing.T) { + kemUUID := uuid.New() + bindingUUID := uuid.New() + + ds := &mockKeyProtectionService{sealEnc: []byte("enc"), sealedCT: []byte("ct")} + op := &mockWorkloadService{err: fmt.Errorf("open FFI error")} + srv := newDecapsTestServer(t, kemUUID, bindingUUID, ds, op) + + body := decapsRequestBody(kemUUID, KemAlgorithmDHKEMX25519HKDFSHA256, []byte("enc-key")) + req := httptest.NewRequest(http.MethodPost, "/v1/keys:decap", strings.NewReader(body)) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Fatalf("expected status 500, got %d", w.Code) + } +} + +func TestHandleDecapsUnsupportedAlgorithm(t *testing.T) { + kemUUID := uuid.New() + bindingUUID := uuid.New() + srv := newDecapsTestServer(t, kemUUID, bindingUUID, &mockKeyProtectionService{}, &mockWorkloadService{}) + + body := decapsRequestBody(kemUUID, KemAlgorithm(999), []byte("enc-key")) + req := httptest.NewRequest(http.MethodPost, "/v1/keys:decap", strings.NewReader(body)) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Fatalf("expected status 400, got %d", w.Code) + } +}