Skip to content

Commit ecb590a

Browse files
committed
PS-9697 POC prototype of KMIP client library
The "kmipclient" C++ library implementation is added, that wraps low-level "kmip.h" calls and bypasses mid-level "kmip_bio" level.
1 parent 00496b5 commit ecb590a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3578
-8
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
build
2+
generated-docs

CMakeLists.txt

+18
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,22 @@ set(KMIP_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
1010

1111
add_subdirectory(libkmip/src)
1212
add_subdirectory(kmippp)
13+
add_subdirectory(kmipclient)
1314

15+
find_package(Doxygen REQUIRED)
16+
17+
if(DOXYGEN_FOUND)
18+
configure_file(Doxyfile ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile COPYONLY)
19+
20+
add_custom_target(doc
21+
COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile
22+
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
23+
COMMENT "Generating API documentation with Doxygen"
24+
VERBATIM
25+
)
26+
27+
# Make the 'doc' target depend on your build targets if necessary
28+
# add_dependencies(doc your_library your_executable)
29+
else()
30+
message(STATUS "Doxygen not found, skipping documentation generation.")
31+
endif()

Doxyfile

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Project information
2+
PROJECT_NAME = libkmip
3+
PROJECT_NUMBER = 0.4.0
4+
OUTPUT_DIRECTORY = generated-docs
5+
6+
# Input settings
7+
INPUT = . # Scan the current directory for source files
8+
RECURSIVE = YES
9+
10+
# Output settings
11+
GENERATE_LATEX = NO
12+
GENERATE_MAN = NO
13+
GENERATE_RTF = NO
14+
GENERATE_XML = NO
15+
GENERATE_HTMLHELP = YES
16+
17+
# UML related settings
18+
UML_LOOK = YES
19+
HAVE_DOT = YES
20+
DOT_PATH = /usr/bin/dot # Adjust this path to where your 'dot' executable is located
21+
PLANTUML_JAR_PATH = /usr/share/java/plantuml.jar
22+
PLANTUML_PREPROC = NO
23+
PLANTUML_INCLUDE_PATH =
24+
PLANTUML_CONFIG_FILE =
25+
# Enable class diagram generation
26+
CLASS_DIAGRAMS = YES
27+
COLLABORATION_GRAPH = YES
28+
UML_LIMIT_NUM_FIELDS = 50
29+
TEMPLATE_RELATIONS = YES
30+
MAX_DOT_GRAPH_DEPTH = 0
31+
MAX_DOT_GRAPH_NODES = 0
32+
HIDE_UNDOC_MEMBERS = NO
33+
HIDE_VIRTUAL_FUNCTIONS = NO
34+
SHOW_INCLUDE_FILES = YES
35+
SHOW_USED_FILES = YES
36+
SHOW_FILES = YES

kmipclient/CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Apr, 2025 Version 0.1.0
2+
3+
Initial implementation of all functionality available in "kmippp"

kmipclient/CMakeLists.txt

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
2+
set(CMAKE_CXX_STANDARD 23)
3+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
4+
set(CMAKE_CXX_EXTENSIONS OFF) # Optional, but recommended for standard compliance
5+
6+
add_library(
7+
kmipclient
8+
STATIC
9+
include/KmipClient.hpp
10+
src/KmipClient.cpp
11+
include/NetClient.hpp
12+
src/NetClientOpenSSL.cpp
13+
include/NetClientOpenSSL.hpp
14+
include/v_expected.hpp
15+
include/kmip_data_types.hpp
16+
src/RequestFactory.cpp
17+
src/RequestFactory.hpp
18+
src/KmipCtx.hpp
19+
src/KmipRequest.hpp
20+
src/IOUtils.cpp
21+
src/IOUtils.hpp
22+
include/Kmip.hpp
23+
src/ResponseResult.cpp
24+
src/ResponseResult.hpp
25+
src/kmip_exceptions.hpp
26+
src/AttributesFactory.cpp
27+
src/AttributesFactory.hpp
28+
src/KeyFactory.cpp
29+
src/KeyFactory.hpp
30+
src/Key.cpp
31+
include/Key.hpp
32+
src/StringUtils.cpp
33+
src/StringUtils.hpp
34+
include/Logger.hpp
35+
)
36+
37+
target_link_libraries(kmipclient kmip)
38+
set_property(TARGET kmipclient PROPERTY POSITION_INDEPENDENT_CODE ON)
39+
40+
target_include_directories(
41+
kmipclient PUBLIC
42+
$<BUILD_INTERFACE:${KMIP_SOURCE_DIR}/kmipclient/>
43+
$<INSTALL_INTERFACE:include>
44+
)
45+
46+
set_target_properties(
47+
kmipclient PROPERTIES PUBLIC_HEADER "Kmip.hpp"
48+
)
49+
50+
export(TARGETS kmip kmipclient FILE "kmipclient.cmake")
51+
52+
install(
53+
TARGETS kmipclient
54+
EXPORT kmipclient
55+
DESTINATION cmake
56+
ARCHIVE DESTINATION lib
57+
PUBLIC_HEADER DESTINATION include/
58+
LIBRARY DESTINATION lib)
59+
60+
macro(add_example name)
61+
add_executable(example_${name} examples/example_${name}.cpp)
62+
target_link_libraries(example_${name} kmipclient)
63+
endmacro()
64+
65+
add_example(create_aes)
66+
add_example(register_secret)
67+
add_example(activate)
68+
add_example(get)
69+
add_example(get_name)
70+
add_example(get_secret)
71+
add_example(revoke)
72+
add_example(destroy)
73+
add_example(register_key)
74+
add_example(locate)
75+
add_example(locate_by_group)
76+
add_example(get_all_ids)

kmipclient/README.md

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
The "kmipclient" library
2+
--
3+
KMIP client is the C++ library that allows simple access to the KMIP servers using the KMIP protocol.
4+
5+
The "kmipclient" library wraps up the low-level libkmip (kmip.h, kmip.c) into C++ code.
6+
The purpose of such wrap-up is to:
7+
8+
## Design goals.
9+
10+
1. Provide easy to use and hard to misuse interface with forced error processing.
11+
2. Hide low-level details.
12+
3. Minimize manual memory management
13+
4. Make the library easy to extend
14+
5. Exclude mid-level (kmip_bio.c), use the low-level (kmip.c) only
15+
6. Easy to replace network communication level
16+
7. Testability
17+
18+
## External dependencies
19+
20+
No extra external dependencies should be used, except existing OpenSSL dependency.
21+
KmipClient itself does not depend on any library except "kmip". The network communication level is injected
22+
into KmipClient instance as implementation of the NetClient interface. The library has ready to use
23+
OpenSSL BIO based implementation called NetClientOpenSSL. User of the library can use any other library to
24+
implement the communication level.
25+
26+
## High level design
27+
28+
The top interface wraps network communication level (based on OpenSSL) and the KMIP protocol encoding level.
29+
It is implemented as header-only class in the file “Kmip.hpp” and can be used similar to the old C++ wrapper
30+
(kmippp.h). Actual high level interface consists of two headers: NetClient.hpp. and KmipClient.hpp.
31+
32+
The first interface is just a contract to wrap low-level network communications similar to well-known
33+
interfaces (socket, OpenSSL bio and others). It contains 4 methods only: connect(), close(), send()
34+
and receive(). This interface also has an implementation, declared “NetClientOpenSSL.hpp”.
35+
It is based on OpenSSL BIO functions.
36+
37+
The second interface is actual KMIP protocol implementation. It requires a NetClient implementation
38+
as a dependency injection in the constructor. This interface is also similar to the existing C++ wrapper
39+
and can be used the similar whay when properly initialized with the NetClient-derived instance.
40+
41+
The main difference to the “kmippp.h” is in-band error processing. It uses a template similar to
42+
std::expected from the C++ 23. Though, project may use older C++ standard (C++ 20), so the interface
43+
includes a C++ 20 implementation, that wraps standard implementation or provides replacement if it is absent.
44+
45+
All KMIP request creation and encoding are encapsulated in the RequestFactory class. All operations are
46+
on stack and do not require memory management.
47+
48+
All KMIP responses processing are encapsulated in ResponseFactory class. It should be operated on stack
49+
to keep data in place. Copy and move operations are disabled.
50+
51+
By the protocol, parsed response contains one or more response batch items. To process these items,
52+
ResponseFactory class is used. It’s purpose is to extract values from the response batch item. V
53+
alues are keys, secrets, attributes, etc. This class does not have a state and consists of static methods.
54+
55+
All operation in the low-level KMIP library are based on context structure KMIP. This structure is
56+
encapsulated in KmipCtx class along with operations on buffers, errors, etc. This class, once created,
57+
is passed by the reference to other classes of the “kmipclient” library. Copy and move operations are
58+
disabled for this class also. Usually, the instance of this class is created on stack in the high-level
59+
methods and does not require memory management.
60+
61+
The high-level interface usage example:
62+
63+
```C++
64+
NetClientOpenSSL net_client (argv[1], argv[2], argv[3], argv[4], argv[5], 200);
65+
KmipClient client (net_client);
66+
67+
const auto opt_key = client.op_get_key (argv[6]);
68+
if (opt_key.has_value ())
69+
{
70+
std::cout << "Key: 0x";
71+
auto k = opt_key.value ();
72+
print_hex (k.value());
73+
}
74+
else
75+
{
76+
std::cerr << "Can not get key with id:"<< argv[6] << " Cause: "<< opt_key.error().message << std::endl;
77+
};
78+
```
79+
As can be seen from the code above, the NetClientOpenSSL class instance is injected as dependency
80+
inversion into the KmipClient class instance. This approach allows to use any net connection with KmipClient.
81+
It is enough to derive the class from NetClient class and wrap 4 calls.
82+
83+
To understand, how to extend functionality, below is example of request creation:
84+
85+
```C++
86+
void
87+
RequestFactory::create_get_rq (KmipCtx &ctx, const id_t &id)
88+
{
89+
KmipRequest rq (ctx);
90+
TextString uuid = {};
91+
uuid.size = id.size ();
92+
uuid.value = const_cast<char *>(id.c_str ());
93+
94+
GetRequestPayload grp {};
95+
grp.unique_identifier = &uuid;
96+
97+
RequestBatchItem rbi {};
98+
kmip_init_request_batch_item (&rbi);
99+
rbi.operation = KMIP_OP_GET;
100+
rbi.request_payload = &grp;
101+
rq.set_batch_item (&rbi);
102+
rq.encode ();
103+
}
104+
```
105+
In the example above we use low-level primitives from “kmip.h” to create the RequestBatchItem and
106+
then we add it to the internal member of “KmipRequest” class, which performs appropriate
107+
request encoding in to the KMIP context.
108+
109+
Below is an example of the response processing:
110+
111+
```C++
112+
ve::expected<Key, Error>
113+
ResponseResultFactory::get_key (ResponseBatchItem *rbi)
114+
{
115+
auto *pld = static_cast<GetResponsePayload *> (rbi->response_payload);
116+
switch (pld->object_type)
117+
{
118+
//name known key to KeyFactory types
119+
case KMIP_OBJTYPE_SYMMETRIC_KEY:
120+
KMIP_OBJTYPE_PUBLIC_KEY:
121+
KMIP_OBJTYPE_PRIVATE_KEY:
122+
KMIP_OBJTYPE_CERTIFICATE:
123+
{
124+
return KeyFactory::parse_response(pld);
125+
};
126+
default:
127+
return Error(-1,"Invalid response object type.");
128+
}
129+
}
130+
131+
```
132+
And here is an example of top-level function implementation
133+
134+
```C++
135+
my::expected<Key, Error>
136+
KmipClient::op_get_key (const id_t &id)
137+
{
138+
KmipCtx ctx;
139+
RequestFactory request_factory(ctx);
140+
ResponseFactory rf(ctx);
141+
try
142+
{
143+
request_factory.create_get_rq (id);
144+
io->do_exchange (ctx);
145+
return rf.get_key(0);
146+
}
147+
catch (ErrorException &e)
148+
{
149+
return Error(e.code (), e.what ());
150+
}
151+
}
152+
```
153+
As can be seen from the source code, each KMIP low-level entity is encapsulated in some C++ class,
154+
therefore advanced C++ memory management is utilized. Also, the design is avoiding any kind
155+
of smart pointers (almost… sometimes we need it), utilizing on-stack variables. Raw pointers from
156+
the low-level code are used rarely just to pass stack-based data for more detailed processing.
157+
158+
It is worth of mentioning, that KMIP protocol supports multiple request items ( batch items )
159+
in one network request. For example, it might be combination of GET and GET_ATTRRIBUTE operations
160+
to have a key with set of it’s attributes. It is important to have key state attribute,
161+
because a key could be outdated, deactivated or marked as compromised.
162+
163+
The design of this library supports multiple batch items in requests and in responses.
164+
165+
## Usage
166+
167+
Please, seee usage examples in the "examples" directory
168+

kmipclient/TODO.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
TODO
2+
--
3+
The list of things yet to be done
4+
5+
1. Test suite for KmipClient class
6+
2. Asymetric keys and certificates support
7+
4. Version negotiation with the KMIP server (Default is 1.4)
8+
5. Multiple batch items requests and responses for cases like "register and activate", "revoke and destroy"
9+
6. Human-readable request and response logging
10+
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// Created by al on 02.04.25.
3+
//
4+
#include "../include/KmipClient.hpp"
5+
#include "../include/NetClientOpenSSL.hpp"
6+
#include "../include/kmipclient_version.hpp"
7+
8+
#include <iostream>
9+
10+
using namespace kmipclient;
11+
12+
int
13+
main (int argc, char **argv)
14+
{
15+
std::cout << "KMIP CLIENT version: " << KMIPCLIENT_VERSION_STR << std::endl;
16+
if (argc < 7)
17+
{
18+
std::cerr << "Usage: example_activate <host> <port> <client_cert> <client_key> <server_cert> <key_id>"
19+
<< std::endl;
20+
return -1;
21+
}
22+
23+
NetClientOpenSSL net_client (argv[1], argv[2], argv[3], argv[4], argv[5], 200);
24+
KmipClient client (net_client);
25+
26+
const auto opt_key = client.op_activate (argv[6]);
27+
if (opt_key.has_value ())
28+
{
29+
std::cout << "Key wih ID: " << argv[6] << " is activated." << std::endl;
30+
}
31+
else
32+
{
33+
std::cerr << "Can not activate key with id:" << argv[6] << " Cause: " << opt_key.error ().message << std::endl;
34+
};
35+
36+
return 0;
37+
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
2+
3+
#include "../include/Kmip.hpp"
4+
#include "../include/KmipClient.hpp"
5+
#include "../include/NetClientOpenSSL.hpp"
6+
#include <iostream>
7+
8+
using namespace kmipclient;
9+
10+
int
11+
main (int argc, char **argv)
12+
{
13+
std::cout << "KMIP CLIENT version: " << KMIPCLIENT_VERSION_STR << std::endl;
14+
if (argc < 7)
15+
{
16+
std::cerr << "Usage: example_create_aes <host> <port> <client_cert> <client_key> <server_cert> <key_id>"
17+
<< std::endl;
18+
return -1;
19+
}
20+
21+
Kmip kmip (argv[1], argv[2], argv[3], argv[4], argv[5], 200);
22+
23+
auto key_opt = kmip.client ().op_create_aes_key (argv[6], "TestGroup");
24+
if (key_opt.has_value ())
25+
{
26+
const name_t &key_id = key_opt.value ();
27+
std::cout << "Key ID: " << key_id << std::endl;
28+
}
29+
else
30+
{
31+
std::cerr << "Can not create key with name:" << argv[6] << " Cause: " << key_opt.error ().message << std::endl;
32+
}
33+
return 0;
34+
}

0 commit comments

Comments
 (0)