Skip to content

PS-9697 C++ KMIP client library added #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
build
generated-docs
18 changes: 18 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,22 @@ set(KMIP_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")

add_subdirectory(libkmip/src)
add_subdirectory(kmippp)
add_subdirectory(kmipclient)

find_package(Doxygen REQUIRED)

if(DOXYGEN_FOUND)
configure_file(Doxyfile ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile COPYONLY)

add_custom_target(doc
COMMAND ${DOXYGEN_EXECUTABLE} ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Generating API documentation with Doxygen"
VERBATIM
)

# Make the 'doc' target depend on your build targets if necessary
# add_dependencies(doc your_library your_executable)
else()
message(STATUS "Doxygen not found, skipping documentation generation.")
endif()
36 changes: 36 additions & 0 deletions Doxyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Project information
PROJECT_NAME = libkmip
PROJECT_NUMBER = 0.4.0
OUTPUT_DIRECTORY = generated-docs

# Input settings
INPUT = . # Scan the current directory for source files
RECURSIVE = YES

# Output settings
GENERATE_LATEX = NO
GENERATE_MAN = NO
GENERATE_RTF = NO
GENERATE_XML = NO
GENERATE_HTMLHELP = YES

# UML related settings
UML_LOOK = YES
HAVE_DOT = YES
DOT_PATH = /usr/bin/dot # Adjust this path to where your 'dot' executable is located
PLANTUML_JAR_PATH = /usr/share/java/plantuml.jar
PLANTUML_PREPROC = NO
PLANTUML_INCLUDE_PATH =
PLANTUML_CONFIG_FILE =
# Enable class diagram generation
CLASS_DIAGRAMS = YES
COLLABORATION_GRAPH = YES
UML_LIMIT_NUM_FIELDS = 50
TEMPLATE_RELATIONS = YES
MAX_DOT_GRAPH_DEPTH = 0
MAX_DOT_GRAPH_NODES = 0
HIDE_UNDOC_MEMBERS = NO
HIDE_VIRTUAL_FUNCTIONS = NO
SHOW_INCLUDE_FILES = YES
SHOW_USED_FILES = YES
SHOW_FILES = YES
3 changes: 3 additions & 0 deletions kmipclient/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Apr, 2025 Version 0.1.0

Initial implementation of all functionality available in "kmippp"
76 changes: 76 additions & 0 deletions kmipclient/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # Optional, but recommended for standard compliance

add_library(
kmipclient
STATIC
include/KmipClient.hpp
src/KmipClient.cpp
include/NetClient.hpp
src/NetClientOpenSSL.cpp
include/NetClientOpenSSL.hpp
include/v_expected.hpp
include/kmip_data_types.hpp
src/RequestFactory.cpp
src/RequestFactory.hpp
src/KmipCtx.hpp
src/KmipRequest.hpp
src/IOUtils.cpp
src/IOUtils.hpp
include/Kmip.hpp
src/ResponseResult.cpp
src/ResponseResult.hpp
src/kmip_exceptions.hpp
src/AttributesFactory.cpp
src/AttributesFactory.hpp
src/KeyFactory.cpp
src/KeyFactory.hpp
src/Key.cpp
include/Key.hpp
src/StringUtils.cpp
src/StringUtils.hpp
include/Logger.hpp
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not a CMake expert, but I don't think we should list header files in the list of library files.

)

target_link_libraries(kmipclient kmip)
set_property(TARGET kmipclient PROPERTY POSITION_INDEPENDENT_CODE ON)

target_include_directories(
kmipclient PUBLIC
$<BUILD_INTERFACE:${KMIP_SOURCE_DIR}/kmipclient/>
$<INSTALL_INTERFACE:include>
)

set_target_properties(
kmipclient PROPERTIES PUBLIC_HEADER "Kmip.hpp"
)

export(TARGETS kmip kmipclient FILE "kmipclient.cmake")

install(
TARGETS kmipclient
EXPORT kmipclient
DESTINATION cmake
ARCHIVE DESTINATION lib
PUBLIC_HEADER DESTINATION include/
LIBRARY DESTINATION lib)

macro(add_example name)
add_executable(example_${name} examples/example_${name}.cpp)
target_link_libraries(example_${name} kmipclient)
endmacro()

add_example(create_aes)
add_example(register_secret)
add_example(activate)
add_example(get)
add_example(get_name)
add_example(get_secret)
add_example(revoke)
add_example(destroy)
add_example(register_key)
add_example(locate)
add_example(locate_by_group)
add_example(get_all_ids)
168 changes: 168 additions & 0 deletions kmipclient/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
The "kmipclient" library
--
KMIP client is the C++ library that allows simple access to the KMIP servers using the KMIP protocol.

The "kmipclient" library wraps up the low-level libkmip (kmip.h, kmip.c) into C++ code.
The purpose of such wrap-up is to:

## Design goals.

1. Provide easy to use and hard to misuse interface with forced error processing.
2. Hide low-level details.
3. Minimize manual memory management
4. Make the library easy to extend
5. Exclude mid-level (kmip_bio.c), use the low-level (kmip.c) only
6. Easy to replace network communication level
7. Testability

## External dependencies

No extra external dependencies should be used, except existing OpenSSL dependency.
KmipClient itself does not depend on any library except "kmip". The network communication level is injected
into KmipClient instance as implementation of the NetClient interface. The library has ready to use
OpenSSL BIO based implementation called NetClientOpenSSL. User of the library can use any other library to
implement the communication level.

## High level design

The top interface wraps network communication level (based on OpenSSL) and the KMIP protocol encoding level.
It is implemented as header-only class in the file “Kmip.hpp” and can be used similar to the old C++ wrapper
(kmippp.h). Actual high level interface consists of two headers: NetClient.hpp. and KmipClient.hpp.

The first interface is just a contract to wrap low-level network communications similar to well-known
interfaces (socket, OpenSSL bio and others). It contains 4 methods only: connect(), close(), send()
and receive(). This interface also has an implementation, declared “NetClientOpenSSL.hpp”.
It is based on OpenSSL BIO functions.

The second interface is actual KMIP protocol implementation. It requires a NetClient implementation
as a dependency injection in the constructor. This interface is also similar to the existing C++ wrapper
and can be used the similar whay when properly initialized with the NetClient-derived instance.
Copy link
Contributor

Choose a reason for hiding this comment

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

whay -> way ?


The main difference to the “kmippp.h” is in-band error processing. It uses a template similar to
std::expected from the C++ 23. Though, project may use older C++ standard (C++ 20), so the interface
includes a C++ 20 implementation, that wraps standard implementation or provides replacement if it is absent.

All KMIP request creation and encoding are encapsulated in the RequestFactory class. All operations are
on stack and do not require memory management.

All KMIP responses processing are encapsulated in ResponseFactory class. It should be operated on stack
to keep data in place. Copy and move operations are disabled.

By the protocol, parsed response contains one or more response batch items. To process these items,
ResponseFactory class is used. It’s purpose is to extract values from the response batch item. V
alues are keys, secrets, attributes, etc. This class does not have a state and consists of static methods.

All operation in the low-level KMIP library are based on context structure KMIP. This structure is
encapsulated in KmipCtx class along with operations on buffers, errors, etc. This class, once created,
is passed by the reference to other classes of the “kmipclient” library. Copy and move operations are
disabled for this class also. Usually, the instance of this class is created on stack in the high-level
methods and does not require memory management.

The high-level interface usage example:

```C++
NetClientOpenSSL net_client (argv[1], argv[2], argv[3], argv[4], argv[5], 200);
KmipClient client (net_client);

const auto opt_key = client.op_get_key (argv[6]);
if (opt_key.has_value ())
{
std::cout << "Key: 0x";
auto k = opt_key.value ();
print_hex (k.value());
}
else
{
std::cerr << "Can not get key with id:"<< argv[6] << " Cause: "<< opt_key.error().message << std::endl;
};
```
As can be seen from the code above, the NetClientOpenSSL class instance is injected as dependency
inversion into the KmipClient class instance. This approach allows to use any net connection with KmipClient.
It is enough to derive the class from NetClient class and wrap 4 calls.

To understand, how to extend functionality, below is example of request creation:

```C++
void
RequestFactory::create_get_rq (KmipCtx &ctx, const id_t &id)
{
KmipRequest rq (ctx);
TextString uuid = {};
uuid.size = id.size ();
uuid.value = const_cast<char *>(id.c_str ());

GetRequestPayload grp {};
grp.unique_identifier = &uuid;

RequestBatchItem rbi {};
kmip_init_request_batch_item (&rbi);
rbi.operation = KMIP_OP_GET;
rbi.request_payload = &grp;
rq.set_batch_item (&rbi);
rq.encode ();
}
```
In the example above we use low-level primitives from “kmip.h” to create the RequestBatchItem and
then we add it to the internal member of “KmipRequest” class, which performs appropriate
request encoding in to the KMIP context.

Below is an example of the response processing:

```C++
ve::expected<Key, Error>
ResponseResultFactory::get_key (ResponseBatchItem *rbi)
{
auto *pld = static_cast<GetResponsePayload *> (rbi->response_payload);
switch (pld->object_type)
{
//name known key to KeyFactory types
case KMIP_OBJTYPE_SYMMETRIC_KEY:
KMIP_OBJTYPE_PUBLIC_KEY:
KMIP_OBJTYPE_PRIVATE_KEY:
KMIP_OBJTYPE_CERTIFICATE:
{
return KeyFactory::parse_response(pld);
};
default:
return Error(-1,"Invalid response object type.");
}
}

```
And here is an example of top-level function implementation

```C++
my::expected<Key, Error>
KmipClient::op_get_key (const id_t &id)
{
KmipCtx ctx;
RequestFactory request_factory(ctx);
ResponseFactory rf(ctx);
try
{
request_factory.create_get_rq (id);
Copy link
Contributor

Choose a reason for hiding this comment

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

In the code snippet above, where the RequestFactory::create_get_rq member function is defined, it is declared with two parameters, neither having a default value. Here, the function is called with one argument.

io->do_exchange (ctx);
return rf.get_key(0);
Copy link
Contributor

Choose a reason for hiding this comment

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

As defined in the code snippet above, the ResponseResultFactory::get_key function accepts a raw pointer and later dereferences it. However, this line passes 0, i.e. null pointer.

}
catch (ErrorException &e)
{
return Error(e.code (), e.what ());
}
}
```
As can be seen from the source code, each KMIP low-level entity is encapsulated in some C++ class,
therefore advanced C++ memory management is utilized. Also, the design is avoiding any kind
of smart pointers (almost… sometimes we need it), utilizing on-stack variables. Raw pointers from
the low-level code are used rarely just to pass stack-based data for more detailed processing.

It is worth of mentioning, that KMIP protocol supports multiple request items ( batch items )
in one network request. For example, it might be combination of GET and GET_ATTRRIBUTE operations
to have a key with set of it’s attributes. It is important to have key state attribute,
because a key could be outdated, deactivated or marked as compromised.

The design of this library supports multiple batch items in requests and in responses.

## Usage

Please, seee usage examples in the "examples" directory

10 changes: 10 additions & 0 deletions kmipclient/TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
TODO
--
The list of things yet to be done

1. Test suite for KmipClient class
2. Asymetric keys and certificates support
4. Version negotiation with the KMIP server (Default is 1.4)
5. Multiple batch items requests and responses for cases like "register and activate", "revoke and destroy"
6. Human-readable request and response logging

37 changes: 37 additions & 0 deletions kmipclient/examples/example_activate.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// Created by al on 02.04.25.
Copy link
Contributor

Choose a reason for hiding this comment

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

I can be better to use a proper file comment with a copyright and a license.

//
#include "../include/KmipClient.hpp"
Copy link
Contributor

Choose a reason for hiding this comment

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

Usually source code doesn't include relative paths to headers and include directories. Instead, it is better to provide required include paths in CMake file.

Also, to prevent potential name clashes in the code that will use the libary, it is better if include directives use a library name as a prefix, e.g.:

#include <kmipclient/NetClientOpenSSL.hpp>

In order to achieve that, headers need to be placed in the kmipclient directory that itself is placed into the include directory.

#include "../include/NetClientOpenSSL.hpp"
#include "../include/kmipclient_version.hpp"
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be better if the headers used the same name scheme, either CamelCase.hpp or snake_case.hpp, but not the mix of both.


#include <iostream>
Copy link
Contributor

Choose a reason for hiding this comment

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

Usually, standard headers are listed before 3rd-party-library headers in include directives' block.


using namespace kmipclient;

int
main (int argc, char **argv)
{
std::cout << "KMIP CLIENT version: " << KMIPCLIENT_VERSION_STR << std::endl;
if (argc < 7)
{
std::cerr << "Usage: example_activate <host> <port> <client_cert> <client_key> <server_cert> <key_id>"
<< std::endl;
return -1;
}

NetClientOpenSSL net_client (argv[1], argv[2], argv[3], argv[4], argv[5], 200);
KmipClient client (net_client);

const auto opt_key = client.op_activate (argv[6]);
if (opt_key.has_value ())
{
std::cout << "Key wih ID: " << argv[6] << " is activated." << std::endl;
}
else
{
std::cerr << "Can not activate key with id:" << argv[6] << " Cause: " << opt_key.error ().message << std::endl;
};

return 0;
}
34 changes: 34 additions & 0 deletions kmipclient/examples/example_create_aes.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@


#include "../include/Kmip.hpp"
#include "../include/KmipClient.hpp"
#include "../include/NetClientOpenSSL.hpp"
#include <iostream>

using namespace kmipclient;

int
main (int argc, char **argv)
{
std::cout << "KMIP CLIENT version: " << KMIPCLIENT_VERSION_STR << std::endl;
if (argc < 7)
{
std::cerr << "Usage: example_create_aes <host> <port> <client_cert> <client_key> <server_cert> <key_id>"
<< std::endl;
return -1;
}

Kmip kmip (argv[1], argv[2], argv[3], argv[4], argv[5], 200);

auto key_opt = kmip.client ().op_create_aes_key (argv[6], "TestGroup");
if (key_opt.has_value ())
{
const name_t &key_id = key_opt.value ();
std::cout << "Key ID: " << key_id << std::endl;
}
else
{
std::cerr << "Can not create key with name:" << argv[6] << " Cause: " << key_opt.error ().message << std::endl;
}
return 0;
}
Loading