diff --git a/cpp/.clang-format b/cpp/.clang-format
new file mode 100644
index 0000000000000000000000000000000000000000..4314079c156529a96bc58ba0c63a1f2b890a1ddd
--- /dev/null
+++ b/cpp/.clang-format
@@ -0,0 +1,33 @@
+---
+Language: Cpp
+AlignAfterOpenBracket: AlwaysBreak
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: false
+AlignConsecutiveMacros: true
+AlignOperands: false
+AlignTrailingComments: true
+AllowAllArgumentsOnNextLine: false
+AllowAllConstructorInitializersOnNextLine: false
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: Empty
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: true
+AlwaysBreakTemplateDeclarations: Yes
+BinPackArguments: false
+BinPackParameters: false
+ColumnLimit: 80
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ContinuationIndentWidth: 2
+Cpp11BracedListStyle: true
+FixNamespaceComments: true
+IndentWidth: 2
+ObjCBlockIndentWidth: 2
+PointerAlignment: Left
+SpacesInSquareBrackets: false
+Standard: Cpp11
+TabWidth: 2
+UseTab: Never
\ No newline at end of file
diff --git a/cpp/proto b/cpp/proto
index 3e82fc7d60056e08b041577e332f1ec5029633ee..cfea384107dec09a91ae566481432bfd1b6702dc 160000
--- a/cpp/proto
+++ b/cpp/proto
@@ -1 +1 @@
-Subproject commit 3e82fc7d60056e08b041577e332f1ec5029633ee
+Subproject commit cfea384107dec09a91ae566481432bfd1b6702dc
diff --git a/cpp/src/CMakeLists.txt b/cpp/src/CMakeLists.txt
index 520ad243c7ff982e01c06d0d7539274c96135545..205e69aee397da56add3ed4d5514932d4461c041 100644
--- a/cpp/src/CMakeLists.txt
+++ b/cpp/src/CMakeLists.txt
@@ -18,6 +18,8 @@ include_directories(
 )
 
 file(GLOB PROTO_SRC ${CMAKE_SOURCE_DIR}/proto/cpp/vereign/client_library/*.cc)
+file(GLOB PROTO_INTERNAL_SRC ${CMAKE_SOURCE_DIR}/proto/cpp/vereign/client_library/internal/*.cc)
+list(APPEND PROTO_SRC ${PROTO_INTERNAL_SRC})
 list(APPEND PROTO_SRC
   ${CMAKE_SOURCE_DIR}/proto/cpp/google/api/annotations.pb.cc
   ${CMAKE_SOURCE_DIR}/proto/cpp/google/api/http.pb.cc
@@ -49,6 +51,7 @@ endif()
 set(VEREIGNLIB_SRC
   vereign/core/rand.cc
   vereign/core/string.cc
+  vereign/core/time.cc
   vereign/fs/util.cc
   vereign/fs/operations.cc
   vereign/fs/path.cc
@@ -76,6 +79,7 @@ set(VEREIGNLIB_SRC
   vereign/crypto/rsa.cc
   vereign/crypto/bio.cc
   vereign/crypto/digest.cc
+  vereign/crypto/cert.cc
 
   vereign/kvstore/lock.cc
   vereign/kvstore/detail/base_crypto_storage.cc
@@ -83,12 +87,12 @@ set(VEREIGNLIB_SRC
   vereign/kvstore/crypto_storage.cc
   vereign/kvstore/detail/value_encoder.cc
 
+  vereign/event/broker.cc
   vereign/identity/provider.cc
 
   vereign/service/identity_service.cc
 )
 
-
 if (LINUX)
   list(APPEND VEREIGNLIB_SRC
     vereign/kvstore/detail/linux_crypto_storage.cc
@@ -116,7 +120,7 @@ target_link_libraries(vereignlib PUBLIC
   gRPC::grpc++_reflection
   gRPC::grpc++
   Boost::filesystem
-  $<$<CXX_COMPILER_ID:MSVC>:Boost::date_time>
+  Boost::date_time
   SQLite::SQLite3
   $<$<CXX_COMPILER_ID:MSVC>:ncrypt.lib>
   $<$<CXX_COMPILER_ID:MSVC>:cryptui.lib>
@@ -145,10 +149,13 @@ set(csandbox_sources
 
 add_executable(csandbox ${csandbox_sources})
 
-target_link_libraries(csandbox
-  PRIVATE vereignlib
-  $<$<CXX_COMPILER_ID:MSVC>:Boost::date_time>
-  Boost::filesystem
+target_link_libraries(csandbox PRIVATE
+  vereignlib
+  # OpenSSL::Crypto
+  # OpenSSL::SSL
+  profiler
+  # $<$<CXX_COMPILER_ID:MSVC>:Boost::date_time>
+  # Boost::filesystem
   # Boost::file
   # Boost::thread
   # vereign
diff --git a/cpp/src/csandbox.cc b/cpp/src/csandbox.cc
index 70382932622480f68ad621fb1f2582e0999d247d..5cbbd22b6df93c5e2eab5ddd8e5d89a49e78efc4 100644
--- a/cpp/src/csandbox.cc
+++ b/cpp/src/csandbox.cc
@@ -1,17 +1,57 @@
-#include "vereign/crypto/rand.hh"
-#include <vereign/bytes/view.hh>
-#include <vereign/bytes/buffer.hh>
-#include <vereign/bytes/view_dump.hh>
-#include <vereign/fs/path.hh>
-#include <vereign/fs/util.hh>
-#include <iostream>
+#include "vereign/client_library/event_types.pb.h"
+#include "vereign/client_library/internal/event_types.pb.h"
+#include "vereign/sync/channel.hh"
+#include "vereign/container/bounded_queue.hh"
+#include <thread>
+// #include "vereign/event/message_pool.hh"
+// #include <future>
+// #include <thread>
+// #include <vereign/crypto/cert.hh>
+
+// #include <vereign/crypto/rsa.hh>
+// #include <vereign/crypto/rand.hh>
+// #include <vereign/crypto/bio.hh>
+// #include <vereign/bytes/view_dump.hh>
+// #include <vereign/fs/util.hh>
+// #include <vereign/core/time.hh>
+// #include <boost/date_time.hpp>
+// #include <boost/pool/object_pool.hpp>
+
+// #include <openssl/x509v3.h>
+
+// #include <chrono>
+// #include <iostream>
+// #include <fstream>
 
 auto main(int argc, char** argv) -> int {
   argc = 0;
   argv = nullptr;
-  auto path = vereign::fs::TempFilePath(vereign::fs::path::Join("tmp", "foo"), "test_db_");
 
-  std::cout << path << std::endl;
+  int64_t iterations = int64_t(1000)*1000*1000*10;
+
+  using clock = std::chrono::high_resolution_clock;
+  auto q = vereign::container::BoundedQueue<int>(10);
+
+  auto start = clock::now();
+  int64_t s = 0;
+
+  for (int64_t i = 0; i < iterations; i++) {
+    if (q.IsFull()) {
+      q.PopFront();
+    }
+
+    q.PushBack(i);
+  }
+
+  auto end = clock::now() - start;
+
+  std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end).count() << std::endl;
+  std::cout << std::chrono::duration_cast<std::chrono::nanoseconds>(end).count()/iterations << std::endl;
+
+  while (!q.IsEmpty()) {
+    s += q.Front();
+    q.PopFront();
+  }
 
-  return 0;
+  std::cout << s << std::endl;
 }
diff --git a/cpp/src/vereign/container/bounded_queue.hh b/cpp/src/vereign/container/bounded_queue.hh
new file mode 100644
index 0000000000000000000000000000000000000000..fbb2ab64b0110e3dd9d5ca0219c9902ab9ee06df
--- /dev/null
+++ b/cpp/src/vereign/container/bounded_queue.hh
@@ -0,0 +1,139 @@
+#ifndef __VEREIGN_CONTAINER_BOUNDED_QUEUE_HH
+#define __VEREIGN_CONTAINER_BOUNDED_QUEUE_HH
+
+#include <vector>
+
+namespace vereign::container {
+
+/**
+ * A queue (FIFO) data structure with fixed size.
+ *
+ * Being a fixed in its size, the BoundedQueue has the opportunity to internally allocate only once
+ * upon construction a single continues block of memory for holding the queue elements.
+ */
+template <typename T>
+class BoundedQueue {
+public:
+  /**
+   * The type of the values in the queue.
+   */
+  using ValueType = T;
+
+  // Helper type that is properly aligned so that it can act as a storage for the ValueType.
+  using StorageType = typename std::aligned_storage_t<sizeof(T), alignof(T)>;
+
+  /**
+   * Creates a queue with given capacity.
+   *
+   * The capacity cannot be changed after construction.
+   */
+  BoundedQueue(std::size_t capacity)
+    : begin_{0},
+      size_{0},
+      cap_{capacity},
+      queue_{new StorageType[capacity]}
+  {
+  }
+
+  /**
+   * Destroy the elements if any, and free all the memory.
+   */
+  ~BoundedQueue() {
+    while (!IsEmpty()) {
+      PopFront();
+    }
+
+    delete [] queue_;
+  }
+
+  // disable copying
+  BoundedQueue(const BoundedQueue&) = delete;
+  auto operator=(const BoundedQueue&) -> BoundedQueue& = delete;
+
+  /**
+   * Retrieve the number of elements in the queue.
+   *
+   * @returns the number of elements in the queue.
+   */
+  auto Size() -> std::size_t {
+    return size_;
+  }
+
+  /**
+   * Check if the queue is empty.
+   *
+   * @returns true when the queue is empty, false otherwise.
+   */
+  auto IsEmpty() -> bool {
+    return size_ == 0;
+  }
+
+  /**
+   * Check if the queue is full up to its capacity.
+   *
+   * @returns true when the queue is full, false otherwise.
+   */
+  auto IsFull() -> bool {
+    return size_ == cap_;
+  }
+
+  /**
+   * Moves a value to the end of the queue.
+   *
+   * @note Calling BoundedQueue::PushBack on full queue, is undefined behaviour.
+   */
+  void PushBack(ValueType&& value) {
+    auto pos = (begin_ + size_) % cap_;
+
+    new(&queue_[pos]) T(std::move(value));
+
+    size_++;
+  }
+
+  /**
+   * Copies a value to the end of the queue.
+   *
+   * @note Calling BoundedQueue::PushBack on full queue, is undefined behaviour.
+   */
+  void PushBack(const ValueType& value) {
+    auto pos = (begin_ + size_) % cap_;
+
+    new(&queue_[pos]) T(value);
+
+    size_++;
+  }
+
+  /**
+   * Access the first value in the queue.
+   *
+   * @note Calling BoundedQueue::Front on empty queue is undefined behaviour.
+   */
+  auto Front() -> ValueType& {
+    return reinterpret_cast<ValueType&>(queue_[begin_]);
+  }
+
+  /**
+   * Remove the fist item from the queue.
+   *
+   * @note Calling BoundedQueue::PopFront on empty queue is undefined behaviour.
+   */
+  void PopFront() {
+    reinterpret_cast<ValueType*>(&queue_[begin_])->~ValueType();
+
+    size_--;
+    begin_ = (begin_ == cap_ - 1) ? 0 : begin_ + 1;
+  }
+
+private:
+  std::size_t begin_;
+
+  std::size_t size_;
+
+  std::size_t cap_;
+
+  StorageType* queue_;
+};
+
+} // namespace vereign::container
+
+#endif // __VEREIGN_CONTAINER_BOUNDED_QUEUE_HH
diff --git a/cpp/src/vereign/core/time.cc b/cpp/src/vereign/core/time.cc
new file mode 100644
index 0000000000000000000000000000000000000000..33030082a682d2bc3091602a24d5e39c22da4b96
--- /dev/null
+++ b/cpp/src/vereign/core/time.cc
@@ -0,0 +1,73 @@
+#include <vereign/core/time.hh>
+
+namespace vereign::time {
+
+auto Epoch() -> boost::posix_time::ptime {
+  static const auto epoch = boost::posix_time::ptime{boost::gregorian::date{1970, 1, 1}};
+
+  return epoch;
+}
+
+auto PosixTimeToTime(boost::posix_time::ptime t) -> time_t {
+  return time_t((t - Epoch()).total_seconds());
+}
+
+auto MakePosixTime(
+  int year,
+  int month,
+  int day,
+  int hours,
+  int minutes,
+  int seconds
+) -> boost::posix_time::ptime {
+  using namespace boost::posix_time;
+  using namespace boost::gregorian;
+
+  return ptime(
+    date{
+      static_cast<date::year_type>(year),
+      static_cast<date::month_type>(month),
+      static_cast<date::day_type>(day)
+    },
+    time_duration(hours, minutes, seconds)
+  );
+}
+
+auto MakeTimeUTC(
+  int year,
+  int month,
+  int day,
+  int hours,
+  int seconds,
+  int milliseconds
+) -> time_t {
+  using namespace boost::posix_time;
+  using namespace boost::gregorian;
+
+  return PosixTimeToTime(
+    ptime(
+      date{
+        static_cast<date::year_type>(year),
+        static_cast<date::month_type>(month),
+        static_cast<date::day_type>(day)
+      },
+      time_duration(hours, seconds, milliseconds)
+    )
+  );
+}
+
+auto MakeTimeUTCFromString(const std::string& str) -> time_t {
+  using namespace boost::posix_time;
+  using namespace boost::gregorian;
+
+  return PosixTimeToTime(boost::posix_time::time_from_string(str));
+}
+
+auto MakeTimeUTCFromISO(const std::string& str) -> time_t {
+  using namespace boost::posix_time;
+  using namespace boost::gregorian;
+
+  return PosixTimeToTime(from_iso_string(str));
+}
+
+} // vereign::time
diff --git a/cpp/src/vereign/core/time.hh b/cpp/src/vereign/core/time.hh
new file mode 100644
index 0000000000000000000000000000000000000000..5b7d775606d25a7c2ac264d543bbbdeac833088c
--- /dev/null
+++ b/cpp/src/vereign/core/time.hh
@@ -0,0 +1,72 @@
+#ifndef __VEREIGN_CORE_TIME_HH
+#define __VEREIGN_CORE_TIME_HH
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+namespace vereign::time {
+
+/**
+ * Returns the gregorian epoch boost posix time - 1970:01:01.
+ */
+auto Epoch() -> boost::posix_time::ptime;
+
+/**
+ * Converts boost posix time to time_t timestamp.
+ *
+ * @param t The source boost posix time.
+ * @returns timestamp.
+ */
+auto PosixTimeToTime(boost::posix_time::ptime t) -> time_t;
+
+/**
+ * Creates boost posix timetamp.
+ *
+ * @param year Year component.
+ * @param month Month component 1-12.
+ * @param day Day component 1-31.
+ * @param hours The hours component 0-23.
+ * @param minutes The minutes component 0-59.
+ * @param seconds The minutes component 0-59.
+ * @returns posix time.
+ */
+auto MakePosixTime(
+  int year,
+  int month,
+  int day,
+  int hours,
+  int minutes,
+  int seconds
+) -> boost::posix_time::ptime;
+
+/**
+ * Creates UTC timestamp.
+ *
+ * @param year Year component.
+ * @param month Month component 1-12.
+ * @param day Day component 1-31.
+ * @param hours The hours component 0-23.
+ * @param minutes The minutes component 0-59.
+ * @param seconds The minutes component 0-59.
+ * @returns timestamp.
+ */
+auto MakeTimeUTC(int year, int month, int day, int hours, int seconds, int milliseconds) -> time_t;
+
+/**
+ * Creates UTC timestamp from a string.
+ *
+ * @param str Time string representation, example: "2002-01-20 23:59:59.000".
+ * @returns timestamp.
+ */
+auto MakeTimeUTCFromString(const std::string& str) -> time_t;
+
+/**
+ * Creates UTC timestamp from ISO formatted string.
+ *
+ * @param str Time string representation, example: "20020120T235959".
+ * @returns timestamp.
+ */
+auto MakeTimeUTCFromISO(const std::string& str) -> time_t;
+
+} // vereign::time
+
+#endif // __VEREIGN_CORE_TIME_HH
diff --git a/cpp/src/vereign/crypto/cert.cc b/cpp/src/vereign/crypto/cert.cc
new file mode 100644
index 0000000000000000000000000000000000000000..bc6f84d2d6f74904056f6b5bfe7c4c9d5402d2a4
--- /dev/null
+++ b/cpp/src/vereign/crypto/cert.cc
@@ -0,0 +1,428 @@
+#include <vereign/crypto/cert.hh>
+
+#include <vereign/crypto/bio.hh>
+#include <vereign/crypto/errors.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/encoding/base64.hh>
+
+#include <openssl/x509v3.h>
+#include <openssl/digest.h>
+#include <openssl/pem.h>
+#include <sstream>
+
+namespace {
+  constexpr const char* certDefaultHashAlg = "SHA256";
+  constexpr const int certVersion = 2;
+}
+
+namespace vereign::crypto::cert {
+
+static auto addCertExtension(
+  const X509* issuer_cert,
+  X509* cert,
+  int nid,
+  const std::string& value
+) -> int {
+  X509V3_CTX ctx;
+
+  // This sets the 'context' of the extensions.
+  // No configuration database
+  X509V3_set_ctx_nodb(&ctx);
+
+  // Issuer and subject certs: both the target since it is self signed,
+  // no request and no CRL
+  X509V3_set_ctx(&ctx, const_cast<X509*>(issuer_cert), cert, nullptr, nullptr, 0);
+  bssl::UniquePtr<X509_EXTENSION> ex{X509V3_EXT_nconf_nid(
+    nullptr,
+    &ctx,
+    nid,
+    const_cast<std::string&>(value).data()
+  )};
+  if (!ex) {
+    return 0;
+  }
+
+  return X509_add_ext(cert, ex.get(), -1);
+}
+
+static void addSubjAltNameExt(X509* cert, const std::string& email, const std::string& url) {
+  auto gens = bssl::UniquePtr<GENERAL_NAMES>(sk_GENERAL_NAME_new_null());
+  if (!gens) {
+    throw OpenSSLError("creating GENERAL_NAMES stack failed");
+  }
+
+  if (!email.empty()) {
+    auto gen = bssl::UniquePtr<GENERAL_NAME>(GENERAL_NAME_new());
+    if (!gen) {
+      throw OpenSSLError("creating GENERAL_NAME failed");
+    }
+
+    auto ia5 = bssl::UniquePtr<ASN1_IA5STRING>(ASN1_IA5STRING_new());
+    if (!ia5) {
+      throw OpenSSLError("creating ASN1_IA5STRING failed");
+    }
+
+    auto r = ASN1_STRING_set(ia5.get(), email.data(), -1);
+    if (r != 1) {
+      throw OpenSSLError("set certificate alternative name email part failed");
+    }
+
+    GENERAL_NAME_set0_value(gen.get(), GEN_EMAIL, ia5.release());
+    r = sk_GENERAL_NAME_push(gens.get(), gen.release());
+    if (r == 0) {
+      throw OpenSSLError("pushing email to certificate subject alternative name failed");
+    }
+  }
+
+  if (!url.empty()) {
+    auto gen = bssl::UniquePtr<GENERAL_NAME>(GENERAL_NAME_new());
+    if (!gen) {
+      throw OpenSSLError("creating GENERAL_NAME failed");
+    }
+
+    auto ia5 = bssl::UniquePtr<ASN1_IA5STRING>(ASN1_IA5STRING_new());
+    if (!ia5) {
+      throw OpenSSLError("creating ASN1_IA5STRING failed");
+    }
+
+    auto r = ASN1_STRING_set(ia5.get(), url.data(), -1);
+    if (r != 1) {
+      throw OpenSSLError("set certificate alternative name URL part failed");
+    }
+
+    GENERAL_NAME_set0_value(gen.get(), GEN_DNS, ia5.release());
+    r = sk_GENERAL_NAME_push(gens.get(), gen.release());
+    if (r == 0) {
+      throw OpenSSLError("pushing URL to certificate subject alternative name failed");
+    }
+  }
+
+  auto r = X509_add1_ext_i2d(cert, NID_subject_alt_name, gens.get(), 0, 0);
+  if (r != 1) {
+    throw OpenSSLError("set certificate alternative name failed");
+  }
+}
+
+static void setCertSubject(const CertData& cert_data, X509* cert) {
+  const CertSubject& subject = cert_data.Subject;
+  auto subject_name = X509_get_subject_name(cert);
+  int r = 0;
+
+  if (!subject.CommonName.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_commonName,
+      MBSTRING_ASC,
+      (unsigned char*) subject.CommonName.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject Common Name failed");
+    }
+  }
+
+  if (!subject.Country.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_countryName,
+      MBSTRING_ASC,
+      (unsigned char*) subject.Country.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject Country Name failed");
+    }
+  }
+
+  if (!subject.Locality.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_localityName,
+      MBSTRING_ASC,
+      (unsigned char*) subject.Locality.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject Locality Name failed");
+    }
+  }
+
+  if (!subject.State.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_stateOrProvinceName,
+      MBSTRING_ASC,
+      (unsigned char*) subject.State.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject State Name failed");
+    }
+  }
+
+  if (!subject.Organization.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_organizationName,
+      MBSTRING_ASC,
+      (unsigned char*) subject.Organization.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject Organization Name failed");
+    }
+  }
+
+  if (!subject.OrganizationUnit.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_organizationalUnitName,
+      MBSTRING_ASC,
+      (unsigned char*) subject.OrganizationUnit.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject Organizational Unit Name failed");
+    }
+  }
+
+
+  if (!cert_data.Email.empty()) {
+    r = X509_NAME_add_entry_by_NID(
+      subject_name,
+      NID_pkcs9_emailAddress,
+      MBSTRING_ASC,
+      (unsigned char*) cert_data.Email.data(),
+      -1,
+      -1,
+      0
+    );
+    if (r != 1) {
+      throw OpenSSLError("set certificate subject Email Address failed");
+    }
+  }
+}
+
+static void addCertExtensions(const CertData& cert_data, const X509* issuer_cert, X509* cert) {
+  // basic constraints
+  const std::string basic_constraints = cert_data.IsCA ? "critical,CA:TRUE" : "critical,CA:FALSE";
+  auto r = addCertExtension(issuer_cert, cert, NID_basic_constraints, basic_constraints);
+  if (r != 1) {
+    throw OpenSSLError("set certificate basic constraints failed");
+  }
+
+  // key usage
+  const std::string key_usage = cert_data.IsCA
+    ? "critical,digitalSignature,keyCertSign,cRLSign"
+    : "critical,digitalSignature,nonRepudiation,keyEncipherment,dataEncipherment";
+  r = addCertExtension(issuer_cert, cert, NID_key_usage, key_usage);
+  if (r != 1) {
+    throw OpenSSLError("set certificate key usage failed");
+  }
+
+  // extended key usage
+  if (!cert_data.IsCA && !cert_data.Email.empty()) {
+    r = addCertExtension(issuer_cert, cert, NID_ext_key_usage, "emailProtection");
+    if (r != 1) {
+      throw OpenSSLError("set certificate extended key usage failed");
+    }
+  }
+
+  // subject alternative name
+  if (!cert_data.Email.empty() || !cert_data.Url.empty()) {
+    addSubjAltNameExt(cert, cert_data.Email, cert_data.Url);
+  }
+
+  // subject key identifier
+  r = addCertExtension(issuer_cert, cert, NID_subject_key_identifier, "hash");
+  if (r != 1) {
+    throw OpenSSLError("set certificate key usage failed");
+  }
+
+  // authority key identifier
+  r = addCertExtension(issuer_cert, cert, NID_authority_key_identifier, "keyid,issuer:always");
+  if (r != 1) {
+    throw OpenSSLError("set certificate key usage failed");
+  }
+}
+
+static void setCertValidity(const CertData& cert_data, X509* cert) {
+  using clock = std::chrono::system_clock;
+
+  long not_before_adj = 0;
+  time_t not_before = cert_data.Validity.NotBefore;
+
+  if (not_before == 0) {
+    auto now = clock::now();
+    not_before = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
+    not_before_adj = -(not_before % (24 * 60 * 60));
+  }
+
+  auto timeret = X509_time_adj(X509_get_notBefore(cert), not_before_adj, &not_before);
+  if (timeret == nullptr) {
+    throw OpenSSLError("adjusting certificate not before validity failed");
+  }
+
+  long not_after_adj = 0;
+  time_t not_after = cert_data.Validity.NotAfter;
+
+  if (not_after == 0) {
+    not_after = not_before;
+
+    int valid_years = cert_data.Validity.ValidYears;
+    if (valid_years == 0) {
+      valid_years = 1;
+    }
+
+    not_after_adj = not_before_adj + valid_years * 365 * 24 * 60 * 60;
+  }
+
+  timeret = X509_time_adj(X509_get_notAfter(cert), not_after_adj, &not_after);
+  if (timeret == nullptr) {
+    throw OpenSSLError("adjusting certificate not after validity failed");
+  }
+}
+
+static auto createCert(
+  const CertData& cert_data,
+  const X509* issuer_cert,
+  const EVP_PKEY* issuer_pkey,
+  const EVP_PKEY* pkey
+) -> bssl::UniquePtr<X509> {
+  const EVP_MD* hash_alg = nullptr;
+  if (cert_data.Algorithms.HashAlg.empty()) {
+    hash_alg = EVP_get_digestbyname(certDefaultHashAlg);
+  } else {
+    hash_alg = EVP_get_digestbyname(cert_data.Algorithms.HashAlg.data());
+  }
+
+  if (hash_alg == nullptr) {
+    throw crypto::OpenSSLError("cannot find hash algorithm");
+  }
+
+  auto cert = bssl::UniquePtr<X509>(X509_new());
+
+  if (issuer_cert == nullptr || issuer_pkey == nullptr) {
+    issuer_cert = cert.get();
+    issuer_pkey = pkey;
+  }
+
+  // set public key
+  auto r = X509_set_pubkey(cert.get(), const_cast<EVP_PKEY*>(pkey));
+  if (r != 1) {
+    throw OpenSSLError("set public key to certificate failed");
+  }
+
+  // set certificate version
+  r = X509_set_version(cert.get(), certVersion);
+  if (r != 1) {
+    throw OpenSSLError("set version to certificate failed");
+  }
+
+  // set serial number
+  auto serial_number = cert_data.SerialNumber;
+  if (serial_number == 0) {
+    serial_number = crypto::RandUint64();
+  }
+  r = ASN1_INTEGER_set_uint64(X509_get_serialNumber(cert.get()), serial_number);
+  if (r != 1) {
+    throw OpenSSLError("set certificate serial number");
+  }
+
+  // set subject
+  setCertSubject(cert_data, cert.get());
+
+  // set issuer name
+  auto issuer_name = X509_get_subject_name(const_cast<X509*>(issuer_cert));
+  r = X509_set_issuer_name(cert.get(), issuer_name);
+  if (r != 1) {
+    throw OpenSSLError("set certificate issuer name failed");
+  }
+
+  // set validity
+  setCertValidity(cert_data, cert.get());
+
+  // add extensions
+  addCertExtensions(cert_data, issuer_cert, cert.get());
+
+  r = X509_sign(cert.get(), const_cast<EVP_PKEY*>(issuer_pkey), hash_alg);
+  if (r == 0) {
+    throw OpenSSLError("certificate sign failed");
+  }
+
+  return cert;
+}
+
+auto CreateCert(
+  const CertData& cert_data,
+  const X509* issuer_cert,
+  const EVP_PKEY* issuer_pkey,
+  const EVP_PKEY* pkey
+) -> bssl::UniquePtr<X509> {
+
+  return createCert(cert_data, issuer_cert, issuer_pkey, pkey);
+}
+
+auto CreateSelfSignedCert(
+  const CertData& cert_data,
+  const EVP_PKEY* pkey
+) -> bssl::UniquePtr<X509> {
+
+  return createCert(cert_data, nullptr, nullptr, pkey);
+}
+
+auto ExportCertToPEM(const X509* cert) -> bssl::UniquePtr<BIO> {
+  bssl::UniquePtr<BIO> mem(BIO_new(BIO_s_mem()));
+  if (!mem) {
+    throw OpenSSLError("creating memory buffer failed");
+  }
+
+  auto r = PEM_write_bio_X509(mem.get(), const_cast<X509*>(cert));
+  if (r != 1) {
+    throw OpenSSLError("exporting certificate to PEM failed");
+  }
+
+  return mem;
+}
+
+auto ImportCertFromPEM(bytes::View pem) -> bssl::UniquePtr<X509> {
+  bssl::UniquePtr<BIO> mem(BIO_new_mem_buf(pem.Data(), pem.Size()));
+  if (mem == nullptr) {
+    throw OpenSSLError("creating memory buffer failed");
+  }
+
+  auto cert = bssl::UniquePtr<X509>(PEM_read_bio_X509(mem.get(), nullptr, nullptr, nullptr));
+  if (cert == nullptr) {
+    throw OpenSSLError("importing certificate from PEM failed");
+  }
+
+  return cert;
+}
+
+void PrintCert(std::ostream& os, const X509* cert) {
+  bssl::UniquePtr<BIO> mem(BIO_new(BIO_s_mem()));
+  if (!mem) {
+    throw OpenSSLError("creating memory buffer failed");
+  }
+
+  auto r = X509_print(mem.get(), const_cast<X509*>(cert));
+  if (r != 1) {
+    throw OpenSSLError("printing certificate failed");
+  }
+
+  os << bio::View(mem.get()).String();
+}
+
+} // vereign::crypto::cert
diff --git a/cpp/src/vereign/crypto/cert.hh b/cpp/src/vereign/crypto/cert.hh
new file mode 100644
index 0000000000000000000000000000000000000000..6a5ed4e010a68dce886223936afbb6f537ded563
--- /dev/null
+++ b/cpp/src/vereign/crypto/cert.hh
@@ -0,0 +1,170 @@
+#ifndef __VEREIGN_CRYPTO_CERT_HH
+#define __VEREIGN_CRYPTO_CERT_HH
+
+#include <vereign/bytes/buffer.hh>
+
+#include <openssl/base.h>
+#include <openssl/x509.h>
+#include <chrono>
+
+namespace vereign::crypto::cert {
+
+/**
+ * Certificate algorithms configuration used for creating certificates.
+ *
+ * Used in CertData struct.
+ */
+struct CertAlgorithms {
+  /**
+   * The hash algorithm, if it is empty the default SHA256 will be used.
+   */
+  std::string HashAlg;
+
+  // FIXME: SignAlg is unused
+  std::string SignAlg;
+  // FIXME: KeyLength is unused
+  int KeyLength;
+};
+
+/**
+ * Certificate subject configuration used for creating certificates.
+ *
+ * All fields are optional for leaf, recommended for CA.
+ *
+ * Used in CertData struct.
+ */
+struct CertSubject {
+  std::string CommonName;
+  std::string Country;
+  std::string Locality;
+  std::string State;
+  std::string Organization;
+  std::string OrganizationUnit;
+};
+
+/**
+ * Certificate validity configuration used for creating certificates.
+ *
+ * Used in CertData struct.
+ */
+struct CertValidity {
+  /**
+   * If 0, the default is today at 00:00:00.
+   */
+  time_t NotBefore;
+
+  /**
+   * If 0, the default is NotBefore + ValidYears at 23:59:59.
+   */
+  time_t NotAfter;
+
+  /**
+   * If 0, the default is 1.
+   */
+  int ValidYears;
+};
+
+/**
+ * Certificate configuration used for creating certificates.
+ *
+ * @see CreateCert
+ * @see CreateSelfSignedCert
+ */
+struct CertData {
+  CertAlgorithms Algorithms;
+  CertSubject Subject;
+  CertValidity Validity;
+
+  /**
+   * If 0, the default is the current milliseconds timestamp - the number of milliseconds since epoch.
+   */
+  uint64_t SerialNumber;
+
+  /**
+   * Added to DN and Subject Alternative Name extension.
+   * Optional for CA. Mandatory for leaf certificate, used for email protection.
+   */
+  std::string Email;
+
+  /**
+   * Optional, recommended for CA, added to Subject Alternative Name extension.
+   */
+  std::string Url;
+
+  /**
+   * Denotes if the created certificate is CA or leaf certificate. The default is false.
+   */
+  bool IsCA;
+};
+
+/**
+ * Creates and signs a certificate.
+ *
+ * @param cert_data Certificate configuration.
+ * @param issuer_cert The issuer certificate.
+ * @param issuer_pkey The issuer private key.
+ * @param pkey The private key for the newly created certificate.
+ * @returns new certificate signed by the provided issuer.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto CreateCert(
+  const CertData& cert_data,
+  const X509* issuer_cert,
+  const EVP_PKEY* issuer_pkey,
+  const EVP_PKEY* pkey
+) -> bssl::UniquePtr<X509>;
+
+/**
+ * Creates self signed certificate.
+ *
+ * @param cert_data Certificate configuration.
+ * @param pkey The private key for the newly created certificate.
+ * @returns the new certificate signed by its own private key.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto CreateSelfSignedCert(
+  const CertData& cert_data,
+  const EVP_PKEY* pkey
+) -> bssl::UniquePtr<X509>;
+
+/**
+ * Exports certificate to PEM format.
+ *
+ * @param cert The certificate to export.
+ * @returns a memory BIO with the exported certificate.
+ */
+auto ExportCertToPEM(const X509* cert) -> bssl::UniquePtr<BIO>;
+
+/**
+ * Exports certificate to PEM format into string.
+ *
+ * @param cert The certificate to export.
+ * @returns a string with the exported certificate.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto ExportCertToPEMString(const X509* cert) -> std::string;
+
+/**
+ * Imports certificate from PEM format.
+ *
+ * @param pem PEM encoded certificate.
+ * @returns imported X509 certificate.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto ImportCertFromPEM(bytes::View pem) -> bssl::UniquePtr<X509>;
+
+/**
+ * Print textual representation of the certificate into output stream.
+ *
+ * @param os The stream where to write the textual representation of the certificate.
+ * @param cert The certificate.
+ */
+void PrintCert(std::ostream& os, const X509* cert);
+
+} // vereign::crypto::cert
+
+#endif // __VEREIGN_CRYPTO_CERT_HH
diff --git a/cpp/src/vereign/crypto/errors.hh b/cpp/src/vereign/crypto/errors.hh
index 949eb248cd8a0caf3014701a8e962c47f866f1ad..825af7e98baf03a468ace2ced84cea76960b5c4f 100644
--- a/cpp/src/vereign/crypto/errors.hh
+++ b/cpp/src/vereign/crypto/errors.hh
@@ -23,9 +23,19 @@ public:
 class OpenSSLError : public Error {
 public:
   OpenSSLError(const std::string& what)
-    : Error(what + ": " + ERR_reason_error_string(ERR_get_error()))
+    : Error(getOpenSSLErrorString(what))
   {
   }
+
+private:
+  static auto getOpenSSLErrorString(const std::string& what) -> std::string {
+    auto error = ERR_get_error();
+    if (error == 0) {
+      return what + ": no openssl error in the error queue";
+    }
+
+    return what + ": " + ERR_reason_error_string(error);
+  }
 };
 
 } // vereign::crypto
diff --git a/cpp/src/vereign/crypto/rand.cc b/cpp/src/vereign/crypto/rand.cc
index 2acc168ed6d9df6905c3ad25e7053c041e29a715..f61274887d5a826a22e0db178c86564ae2adcad8 100644
--- a/cpp/src/vereign/crypto/rand.cc
+++ b/cpp/src/vereign/crypto/rand.cc
@@ -23,4 +23,15 @@ auto Rand(std::size_t size) -> bytes::Buffer {
   return buf;
 }
 
+auto RandUint64() -> uint64_t {
+  uint64_t x = 0;
+  int result = RAND_bytes((uint8_t*) &x, sizeof(x));
+  if (result == 0) {
+    ERR_clear_error();
+    throw Error("crypto rand failed");
+  }
+
+  return x;
+}
+
 } // vereign::crypto
diff --git a/cpp/src/vereign/crypto/rand.hh b/cpp/src/vereign/crypto/rand.hh
index 4c3979900f1464d2e54c9957f689f503af3e7e2f..e377ac88ce42544b57f337ef82f8ba7c9b24f71c 100644
--- a/cpp/src/vereign/crypto/rand.hh
+++ b/cpp/src/vereign/crypto/rand.hh
@@ -43,6 +43,15 @@ void Rand(bytes::Buffer& buf, std::size_t size);
  */
 auto Rand(std::size_t size) -> bytes::Buffer;
 
+/**
+ * Generates random uint64_t.
+ *
+ * @returns random unsigned 64 bit integer.
+ *
+ * @throws crypto::Error on failure.
+ */
+auto RandUint64() -> uint64_t;
+
 } // vereign::crypto
 
 #endif // __VEREIGN_CRYPTO_RAND_HH
diff --git a/cpp/src/vereign/crypto/rsa.cc b/cpp/src/vereign/crypto/rsa.cc
index e9a519f6b5887486f37819759e65d33108264602..2c89dfcb8078be5b4f232a33eb9c23781690b8cc 100644
--- a/cpp/src/vereign/crypto/rsa.cc
+++ b/cpp/src/vereign/crypto/rsa.cc
@@ -3,6 +3,7 @@
 #include <vereign/bytes/view.hh>
 #include <vereign/crypto/errors.hh>
 #include <vereign/bytes/buffer.hh>
+#include <vereign/crypto/bio.hh>
 
 #include <openssl/base.h>
 #include <openssl/bn.h>
@@ -150,6 +151,12 @@ auto ExportPrivateKeyToPEM(EVP_PKEY* key) -> bssl::UniquePtr<BIO> {
   return mem;
 }
 
+auto ExportPrivateKeyToPEMString(EVP_PKEY* key) -> std::string {
+  auto key_bio = ExportPublicKeyToPEM(key);
+
+  return std::string{crypto::bio::View(key_bio.get()).String()};
+}
+
 auto ImportPrivateKeyFromPEM(bytes::View pem) -> bssl::UniquePtr<EVP_PKEY> {
   bssl::UniquePtr<BIO> mem(BIO_new_mem_buf(pem.Data(), pem.Size()));
   if (mem == nullptr) {
@@ -158,7 +165,7 @@ auto ImportPrivateKeyFromPEM(bytes::View pem) -> bssl::UniquePtr<EVP_PKEY> {
 
   auto key_ptr = PEM_read_bio_PrivateKey(mem.get(), nullptr, nullptr, nullptr);
   if (key_ptr == nullptr) {
-    throw OpenSSLError("importing public key from PEM failed");
+    throw OpenSSLError("importing private key from PEM failed");
   }
 
   return bssl::UniquePtr<EVP_PKEY>(key_ptr);
diff --git a/cpp/src/vereign/crypto/rsa.hh b/cpp/src/vereign/crypto/rsa.hh
index 4d8e18fc38701d7a921dc2ea42a2781030ee32a6..03a6c136503f3b0bfdda1b1df80d3a8a640b80f1 100644
--- a/cpp/src/vereign/crypto/rsa.hh
+++ b/cpp/src/vereign/crypto/rsa.hh
@@ -125,6 +125,23 @@ auto ImportPublicKeyFromPEM(bytes::View pem) -> bssl::UniquePtr<EVP_PKEY>;
  */
 auto ExportPrivateKeyToPEM(EVP_PKEY* key) -> bssl::UniquePtr<BIO>;
 
+/**
+ * Export private key from PEM format.
+ *
+ * @code
+ * auto key = crypto::rsa::GenerateKey(2048);
+ *
+ * auto pem  = crypto::rsa::ExportPrivateKeyToPEMString(key.get());
+ * std::cout << pem << std::endl;
+ * @endcode
+ *
+ * @param key The key to export.
+ * @returns the exported key in PEM format.
+ *
+ * @throws crypto::OpenSSLError on failure.
+ */
+auto ExportPrivateKeyToPEMString(EVP_PKEY* key) -> std::string;
+
 /**
  * Import private key from PEM format.
  *
diff --git a/cpp/src/vereign/encoding/base64.cc b/cpp/src/vereign/encoding/base64.cc
index 47ef85a53f69d204c474b6ad862a1cc0942ebbfa..88e1e539f83ac5c8cb960a500a7b605fde093fcc 100644
--- a/cpp/src/vereign/encoding/base64.cc
+++ b/cpp/src/vereign/encoding/base64.cc
@@ -14,6 +14,19 @@ void Encode(bytes::View src, bytes::Buffer& encoded) {
   encoded.IncSize(written);
 }
 
+auto EncodeToString(bytes::View src) -> std::string {
+  if (src.Size() == 0) {
+    return "";
+  }
+
+  std::string encoded;
+  encoded.resize(boost::beast::detail::base64::encoded_size(src.Size()));
+  auto written = boost::beast::detail::base64::encode(encoded.data(), src.Data(), src.Size());
+  encoded.resize(written);
+
+  return encoded;
+}
+
 void Decode(bytes::View src, bytes::Buffer& decoded) {
   if (src.Size() == 0) {
     return ;
diff --git a/cpp/src/vereign/encoding/base64.hh b/cpp/src/vereign/encoding/base64.hh
index a69a559403f9126045cdb69e89bbf084a519fd08..d3ee3ce4b13a22628dca0c1fe76653e762c3aacd 100644
--- a/cpp/src/vereign/encoding/base64.hh
+++ b/cpp/src/vereign/encoding/base64.hh
@@ -25,6 +25,24 @@ namespace vereign::encoding::base64 {
  */
 void Encode(bytes::View src, bytes::Buffer& encoded);
 
+/**
+ * Encodes source bytes into base64 encoding.
+ *
+ * No new lines are inserted.
+ *
+ * Example:
+ * @code
+ * std::string s{"foob"};
+ * auto encoded = encoding::base64::Encode(bytes::View(s));
+ *
+ * assert(encoded == "Zm9vYg==")
+ * @endcode
+ *
+ * @param src The source bytes that will be encoded.
+ * @returns the encoded string.
+ */
+auto EncodeToString(bytes::View src) -> std::string;
+
 /**
  * Decodes base64 encoded bytes.
  *
diff --git a/cpp/src/vereign/event/broker.cc b/cpp/src/vereign/event/broker.cc
new file mode 100644
index 0000000000000000000000000000000000000000..f86e3e3932ec44b7d2cd0e943e5d1f2bcc499bab
--- /dev/null
+++ b/cpp/src/vereign/event/broker.cc
@@ -0,0 +1,266 @@
+#include <vereign/event/broker.hh>
+
+#include <vereign/restapi/client_session.hh>
+#include <vereign/identity/provider.hh>
+
+namespace {
+
+const auto pollInterval = std::chrono::seconds(1);
+
+const auto eventsModeEntity = std::string{"entity"};
+const auto eventsModeDeviceKey = std::string{"devicekey"};
+
+const auto getNewEventsWithoutSessionPath = std::string{"/event/getNewEventsWithoutSession"};
+const auto getNewEventsPath = std::string{"/event/getNewEvents"};
+
+const auto updateLastViewedWithoutSessionPath = std::string{"/event/updateLastViewedWithoutSession"};
+const auto updateLastViewedPath = std::string{"/event/updateLastViewed"};
+
+auto eventBatchTime(vereign::client_library::GetNewEventsFormResponse& resp) -> int64_t {
+  int64_t max_time = 0;
+  for (auto& event : resp.data()) {
+    max_time = std::max(max_time, event.stamp());
+  }
+
+  return max_time;
+}
+
+}
+
+namespace vereign::event {
+
+Broker::Broker(restapi::ClientSession& client_session)
+  : client_session_{client_session}
+{
+  entity_req_.set_mode(eventsModeEntity);
+  device_req_.set_mode(eventsModeDeviceKey);
+
+  update_entity_req_.set_mode(eventsModeEntity);
+  update_device_req_.set_mode(eventsModeDeviceKey);
+
+  dispatch_thread_ = std::thread([this] {
+    pollAndDispatch();
+  });
+}
+
+auto Broker::Subscribe() -> Subscription {
+  auto Subscribe() -> Subscription;
+  auto channel = std::make_shared<sync::Channel<EventTypeSharedPtr>>(10);
+
+  uint64_t id = 0;
+
+  {
+    std::lock_guard<std::mutex> lock{mu_};
+
+    id = ++next_subscription_id_;
+    subscriptions_[id] = Subscription{};
+    subscriptions_[id].ID = id;
+    subscriptions_[id].EventsChannel = channel;
+  }
+
+  cv_.notify_one();
+
+  return subscriptions_[id];
+}
+
+void Broker::Unsubscribe(const Subscription& subscription) {
+  std::unique_lock<std::mutex> lock{mu_};
+
+  auto it = subscriptions_.find(subscription.ID);
+  if (it == subscriptions_.end()) {
+    return;
+  }
+
+  it->second.EventsChannel->Close();
+
+  subscriptions_.erase(it);
+}
+
+auto Broker::dispatchEvents(EventTypeSharedPtr events) -> int {
+  std::unique_lock<std::mutex> lock{mu_};
+  cv_.wait(lock, [this] {
+    return subscriptions_.size() > 0;
+  });
+
+  int dispatched_cnt = 0;
+
+  for (auto& s : subscriptions_) {
+    auto channel = s.second.EventsChannel;
+
+    auto r = channel->TryAdd(events);
+    if (r.IsOk()) {
+      dispatched_cnt++;
+    }
+  }
+
+  return dispatched_cnt;
+}
+
+// NOTE: it is assumed that the mutex is already locked.
+void Broker::closeAllSubscriptions() {
+  for (auto& s: subscriptions_) {
+    s.second.EventsChannel->Close();
+  }
+}
+
+void Broker::pollEventsWithoutSession() {
+  if (!client_session_.HasIdentity()) {
+    return;
+  }
+
+  auto resp = std::make_shared<client_library::GetNewEventsFormResponse>();
+
+  auto result = client_session_.PublicPost(
+    getNewEventsWithoutSessionPath,
+    &device_req_,
+    resp.get()
+  );
+
+  result.wait();
+
+  // do not dispatch successful poll, when there are no new events
+  if (resp->code() == "200" && resp->data().size() == 0) {
+    return;
+  }
+
+  auto cnt = dispatchEvents(resp);
+  // if nothing is dispatched, do not update the last viewed events
+  if (cnt == 0) {
+    return;
+  }
+
+  auto time = eventBatchTime(*resp);
+  if (time == 0) {
+    return;
+  }
+
+  update_device_req_.set_lastviewed(std::to_string(time));
+  auto update_result = client_session_.PublicPost(
+    updateLastViewedWithoutSessionPath,
+    &update_device_req_,
+    &update_device_resp_
+  );
+
+  update_result.wait();
+
+  // TODO: Add debug log here on failure.
+}
+
+void Broker::pollEntityEvents() {
+  auto resp = std::make_shared<client_library::GetNewEventsFormResponse>();
+
+  auto result = client_session_.Post(getNewEventsPath, &entity_req_, resp.get());
+
+  result.wait();
+
+  // do not dispatch successful poll, when there are no new events
+  if (resp->code() == "200" && resp->data().size() == 0) {
+    return;
+  }
+
+  auto cnt = dispatchEvents(resp);
+  // if nothing is dispatched, do not update the last viewed events
+  if (cnt == 0) {
+    return;
+  }
+
+  auto time = eventBatchTime(*resp);
+  if (time == 0) {
+    return;
+  }
+
+  update_entity_req_.set_lastviewed(std::to_string(time));
+  auto update_result = client_session_.Post(
+    updateLastViewedPath,
+    &update_entity_req_,
+    &update_entity_resp_
+  );
+
+  update_result.wait();
+
+  // TODO: Add debug log here on failure.
+}
+
+void Broker::pollDeviceEvents() {
+  auto resp = std::make_shared<client_library::GetNewEventsFormResponse>();
+
+  auto result = client_session_.Post(getNewEventsPath, &device_req_, resp.get());
+
+  result.wait();
+
+  // do not dispatch successful poll, when there are no new events
+  if (resp->code() == "200" && resp->data().size() == 0) {
+    return;
+  }
+
+  auto cnt = dispatchEvents(resp);
+  // if nothing is dispatched, do not update the last viewed events
+  if (cnt == 0) {
+    return;
+  }
+
+  auto time = eventBatchTime(*resp);
+  if (time == 0) {
+    return;
+  }
+
+  update_device_req_.set_lastviewed(std::to_string(time));
+  auto update_result = client_session_.Post(
+    updateLastViewedPath,
+    &update_device_req_,
+    &update_device_resp_
+  );
+
+  update_result.wait();
+
+  // TODO: Add debug log here on failure.
+}
+
+void Broker::pollAndDispatch() {
+  for (;;) {
+    {
+      std::unique_lock<std::mutex> lock{mu_};
+      cv_.wait(lock, [this] {
+        return subscriptions_.size() > 0 || stopped_ == true;
+      });
+
+      if (stopped_) {
+        closeAllSubscriptions();
+        break;
+      }
+    }
+
+    if (!client_session_.HasSession()) {
+      pollEventsWithoutSession();
+    } else {
+      pollDeviceEvents();
+      pollEntityEvents();
+    }
+
+    std::this_thread::sleep_for(pollInterval);
+  }
+}
+
+void Broker::Shutdown() {
+  {
+    std::lock_guard<std::mutex> lock{mu_};
+
+    if (stopped_) {
+      return;
+    }
+
+    stopped_ = true;
+  }
+
+  cv_.notify_one();
+
+  if (dispatch_thread_.joinable()) {
+    dispatch_thread_.join();
+  }
+}
+
+Broker::~Broker() {
+  Shutdown();
+}
+
+} // namespace vereign::event
diff --git a/cpp/src/vereign/event/broker.hh b/cpp/src/vereign/event/broker.hh
new file mode 100644
index 0000000000000000000000000000000000000000..f8842dc209a989cb1d14f118dbc5ac63aa22a5e6
--- /dev/null
+++ b/cpp/src/vereign/event/broker.hh
@@ -0,0 +1,131 @@
+#ifndef __VEREIGN_EVENT_BROKER_HH
+#define __VEREIGN_EVENT_BROKER_HH
+
+#include <vereign/sync/channel.hh>
+#include <vereign/client_library/event_types.pb.h>
+#include <vereign/client_library/internal/event_types.pb.h>
+#include <vereign/client_library/common_types.pb.h>
+
+#include <thread>
+#include <unordered_map>
+
+// forward declarations
+namespace vereign::restapi {
+
+class ClientSession;
+
+} // namespace vereign
+
+namespace vereign::event {
+
+using EventType = client_library::GetNewEventsFormResponse;
+using EventTypeSharedPtr = std::shared_ptr<EventType>;
+using EventsChannelSharedPtr = std::shared_ptr<sync::Channel<EventTypeSharedPtr>>;
+
+/**
+ * Subscription that is returned, when calling Broker::Subscribe.
+ *
+ * Once the consumer is done consuming, it must call Broker::Unsubscribe, passing the
+ * Subscription object.
+ */
+struct Subscription {
+  /**
+   * Subscription id.
+   */
+  uint64_t ID;
+
+  /**
+   * Channel that the subscribers can use to receive dispatched events.
+   */
+  EventsChannelSharedPtr EventsChannel;
+};
+
+/**
+ * Broker polls restful API for events and dispatches them to a set of subscribers.
+ *
+ * All public methods are thread safe.
+ */
+class Broker {
+public:
+  /**
+   * Creates a broker.
+   *
+   * @param client_session The client session used for polling the restful API for new events.
+   */
+  Broker(restapi::ClientSession& client_session);
+
+  /**
+   * Shutdown the broker.
+   */
+  ~Broker();
+
+  // disable copying
+  Broker(const Broker&) = delete;
+  auto operator=(const Broker&) -> Broker = delete;
+
+  /**
+   * Subscribe for events.
+   *
+   * Once the consumer is done with the returned Subscription, it must call
+   * Broker::Unsubscribe, passing the Subscription object.
+   *
+   * @returns a subscription.
+   */
+  auto Subscribe() -> Subscription;
+
+  /**
+   * Remove a subscription.
+   *
+   * The broker stops to dispatch events to the `subscription` channel.
+   */
+  void Unsubscribe(const Subscription& subscription);
+
+  /**
+   * Shutdown the broker.
+   *
+   * All subscription channels will be closed.
+   *
+   * Shutdown is idempotent, meaning that it can be safely called more than once.
+   */
+  void Shutdown();
+
+private:
+  void pollEventsWithoutSession();
+  void pollDeviceEvents();
+  void pollEntityEvents();
+  void pollAndDispatch();
+  auto dispatchEvents(EventTypeSharedPtr events) -> int;
+  void closeAllSubscriptions();
+
+private:
+  restapi::ClientSession& client_session_;
+
+  std::thread dispatch_thread_;
+
+  client_library::internal::GetNewEventsForm entity_req_;
+
+  client_library::internal::GetNewEventsForm device_req_;
+
+  client_library::internal::UpdateLastViewedForm update_entity_req_;
+
+  client_library::internal::UpdateLastViewedForm update_device_req_;
+
+  client_library::EmptyResponse update_entity_resp_;
+
+  client_library::EmptyResponse update_device_resp_;
+
+  // protects the fields that follow
+  std::mutex mu_;
+
+  std::condition_variable cv_;
+
+  bool stopped_ = false;
+
+  int64_t next_subscription_id_ = 0;
+
+  std::unordered_map<int64_t, Subscription> subscriptions_;
+};
+
+} // namespace vereign::event
+
+#endif // __VEREIGN_EVENT_BROKER_HH
diff --git a/cpp/src/vereign/fs/errors.hh b/cpp/src/vereign/fs/errors.hh
index 035dc67edc34f39c03bbd90bbc649896af48cc3b..ef72c8c00655fd544e4cca926ca1493f94161d73 100644
--- a/cpp/src/vereign/fs/errors.hh
+++ b/cpp/src/vereign/fs/errors.hh
@@ -1,25 +1,32 @@
 #ifndef __VEREIGN_FS_ERRORS_HH
 #define __VEREIGN_FS_ERRORS_HH
 
-#include <exception>
+#include <string>
+#include <stdexcept>
 
 namespace vereign::fs {
 
-class Error : public std::exception {
-  auto what() const noexcept -> const char* override {
-    return "filesystem error";
+class Error : public std::runtime_error {
+public:
+  Error(const std::string& what)
+    : std::runtime_error{what}
+  {
   }
 };
 
-class HomeNotFoundError : public std::exception {
-  auto what() const noexcept -> const char* override {
-    return "cannot find user's home dir";
+class HomeNotFoundError : public Error {
+public:
+  HomeNotFoundError()
+    : Error{"cannot find user's home dir"}
+  {
   }
 };
 
-class TempDirNotFoundError : public std::exception {
-  auto what() const noexcept -> const char* override {
-    return "cannot find user's temp dir";
+class TempDirNotFoundError : public Error {
+public:
+  TempDirNotFoundError()
+    : Error{"cannot find user's temp dir"}
+  {
   }
 };
 
diff --git a/cpp/src/vereign/fs/util.cc b/cpp/src/vereign/fs/util.cc
index 2e39a022f9ecd8d38388467b85beaedb955856d6..8d60e3e827c1228e2f5db2a00444ebab66674db2 100644
--- a/cpp/src/vereign/fs/util.cc
+++ b/cpp/src/vereign/fs/util.cc
@@ -7,6 +7,10 @@
 #include <vereign/core/rand.hh>
 #include <vereign/core/string.hh>
 #include <boost/filesystem/operations.hpp>
+#include <boost/filesystem/fstream.hpp>
+
+// FIXME: remove
+#include <iostream>
 
 #ifdef _WIN32
 # include <shlobj_core.h>
@@ -110,4 +114,40 @@ auto HomePath() -> std::string {
 #endif
 }
 
+auto ReadFile(std::string_view path) -> bytes::Buffer {
+  boost::filesystem::ifstream file{
+    detail::StringToPath(path),
+    boost::filesystem::fstream::binary | boost::filesystem::fstream::ate,
+  };
+
+  if (!file.is_open()) {
+    throw fs::Error("open file for reading failed");
+  }
+
+  auto size = file.tellg();
+  bytes::Buffer result{static_cast<size_t>(size)};
+  file.seekg(0);
+
+  file.read((char*) result.end(), size);
+  result.IncSize(file.gcount());
+
+  file.close();
+
+  return result;
+}
+
+void WriteFile(std::string_view path, bytes::View data) {
+  boost::filesystem::ofstream file{
+    detail::StringToPath(path),
+    boost::filesystem::fstream::binary | boost::filesystem::fstream::trunc,
+  };
+
+  if (!file.is_open()) {
+    throw fs::Error("open file for writing failed");
+  }
+
+  file.write(data.CharData(), data.Size());
+  file.close();
+}
+
 } // namespace vereign::fs
diff --git a/cpp/src/vereign/fs/util.hh b/cpp/src/vereign/fs/util.hh
index c4e9d3dbb03b08f32a63de99fd6a1da389cc8f35..d665902476cd8f2760a7729fe3649027463c13cc 100644
--- a/cpp/src/vereign/fs/util.hh
+++ b/cpp/src/vereign/fs/util.hh
@@ -1,6 +1,7 @@
 #ifndef __VEREIGN_FS_UTIL_HH
 #define __VEREIGN_FS_UTIL_HH
 
+#include <vereign/bytes/buffer.hh>
 #include <boost/filesystem/path.hpp>
 #include <string>
 #include <string_view>
@@ -108,6 +109,9 @@ auto TempDir(std::string_view prefix) -> std::string;
  */
 auto HomePath() -> std::string;
 
+auto ReadFile(std::string_view path) -> bytes::Buffer;
+void WriteFile(std::string_view path, bytes::View data);
+
 } // namespace vereign::fs
 
 #endif // __VEREIGN_FS_UTIL_HH
diff --git a/cpp/src/vereign/grpc/event_api.hh b/cpp/src/vereign/grpc/event_api.hh
new file mode 100644
index 0000000000000000000000000000000000000000..42afa02396fec2ce80a280924e9d1d897f978be6
--- /dev/null
+++ b/cpp/src/vereign/grpc/event_api.hh
@@ -0,0 +1,86 @@
+#ifndef __VEREIGN_GRPC_EVENT_API_HH
+#define __VEREIGN_GRPC_EVENT_API_HH
+
+#include <vereign/grpc/gen/event_api.hh>
+
+#include <vereign/event/broker.hh>
+#include <vereign/core/scope_guard.hh>
+
+// #include <vereign/grpc/error_code.hh>
+// #include <vereign/kvstore/errors.hh>
+// #include <vereign/client_library/common_types.pb.h>
+// #include <vereign/client_library/identity_types.pb.h>
+#include <boost/core/ignore_unused.hpp>
+
+#include <thread>
+
+namespace vereign::grpc {
+
+/**
+ * Implementation of the gRPC `vereign::client_library::EventAPI::Service` service.
+ *
+ * Inherits all the API implementations from the generated gen::EventAPI and adds some
+ * additional implementations.
+ */
+template <class VereignService>
+class EventAPI final : public gen::EventAPI<VereignService> {
+public:
+  // API service name.
+  static constexpr const char* Name = gen::EventAPI<VereignService>::Name;
+
+  using VereignServiceType = VereignService;
+  using VereignServicePtr = std::unique_ptr<VereignService>;
+
+  /**
+   * Constructs EventAPI instance.
+   *
+   * @param service The client library Event service.
+   */
+  EventAPI(VereignServicePtr&& service, event::Broker& events_broker)
+    : gen::EventAPI<VereignService>{std::move(service)},
+      events_broker_{events_broker}
+  {}
+
+  // disable copying
+  EventAPI(const EventAPI&) = delete;
+  auto operator=(const EventAPI&) -> EventAPI& = delete;
+
+  /**
+   * Returns to the gRPC client a gRPC stream for receiving last available events.
+   */
+  auto GetNewEvents(
+    ::grpc::ServerContext* ctx,
+    const client_library::GetNewEventsForm* req,
+    ::grpc::ServerWriter<client_library::GetNewEventsFormResponse>* resp_stream
+  ) -> ::grpc::Status override {
+    boost::ignore_unused(ctx);
+    boost::ignore_unused(req);
+
+    auto subscription = events_broker_.Subscribe();
+    auto unsubscribe = core::MakeScopeGuard([this, &subscription] {
+      events_broker_.Unsubscribe(subscription);
+    });
+    auto channel = subscription.EventsChannel;
+
+    for (;;) {
+      auto msg = channel->Get();
+      if (!msg) {
+        break;
+      }
+
+      auto ok = resp_stream->Write(*msg.Value());
+      if (!ok) {
+        break;
+      }
+    }
+
+    return ::grpc::Status::OK;
+  }
+
+private:
+  event::Broker& events_broker_;
+};
+
+} // namespace vereign::grpc
+
+#endif // __VEREIGN_GRPC_IDENTITY_API_HH
diff --git a/cpp/src/vereign/grpc/server.cc b/cpp/src/vereign/grpc/server.cc
index f0fae25a176de979bd9274693687a1d1ef6b272f..5e1e899f139a340fae45b06ca97874cee9f90ab4 100644
--- a/cpp/src/vereign/grpc/server.cc
+++ b/cpp/src/vereign/grpc/server.cc
@@ -17,6 +17,7 @@
 // manually written api
 #include <vereign/service/identity_service.hh>
 #include <vereign/grpc/identity_api.hh>
+#include <vereign/grpc/event_api.hh>
 
 #include <grpcpp/server.h>
 #include <boost/asio/io_context.hpp>
@@ -28,6 +29,12 @@
 
 #include <boost/asio/executor_work_guard.hpp>
 
+namespace {
+
+constexpr auto shutdownTimeout = std::chrono::seconds(2);
+
+}
+
 namespace vereign::grpc {
 
 namespace asio = boost::asio;
@@ -46,6 +53,7 @@ public:
       ioc_, ssl_context_, vereign_host, vereign_port
     )},
     client_session_{std::make_unique<restapi::ClientSession>(*client_)},
+    event_broker_{std::make_unique<event::Broker>(*client_session_)},
     kvstorage_{nullptr},
     crypto_storage_{nullptr},
     identity_provider_{nullptr},
@@ -60,7 +68,7 @@ public:
 
     kvstorage_ = std::make_unique<kvstore::SqliteStorage>(storage_path);
     crypto_storage_ = std::make_unique<kvstore::CryptoStorage>(*kvstorage_);
-    identity_provider_ = std::make_unique<identity::Provider>(*crypto_storage_);
+    identity_provider_ = std::make_unique<identity::Provider>(*client_session_, *crypto_storage_);
 
     // FIXME: Verify the remote server's certificate
     // ssl_context.set_verify_mode(ssl::verify_peer);
@@ -76,6 +84,12 @@ public:
       *client_session_,
       *identity_provider_
     );
+    services_registry_.RegisterIfNotExist(
+      std::make_unique<EventAPI<service::gen::EventService>>(
+        std::make_unique<service::gen::EventService>(*client_session_),
+        *event_broker_
+      )
+    );
 
     // register all generated services
     grpc::gen::RegisterAll(*client_session_, services_registry_);
@@ -97,9 +111,17 @@ public:
   }
 
   void Shutdown() {
+    if (stopped_.test_and_set()) {
+      return;
+    }
+
     client_session_->Close();
 
-    server_->Shutdown();
+    event_broker_->Shutdown();
+
+    auto deadline = std::chrono::system_clock::now() + shutdownTimeout;
+
+    server_->Shutdown(deadline);
     if (server_thread_.joinable()) {
       server_thread_.join();
     }
@@ -121,6 +143,7 @@ private:
   asio::ssl::context ssl_context_;
   std::unique_ptr<vereign::restapi::Client> client_;
   std::unique_ptr<vereign::restapi::ClientSession> client_session_;
+  std::unique_ptr<vereign::event::Broker> event_broker_;
   ServiceRegistry services_registry_;
   std::unique_ptr<kvstore::Storage> kvstorage_;
   std::unique_ptr<kvstore::CryptoStorage> crypto_storage_;
@@ -128,6 +151,7 @@ private:
   std::unique_ptr<::grpc::Server> server_;
   std::thread server_thread_;
   std::thread service_thread_;
+  std::atomic_flag stopped_ = ATOMIC_FLAG_INIT;
 };
 
 Server::Server(
diff --git a/cpp/src/vereign/grpc/service_registry.hh b/cpp/src/vereign/grpc/service_registry.hh
index 94c0fd355fc8a4c1e7bfae838a07b43f41a603ac..cc60016be9b268e3ad3572242736a73ce72334e6 100644
--- a/cpp/src/vereign/grpc/service_registry.hh
+++ b/cpp/src/vereign/grpc/service_registry.hh
@@ -13,6 +13,18 @@ class ServiceRegistry {
 public:
   void RegisterIntoBuilder(::grpc::ServerBuilder& builder);
 
+  template <class API>
+  auto RegisterIfNotExist(std::unique_ptr<API>&& api) -> bool {
+    auto it = services_.find(API::Name);
+    if (it != services_.end()) {
+      return false;
+    }
+
+    services_[API::Name] = std::move(api);
+
+    return true;
+  }
+
   template <class API>
   auto RegisterIfNotExist(restapi::ClientSession& client_session) -> bool {
     auto it = services_.find(API::Name);
diff --git a/cpp/src/vereign/identity/errors.hh b/cpp/src/vereign/identity/errors.hh
index 7140afec338eeef55150bc988beb260de2b901ac..ffc59e9f9564d081c8c3467747cab9055e5a8598 100644
--- a/cpp/src/vereign/identity/errors.hh
+++ b/cpp/src/vereign/identity/errors.hh
@@ -13,6 +13,25 @@ public:
   }
 };
 
+class ServerSignCertificateError : public Error {
+public:
+  std::string Code;
+  std::string Status;
+  std::string Error;
+
+  ServerSignCertificateError(
+    std::string code,
+    std::string status,
+    std::string error
+  )
+    : identity::Error(error),
+      Code{std::move(code)},
+      Status{std::move(status)},
+      Error{std::move(error)}
+  {
+  }
+};
+
 } // namespace vereign::identity
 
 #endif // __VEREIGN_IDENTITY_ERRORS_HH
diff --git a/cpp/src/vereign/identity/provider.cc b/cpp/src/vereign/identity/provider.cc
index e69494ad66845c3fdd56216b20559ffffb887942..6e96552f319de93e66201bd4423752ce1868fb05 100644
--- a/cpp/src/vereign/identity/provider.cc
+++ b/cpp/src/vereign/identity/provider.cc
@@ -1,18 +1,54 @@
 #include <vereign/identity/provider.hh>
 
+#include <vereign/identity/errors.hh>
+
 #include <vereign/crypto/digest.hh>
 #include <vereign/crypto/bio.hh>
 #include <vereign/crypto/rsa.hh>
+#include <vereign/crypto/cert.hh>
 #include <vereign/encoding/base64.hh>
 
+#include <vereign/client_library/sign_types.pb.h>
+
 namespace {
-  constexpr int rsaKeySizeBits = 2048;
+
+constexpr int rsaKeySizeBits = 2048;
+constexpr const auto profileCertCountry = std::string_view{"CH"};
+constexpr const auto profileCertState = std::string_view{"Zug"};
+constexpr const auto profileCertLocality = std::string_view{"Zug"};
+constexpr const auto profileCertOrganization = std::string_view{"Vereign AG"};
+constexpr const auto profileCertOrganizationUnit = std::string_view{"Business Dep"};
+constexpr const int profileCertValidityYears = 5;
+
+const auto signCertificatePath = std::string{"/sign/signCertificate"};
+
 }
 
 namespace vereign::identity {
 
-Provider::Provider(kvstore::CryptoStorage& storage)
-  : storage_{storage}
+static auto makeProfileCertificate(
+  bytes::View key_pem,
+  const client_library::SignCertificateFormResponsePayload& msg
+) -> std::unique_ptr<ProfileCertificate> {
+
+  auto profile_cert = std::make_unique<ProfileCertificate>();
+
+  profile_cert->PrivateKeyPEM = bytes::Buffer{key_pem};
+  encoding::base64::Decode(bytes::View(msg.signedcertificate()), profile_cert->CertificatePEM);
+  profile_cert->CertificateUUID = msg.certificateuuid();
+
+  for (auto& cert : msg.chain()) {
+    bytes::Buffer buf;
+    encoding::base64::Decode(bytes::View(cert), buf);
+    profile_cert->Chain.push_back(std::move(buf));
+  }
+
+  return profile_cert;
+}
+
+Provider::Provider(restapi::ClientSession& client_session, kvstore::CryptoStorage& storage)
+  : client_session_{client_session},
+    storage_{storage}
 {}
 
 Provider::~Provider() = default;
@@ -33,6 +69,8 @@ auto Provider::RecreateIdentity(const std::string& pin) -> std::string {
   bytes::Buffer encoded;
   encoding::base64::Encode(crypto::bio::View(public_key.get()), encoded);
 
+  has_identity_ = true;
+
   return std::string{encoded.View().String()};
 }
 
@@ -47,6 +85,8 @@ auto Provider::LoadIdentity(const std::string& pin) -> std::string {
   bytes::Buffer encoded;
   encoding::base64::Encode(public_key.View(), encoded);
 
+  has_identity_ = true;
+
   return std::string(encoded.View().String());
 }
 
@@ -77,4 +117,129 @@ auto Provider::GetDeviceHash() -> std::string {
   return std::string(encoded.View().String());
 }
 
+auto Provider::getProfileCertificateFromStorage(
+  const std::string& profile_uuid
+) -> std::unique_ptr<ProfileCertificate> {
+
+  std::lock_guard<std::mutex> l{mu_};
+
+  auto key_pem = bytes::Buffer{};
+  auto cert_binary = bytes::Buffer{};
+
+  storage_.GetBytes("profile-key-" + profile_uuid, key_pem);
+  storage_.GetBytes("profile-cert-" + profile_uuid, cert_binary);
+
+  if (key_pem.Size() == 0 || cert_binary.Size() == 0) {
+    return nullptr;
+  }
+
+  auto cert_msg = client_library::SignCertificateFormResponsePayload{};
+  auto result = cert_msg.ParseFromArray(cert_binary.View().Data(), cert_binary.View().Size());
+  if (!result) {
+    return nullptr;
+  }
+
+  return makeProfileCertificate(key_pem.View(), cert_msg);
+}
+
+auto Provider::GetProfileCertificate(
+  const std::string& profile_uuid
+) -> std::unique_ptr<ProfileCertificate> {
+  auto profile_cert = getProfileCertificateFromStorage(profile_uuid);
+  if (profile_cert) {
+    return profile_cert;
+  }
+
+  auto key = crypto::rsa::GenerateKey(rsaKeySizeBits);
+
+  crypto::cert::CertData cert_data{};
+  cert_data.Subject.CommonName = profile_uuid + "-userdevice";
+  cert_data.Subject.Country = profileCertCountry;
+  cert_data.Subject.State = profileCertState;
+  cert_data.Subject.Locality = profileCertLocality;
+  cert_data.Subject.Organization = profileCertOrganization;
+  cert_data.Subject.OrganizationUnit = profileCertOrganizationUnit;
+
+  // cert_data.Email = "ca@vereign.com"; // added to DN and Subject Alternative Name extension. Optional for CA. Mandatory for leaf certificate, used for email protection
+  // cert_data.Url = "www.vereign.com"; // optional url, recommended for CA, added to Subject Alternative Name extension
+
+  cert_data.Validity.ValidYears = profileCertValidityYears;
+  cert_data.IsCA = true;
+
+  auto cert = crypto::cert::CreateSelfSignedCert(cert_data, key.get());
+  auto cert_pem_bio = crypto::cert::ExportCertToPEM(cert.get());
+  auto cert_pem = encoding::base64::EncodeToString(crypto::bio::View(cert_pem_bio.get()));
+
+  // sign the new profile certificate
+  auto req = client_library::SignCertificateForm{};
+  req.set_passportuuid(profile_uuid);
+  req.set_certificate(cert_pem);
+  auto resp = client_library::SignCertificateFormResponse{};
+
+  auto result = client_session_.Post(signCertificatePath, &req, &resp);
+  result.wait();
+
+  if (resp.code() != "200") {
+    throw ServerSignCertificateError(resp.code(), resp.status(), resp.error());
+  }
+
+  // serialize cert data
+  auto payload_size = resp.data().ByteSizeLong();
+  auto cert_binary = bytes::Buffer{payload_size};
+  resp.data().SerializeToArray(cert_binary.end(), payload_size);
+  cert_binary.IncSize(payload_size);
+
+  auto key_bio = crypto::rsa::ExportPrivateKeyToPEM(key.get());
+
+  {
+    std::lock_guard<std::mutex> l{mu_};
+
+    storage_.PutBytes("profile-key-" + profile_uuid, crypto::bio::View(key_bio.get()));
+    storage_.PutBytes("profile-cert-" + profile_uuid, cert_binary.View());
+  }
+
+  return makeProfileCertificate(crypto::bio::View(key_bio.get()), resp.data());
+}
+
+auto Provider::GetProfileOneTimeCertificate(
+  const std::string& profile_uuid,
+  const std::string& email
+) -> std::unique_ptr<ProfileCertificate> {
+  auto profile_cert = GetProfileCertificate(profile_uuid);
+
+  auto key = crypto::rsa::GenerateKey(rsaKeySizeBits);
+
+  crypto::cert::CertData cert_data{};
+  cert_data.Subject.CommonName = profile_uuid + "-onetime";
+  cert_data.Subject.Country = profileCertCountry;
+  cert_data.Subject.State = profileCertState;
+  cert_data.Subject.Locality = profileCertLocality;
+  cert_data.Subject.Organization = profileCertOrganization;
+  cert_data.Subject.OrganizationUnit = profileCertOrganizationUnit;
+  cert_data.Email = email;
+
+  cert_data.Validity.ValidYears = profileCertValidityYears;
+  cert_data.IsCA = false;
+
+  auto issuer_cert = crypto::cert::ImportCertFromPEM(profile_cert->CertificatePEM.View());
+  auto issuer_pkey = crypto::rsa::ImportPrivateKeyFromPEM(profile_cert->PrivateKeyPEM.View());
+
+  auto cert = crypto::cert::CreateCert(cert_data, issuer_cert.get(), issuer_pkey.get(),  key.get());
+  auto cert_bio = crypto::cert::ExportCertToPEM(cert.get());
+
+  auto key_bio = crypto::rsa::ExportPrivateKeyToPEM(key.get());
+
+  auto result = std::make_unique<ProfileCertificate>();
+  result->PrivateKeyPEM = crypto::bio::View(key_bio.get());
+  result->CertificatePEM = crypto::bio::View(cert_bio.get());
+
+  result->Chain.emplace_back(profile_cert->CertificatePEM.View());
+
+  for (const auto& chain_cert : profile_cert->Chain) {
+    result->Chain.emplace_back(chain_cert.View());
+  }
+
+  return result;
+}
+
 } // namespace vereign::identity
diff --git a/cpp/src/vereign/identity/provider.hh b/cpp/src/vereign/identity/provider.hh
index c526e41fa0e2353ca8dc2258609e9076a6a3f17a..c20763d835204009ea56b2c541147f5b8e91a4c3 100644
--- a/cpp/src/vereign/identity/provider.hh
+++ b/cpp/src/vereign/identity/provider.hh
@@ -1,12 +1,26 @@
 #ifndef __VEREIGN_IDENTITY_PROVIDER_HH
 #define __VEREIGN_IDENTITY_PROVIDER_HH
 
+#include <vereign/restapi/client_session.hh>
 #include <vereign/kvstore/crypto_storage.hh>
 
 #include <mutex>
 
 namespace vereign::identity {
 
+/**
+ * A DTO representing a profile/onetime certificate with its private key, UUID and certificate chain.
+ *
+ * @see Provider::GetProfileCertificate
+ * @see Provider::GetProfileOneTimeCertificate
+ */
+struct ProfileCertificate {
+  bytes::Buffer PrivateKeyPEM;
+  bytes::Buffer CertificatePEM;
+  std::string CertificateUUID;
+  std::vector<bytes::Buffer> Chain;
+};
+
 /**
  * Identity provider that manages the locally stored user identity.
  *
@@ -19,7 +33,7 @@ public:
    *
    * @param storage The crypto storage used for read/write identity properties.
    */
-  Provider(kvstore::CryptoStorage& storage);
+  Provider(restapi::ClientSession& client_session, kvstore::CryptoStorage& storage);
 
   /**
    * Default constructor.
@@ -64,10 +78,71 @@ public:
    */
   auto GetDeviceHash() -> std::string;
 
+  /**
+   * Retrieve profile certificate.
+   *
+   * When the profile certificate does not exist in the crypto storage just yet, a new private key
+   * and certificate is created and signed by Vereign server. Then the private key, the certificate
+   * and the issuers certificate chain are stored into the crypto storage.
+   *
+   * This means that the profile certificate is reused for the lifetime of the device registration.
+   *
+   * The profile certificate is a CA certificate and is used for signing one-time certificates.
+   *
+   * The `Common Name` of the certificate is derived from the
+   * `profile_uuid` - "<profile_uuid>-userdevice".
+   *
+   * Currently the profile certificate is part of the following certificate chain:
+   * 1. profile-userdevice
+   * 2. profile-serverside
+   * 3. vereign
+   *
+   * @param profile_uuid The profile UUID.
+   * @returns the profile certificate, its private key and the issuers certificate chain.
+   */
+  auto GetProfileCertificate(const std::string& profile_uuid) -> std::unique_ptr<ProfileCertificate>;
+
+  /**
+   * Creates and returns a one-time certificate for given profile.
+   *
+   * The one-time certificate is signed by the user-device profile certificate.
+   *
+   * When the profile certificate does not exist in the crypto storage just yet, a new private key
+   * and certificate is created and signed by Vereign server. Then the private key, the certificate
+   * and the issuers certificate chain are stored into the crypto storage.
+   *
+   * This means that the profile certificate is reused for the lifetime of the device registration.
+   *
+   * The one-time certificate is a leaf certificate and must be used one only sign operation.
+   *
+   * The `Common Name` of the certificate is derived from the
+   * `profile_uuid` - "<profile_uuid>-onetime".
+   *
+   * Currently the one-time certificate is part of the following certificate chain:
+   * 1. profile-onetime
+   * 2. profile-userdevice
+   * 3. profile-serverside
+   * 4. vereign
+   *
+   */
+  auto GetProfileOneTimeCertificate(
+    const std::string& profile_uuid,
+    const std::string& email
+  ) -> std::unique_ptr<ProfileCertificate>;
+
 private:
+  auto getProfileCertificateFromStorage(
+    const std::string& profile_uuid
+  ) -> std::unique_ptr<ProfileCertificate>;
+
+private:
+  restapi::ClientSession& client_session_;
+
   std::mutex mu_;
 
   kvstore::CryptoStorage& storage_;
+
+  bool has_identity_ = false;
 };
 
 } // namespace vereign::identity
diff --git a/cpp/src/vereign/kvstore/crypto_storage.cc b/cpp/src/vereign/kvstore/crypto_storage.cc
index 7db16463cd4b0cc9947ff11dc2e9e56c54fb5a6c..1990c7f86211005f96ed3618757b1eeacf1af8ba 100644
--- a/cpp/src/vereign/kvstore/crypto_storage.cc
+++ b/cpp/src/vereign/kvstore/crypto_storage.cc
@@ -26,8 +26,8 @@ void CryptoStorage::PutBytes(const std::string& key, bytes::View value) {
   impl_->PutBytes(key, value);
 }
 
-void CryptoStorage::GetBytes(const std::string& key, bytes::Buffer& value) {
-  impl_->GetBytes(key, value);
+auto CryptoStorage::GetBytes(const std::string& key, bytes::Buffer& value) -> bool {
+  return impl_->GetBytes(key, value);
 }
 
 } // namespace vereign::identity
diff --git a/cpp/src/vereign/kvstore/crypto_storage.hh b/cpp/src/vereign/kvstore/crypto_storage.hh
index f0c03648c9d2afe06e7fde140157c691f66d954a..23f0e2fa5af3858d3d0677d012e00e6a3eed02f8 100644
--- a/cpp/src/vereign/kvstore/crypto_storage.hh
+++ b/cpp/src/vereign/kvstore/crypto_storage.hh
@@ -97,11 +97,10 @@ public:
    * @param value Buffer where the value will be returned.
    *
    * @throws StorageNotInitializedError when the storage is not initialized.
-   * @throws ValueNotFoundError when there is no value under the provided key.
    * @throws encoding::Error when the encrypted value cannot be decoded.
    * @throws crypto::OpenSSLError when the value cannot be decrypted.
    */
-  void GetBytes(const std::string& key, bytes::Buffer& value);
+  auto GetBytes(const std::string& key, bytes::Buffer& value) -> bool;
 
 private:
   std::unique_ptr<detail::CryptoStorageImpl> impl_;
diff --git a/cpp/src/vereign/kvstore/detail/base_crypto_storage.cc b/cpp/src/vereign/kvstore/detail/base_crypto_storage.cc
index 83bb4c7b1930b76829af0192b6357b674a42f7ae..44243c88497a01759a559f8e6ece6d430e5a237e 100644
--- a/cpp/src/vereign/kvstore/detail/base_crypto_storage.cc
+++ b/cpp/src/vereign/kvstore/detail/base_crypto_storage.cc
@@ -56,7 +56,7 @@ void BaseCryptoStorageImpl::PutBytes(const std::string& key, bytes::View value)
   encryptBytes(key, value);
 }
 
-void BaseCryptoStorageImpl::GetBytes(const std::string& key, bytes::Buffer& value) const {
+auto BaseCryptoStorageImpl::GetBytes(const std::string& key, bytes::Buffer& value) const -> bool {
   kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep};
 
   if (key_.Size() == 0) {
@@ -64,7 +64,10 @@ void BaseCryptoStorageImpl::GetBytes(const std::string& key, bytes::Buffer& valu
   }
 
   bytes::Buffer encoded;
-  storage_.GetBytes(key, encoded);
+  auto found = storage_.GetBytes(key, encoded);
+  if (!found) {
+    return false;
+  }
 
   bytes::View iv;
   bytes::View tag;
@@ -72,6 +75,8 @@ void BaseCryptoStorageImpl::GetBytes(const std::string& key, bytes::Buffer& valu
   DecodeEncryptedValue(encoded.View(), iv, tag, encrypted);
 
   crypto::aes::GCM256Decrypt(encrypted, key_.View(), iv, tag, value);
+
+  return true;
 }
 
 void BaseCryptoStorageImpl::initKey(bytes::Buffer&& key) {
@@ -81,9 +86,8 @@ void BaseCryptoStorageImpl::initKey(bytes::Buffer&& key) {
 auto BaseCryptoStorageImpl::isTagValid() const -> bool {
   bytes::Buffer tag;
   try {
-    GetBytes("__tag", tag);
+    return GetBytes("__tag", tag);
 
-    return true;
   } catch (const crypto::Error&) {
     return false;
   }
diff --git a/cpp/src/vereign/kvstore/detail/base_crypto_storage.hh b/cpp/src/vereign/kvstore/detail/base_crypto_storage.hh
index 49278c3f6847d4555dfd3ba58fc52a38a7d05e07..3039b8fe0fbb82f9207689dc9faa213795fab5c5 100644
--- a/cpp/src/vereign/kvstore/detail/base_crypto_storage.hh
+++ b/cpp/src/vereign/kvstore/detail/base_crypto_storage.hh
@@ -14,7 +14,7 @@ public:
   auto operator=(const BaseCryptoStorageImpl&) -> BaseCryptoStorageImpl& = delete;
 
   void PutBytes(const std::string& key, bytes::View value);
-  void GetBytes(const std::string& key, bytes::Buffer& value) const;
+  auto GetBytes(const std::string& key, bytes::Buffer& value) const -> bool;
 
 protected:
   void initKey(bytes::Buffer&& key);
diff --git a/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc
index 1d59144d03098d016c2d7d1c29942fb7e95bac07..eb49ca7171ff49c3009c6bd5fbbf7cb9a9303e76 100644
--- a/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc
+++ b/cpp/src/vereign/kvstore/detail/linux_crypto_storage.cc
@@ -15,13 +15,15 @@
 #include <chrono>
 
 namespace {
-  // FIXME: should these be injected and provided by the integrator
-  constexpr int iterations = 1 << 18;
-  constexpr int saltSizeBytes = 16;
-  constexpr int aesKeySizeBytes = 32;
 
-  constexpr int lockRetryCount = 10;
-  constexpr auto lockRetrySleep = std::chrono::milliseconds{1000};
+// FIXME: should these be injected and provided by the integrator
+constexpr int iterations = 1 << 18;
+constexpr int saltSizeBytes = 16;
+constexpr int aesKeySizeBytes = 32;
+
+constexpr int lockRetryCount = 10;
+constexpr auto lockRetrySleep = std::chrono::milliseconds{1000};
+
 }
 
 namespace vereign::kvstore::detail {
@@ -40,16 +42,16 @@ void CryptoStorageImpl::Open(const std::string& pin) {
   kvstore::Lock l{storage_, lockRetryCount, lockRetrySleep};
 
   bytes::Buffer salt;
-
   int64_t iterations = 0;
+
   try {
-    iterations = storage_.GetInt64("__master_key_iterations");
+    storage_.GetInt64("__master_key_iterations", iterations);
     storage_.GetBytes("__master_key_salt", salt);
   } catch (const std::exception& e) {
     throw StorageNotInitializedError{e.what()};
   }
 
-  if (iterations == 0) {
+  if (iterations == 0 || salt.Size() == 0) {
     throw StorageNotInitializedError{"iterations cannot be zero"};
   }
 
diff --git a/cpp/src/vereign/kvstore/detail/win_crypto_storage.cc b/cpp/src/vereign/kvstore/detail/win_crypto_storage.cc
index 6d8651f38261282aa1de180d208e1a3506df1b94..1e78842bec9d48ad965f2486df9836da57d266d0 100644
--- a/cpp/src/vereign/kvstore/detail/win_crypto_storage.cc
+++ b/cpp/src/vereign/kvstore/detail/win_crypto_storage.cc
@@ -18,19 +18,21 @@
 #include <chrono>
 
 namespace {
-  // FIXME: should these be injected and provided by the integrator
-  constexpr int keySizeBits = 2048;
-  constexpr int aesKeySizeBytes = 32;
-
-  constexpr int lockRetryCount = 10;
-  constexpr auto lockRetrySleep = std::chrono::milliseconds{1000};
-
-  // FIXME: ask business for these values
-  constexpr const auto vereignKeyCreationTitle = std::string_view{"Vereign Client"};
-  constexpr const auto vereignKeyDescription = std::string_view{
-    "Vereign Client will use this key to authenticate with the Vereign Services"
-  };
-  constexpr const auto vereignKeyFriendlyName = std::string_view{"Vereign Client Identity Key"};
+
+// FIXME: should these be injected and provided by the integrator
+constexpr int keySizeBits = 2048;
+constexpr int aesKeySizeBytes = 32;
+
+constexpr int lockRetryCount = 10;
+constexpr auto lockRetrySleep = std::chrono::milliseconds{1000};
+
+// FIXME: ask business for these values
+constexpr const auto vereignKeyCreationTitle = std::string_view{"Vereign Client"};
+constexpr const auto vereignKeyDescription = std::string_view{
+  "Vereign Client will use this key to authenticate with the Vereign Services"
+};
+constexpr const auto vereignKeyFriendlyName = std::string_view{"Vereign Client Identity Key"};
+
 }
 
 namespace vereign::kvstore::detail {
diff --git a/cpp/src/vereign/kvstore/errors.hh b/cpp/src/vereign/kvstore/errors.hh
index c119c510273cce58d67071e56da2ad16f43983da..50ec7271532c6b55cc6b8fc3adc7a1ddb1a569db 100644
--- a/cpp/src/vereign/kvstore/errors.hh
+++ b/cpp/src/vereign/kvstore/errors.hh
@@ -13,14 +13,6 @@ public:
   }
 };
 
-class ValueNotFoundError : public Error {
-public:
-  ValueNotFoundError()
-    : Error{"value not found"}
-  {
-  }
-};
-
 class StorageNotInitializedError : public Error {
 public:
   StorageNotInitializedError(const std::string& reason)
diff --git a/cpp/src/vereign/kvstore/sqlite_storage.cc b/cpp/src/vereign/kvstore/sqlite_storage.cc
index 4789346fef43cde99a8701c620d2d93eea7f0fb7..21371363d40fc84cb497c283b869b999baed8ab3 100644
--- a/cpp/src/vereign/kvstore/sqlite_storage.cc
+++ b/cpp/src/vereign/kvstore/sqlite_storage.cc
@@ -83,7 +83,7 @@ void SqliteStorage::PutBytes(const std::string& key, bytes::View value) {
   stmt.Step();
 }
 
-void SqliteStorage::GetBytes(const std::string& key, bytes::Buffer& value) {
+auto SqliteStorage::GetBytes(const std::string& key, bytes::Buffer& value) -> bool {
   kvstore::Lock l{*this};
 
   auto stmt = db_.Prepare("SELECT value FROM storage WHERE key = ?");
@@ -91,10 +91,12 @@ void SqliteStorage::GetBytes(const std::string& key, bytes::Buffer& value) {
   auto end = stmt.Step();
 
   if (end) {
-    throw ValueNotFoundError{};
+    return false;
   }
 
   value.Write(stmt.GetColumnBlob(0));
+
+  return true;
 }
 
 void SqliteStorage::PutInt64(const std::string& key, int64_t value) {
@@ -109,7 +111,7 @@ void SqliteStorage::PutInt64(const std::string& key, int64_t value) {
   stmt.Step();
 }
 
-auto SqliteStorage::GetInt64(const std::string& key) -> int64_t {
+auto SqliteStorage::GetInt64(const std::string& key, int64_t& value) -> bool {
   kvstore::Lock l{*this};
 
   auto stmt = db_.Prepare("SELECT value FROM storage WHERE key = ?");
@@ -117,17 +119,17 @@ auto SqliteStorage::GetInt64(const std::string& key) -> int64_t {
   auto end = stmt.Step();
 
   if (end) {
-    throw ValueNotFoundError{};
+    return false;
   }
 
   auto buf = stmt.GetColumnBlob(0);
   if (buf.Size() != 8) {
-    throw Error("cannot decode bytes size");
+    throw Error("cannot decode int64 value");
   }
 
-  int64_t result = encoding::binary::DecodeUint64(buf);
+  value = encoding::binary::DecodeUint64(buf);
 
-  return result;
+  return true;
 }
 
 } // namespace vereign::kvstore
diff --git a/cpp/src/vereign/kvstore/sqlite_storage.hh b/cpp/src/vereign/kvstore/sqlite_storage.hh
index 6f7d13992cb82dd6c1507e1f66183da9ca4a5012..6fdfce9d8e996351f99c85f0f9dc1f7e6c2525c8 100644
--- a/cpp/src/vereign/kvstore/sqlite_storage.hh
+++ b/cpp/src/vereign/kvstore/sqlite_storage.hh
@@ -72,10 +72,9 @@ public:
    * @param key The key of the value that will be retrieved.
    * @param value Buffer where the value will be returned.
    *
-   * @throws ValueNotFoundError when the key does not exist.
    * @throws sqlite::Error on failure.
    */
-  void GetBytes(const std::string& key, bytes::Buffer& value) override;
+  auto GetBytes(const std::string& key, bytes::Buffer& value) -> bool override;
 
   /**
    * Store int64_t value into the storage.
@@ -93,10 +92,9 @@ public:
    * @param key The key of the value that will be retrieved.
    * @param value Buffer where the value will be returned.
    *
-   * @throws ValueNotFoundError when the key does not exist.
    * @throws sqlite::Error on failure.
    */
-  auto GetInt64(const std::string& key) -> int64_t override;
+  auto GetInt64(const std::string& key, int64_t& value) -> bool override;
 
 private:
   sqlite::Connection db_;
diff --git a/cpp/src/vereign/kvstore/storage.hh b/cpp/src/vereign/kvstore/storage.hh
index 2e8c55344f0c8481e924fb0c4673e263faa05585..ccb809468144a940243ee0e7d4769726056b8673 100644
--- a/cpp/src/vereign/kvstore/storage.hh
+++ b/cpp/src/vereign/kvstore/storage.hh
@@ -53,10 +53,8 @@ public:
    *
    * @param key The key of the value that will be retrieved.
    * @param value Buffer where the value will be returned.
-   *
-   * @throws ValueNotFoundError when the key does not exist.
    */
-  virtual void GetBytes(const std::string& key, bytes::Buffer& value) = 0;
+  virtual auto GetBytes(const std::string& key, bytes::Buffer& value) -> bool = 0;
 
   /**
    * Store int64_t value into the storage.
@@ -71,10 +69,8 @@ public:
    *
    * @param key The key of the value that will be retrieved.
    * @param value Buffer where the value will be returned.
-   *
-   * @throws ValueNotFoundError when the key does not exist.
    */
-  virtual auto GetInt64(const std::string& key) -> int64_t = 0;
+  virtual auto GetInt64(const std::string& key, int64_t& value) -> bool = 0;
 
   /**
    * Destroy and cleanup.
diff --git a/cpp/src/vereign/restapi/client.cc b/cpp/src/vereign/restapi/client.cc
index e42834bb8dc48363d3b2a67bb38ca520b8c1ef2d..38f023c008b9a9c357fc430efa95fdc04083cebf 100644
--- a/cpp/src/vereign/restapi/client.cc
+++ b/cpp/src/vereign/restapi/client.cc
@@ -12,24 +12,23 @@ namespace {
   constexpr std::string_view httpUserAgent = "Vereign Client Library";
 }
 
-namespace vereign {
-namespace restapi {
+namespace vereign::restapi {
 
 constexpr auto defaultTimeout = std::chrono::seconds(30);
 
 Client::Client(
   boost::asio::io_context& ioc,
   boost::asio::ssl::context& ssl_ctx,
-  const std::string& host,
-  const std::string& port
+  std::string host,
+  std::string port
 )
   : user_agent_{httpUserAgent},
     executor_{asio::make_strand(ioc)},
     ssl_ctx_{ssl_ctx},
     resolver_{ioc},
     reader_{nullptr},
-    host_{host},
-    port_{port},
+    host_{std::move(host)},
+    port_{std::move(port)},
     expiry_time_{defaultTimeout},
     connecting_{false},
     writing_{false},
@@ -41,8 +40,8 @@ Client::Client(
 Client::Client(
   boost::asio::io_context& ioc,
   boost::asio::ssl::context& ssl_ctx,
-  const std::string& host,
-  const std::string& port,
+  std::string host,
+  std::string port,
   std::chrono::nanoseconds expiry_time
 )
   : user_agent_{httpUserAgent},
@@ -50,8 +49,8 @@ Client::Client(
     ssl_ctx_{ssl_ctx},
     resolver_{ioc},
     reader_{nullptr},
-    host_{host},
-    port_{port},
+    host_{std::move(host)},
+    port_{std::move(port)},
     expiry_time_{expiry_time},
     connecting_{false},
     writing_{false},
@@ -85,7 +84,7 @@ void Client::Close() {
   );
 }
 
-const asio::executor& Client::GetExecutor() const {
+auto Client::GetExecutor() const -> const asio::executor& {
   return executor_;
 }
 
@@ -161,7 +160,9 @@ void Client::connect(tcp::resolver::results_type results) {
   auto stream = stream_;
   beast::get_lowest_layer(*stream_).async_connect(
     results,
-    [this, stream](beast::error_code ec, endpoint_t) {
+    [this, stream](beast::error_code ec, endpoint_t endpoint) {
+      boost::ignore_unused(endpoint);
+
       if (ec) {
         connecting_ = false;
         handleError(ec.message());
@@ -286,5 +287,4 @@ void Client::readResponse() {
     });
 }
 
-}
-}
+} // namespace vereign::restapi
diff --git a/cpp/src/vereign/restapi/client.hh b/cpp/src/vereign/restapi/client.hh
index 6fddf6037d58c5ce4f8c562ba7b00b273510ec75..c03304c3eb97b650b1635cf2cbbbe49e496baa2f 100644
--- a/cpp/src/vereign/restapi/client.hh
+++ b/cpp/src/vereign/restapi/client.hh
@@ -17,8 +17,7 @@
 #include <chrono>
 #include <type_traits>
 
-namespace vereign {
-namespace restapi {
+namespace vereign::restapi {
 
 namespace beast = boost::beast;
 namespace asio = boost::asio;
@@ -69,8 +68,8 @@ public:
   Client(
     asio::io_context& ioc,
     asio::ssl::context& ssl_ctx,
-    const std::string& host,
-    const std::string& port
+    std::string host,
+    std::string port
   );
 
   /**
@@ -86,8 +85,8 @@ public:
   Client(
     asio::io_context& ioc,
     asio::ssl::context& ssl_ctx,
-    const std::string& host,
-    const std::string& port,
+    std::string host,
+    std::string port,
     std::chrono::nanoseconds expiry_time
   );
 
@@ -100,7 +99,7 @@ public:
 
   // Disable copying
   Client(const Client&) = delete;
-  Client& operator=(const Client&) = delete;
+  auto operator=(const Client&) -> Client& = delete;
 
   /**
    * Retrieve client http user agent string.
@@ -126,7 +125,7 @@ public:
    *
    * @returns the client's strand executor.
    */
-  const asio::executor& GetExecutor() const;
+  auto GetExecutor() const -> const asio::executor&;
 
   /**
    * Post makes a blocking post request.
@@ -365,7 +364,6 @@ private:
   bool closed_;
 };
 
-} // namespace restapi
-} // namespace vereign
+} // namespace vereign::restapi
 
 #endif // __VEREIGN_RESTAPI_CLIENT_HH
diff --git a/cpp/src/vereign/restapi/client_session.hh b/cpp/src/vereign/restapi/client_session.hh
index a94fdce3cb020f7ea9c1a11eec61044c4ba7c83d..55ee67962299230e8a280016a8e5631c69f0a676 100644
--- a/cpp/src/vereign/restapi/client_session.hh
+++ b/cpp/src/vereign/restapi/client_session.hh
@@ -45,6 +45,8 @@ inline auto NoAuthError() -> AuthError {
 }
 
 struct Session {
+  std::string PublicKey;
+  std::string DeviceHash;
   std::string Token;
   std::string UUID;
 };
@@ -69,7 +71,10 @@ public:
   ClientSession(Client& client)
     : client_{client},
       base_path_{"/api"},
-      pub_key_{""}
+      uuid_{""},
+      session_token_{""},
+      pub_key_{""},
+      device_hash_{""}
   {}
 
   /**
@@ -79,9 +84,13 @@ public:
    * @param base_path API base path, for example `/api`
    * @returns ClientSession instance.
    */
-  ClientSession(Client& client, std::string base_path) : client_{client},
+  ClientSession(Client& client, std::string base_path)
+    : client_{client},
       base_path_{std::move(base_path)},
-      pub_key_{""}
+      uuid_{""},
+      session_token_{""},
+      pub_key_{""},
+      device_hash_{""}
   {}
 
   // Disable copying.
@@ -102,21 +111,24 @@ public:
    *
    * @param key The public key that will be used for authentication.
    */
-  void SetPubKey(std::string key) {
+  void SetPubKey(std::string key, std::string hash) {
     std::lock_guard<std::mutex> l{mu_};
 
     pub_key_ = std::move(key);
+    device_hash_ = std::move(hash);
     session_token_ = "";
     uuid_ = "";
   }
 
   /**
-   * Retrieve the current public key used for authentication.
+   * Checks if the client has identity.
+   *
+   * @returns true when there device identity exits, false otherwise.
    */
-  auto GetPubKey() -> std::string {
+  auto HasIdentity() -> bool {
     std::lock_guard<std::mutex> l{mu_};
 
-    return pub_key_;
+    return pub_key_.size() != 0;
   }
 
   /**
@@ -139,7 +151,7 @@ public:
   }
 
   /**
-   * Post makes a blocking post request.
+   * Post makes an authenticated blocking post request.
    *
    * The passed `req` and `resp` parameters are moved in and returned back once,
    * the returned future is resolved.
@@ -181,7 +193,7 @@ public:
   }
 
   /**
-   * PostAsync makes non blocking post request.
+   * PostAsync makes an authenticated non blocking post request.
    *
    * The passed `req` and `resp` parameters are moved in and returned back once
    * the CompletionFunc is called.
@@ -225,6 +237,93 @@ public:
     );
   }
 
+  /**
+   * PublicPostAsync makes a non authenticated non blocking post request.
+   *
+   * The request is without session, only the device identity - public key, device hash are used
+   * for this request.
+   * It is suitable for the public API.
+   *
+   * The passed `req` and `resp` parameters are moved in and returned back once
+   * the CompletionFunc is called.
+   *
+   * @tparam CompletionFunc A completion functor - `void (Result&&)`, where
+   *    the `Result` is restapi::PostResult <RequestPtr, ResponsePtr>, where
+   *    RequestPtr is `std::remove_reference<RequestPtrType>::type` and
+   *    ResponsePtr is `std::remove_reference<RequestPtrType>::type`.
+   *
+   * @param path HTTP path, for example `/passport/listPassports`.
+   * @param req Request object that will be serialized as JSON body.
+   *    It can be a const pointer or std::unique_ptr to protobuf message.
+   *
+   * @param resp Response object that will be used to decode the response.
+   *    It can be a  pointer or std::unique_ptr to protobuf message.
+   *
+   * @param func Completion func, that will be called when the post request
+   *    is finished.
+   */
+  template <class RequestPtrType, class ResponsePtrType, class CompletionFunc>
+  void PublicPostAsync(
+    const std::string& path,
+    RequestPtrType&& req,
+    ResponsePtrType&& resp,
+    CompletionFunc&& cf
+  ) {
+
+    auto session = getSession();
+
+    client_.PostAsync(
+      base_path_ + path,
+      std::move(req),
+      std::move(resp),
+      std::vector<vereign::restapi::HttpHeader>{
+        {"publicKey", session.PublicKey},
+        {"deviceHash", session.DeviceHash}
+      },
+      std::move(cf)
+    );
+  }
+
+  /**
+   * PublicPost makes a non authenticated blocking post request.
+   *
+   * The request is without session, only the device identity - public key, device hash are used
+   * for this request.
+   * It is suitable for the public API.
+   *
+   * The passed `req` and `resp` parameters are moved in and returned back once,
+   * the returned future is resolved.
+   *
+   * @param path HTTP path, for example `/passport/listPassports`.
+   *    The path will be added to the ClientSession base path.
+   * @param req Request object that will be serialized as JSON body.
+   *    It can be a const pointer or std::unique_ptr to protobuf message.
+   *
+   * @param resp Response object that will be used to decode the response.
+   *    It can be a  pointer or std::unique_ptr to protobuf message.
+   *
+   * @returns future that will be resolved with a result containing both
+   *    the request and response objects originally passed to the `Post` call.
+   */
+  template <class RequestPtrType, class ResponsePtrType>
+  auto PublicPost(
+    const std::string& path,
+    RequestPtrType&& req,
+    ResponsePtrType&& resp
+  ) {
+    auto session = getSession();
+
+    return client_.Post(
+      base_path_ + path,
+      std::move(req),
+      std::move(resp),
+      std::vector<vereign::restapi::HttpHeader>{
+        {"publicKey", session.PublicKey},
+        {"deviceHash", session.DeviceHash}
+      }
+    );
+  }
+
   /**
    * Initiate authentication.
    *
@@ -233,13 +332,18 @@ public:
    * @param pub_key The public key to authenticate with.
    * @param resp Authentication response.
    */
-  auto Authenticate(const std::string& pub_key, client_library::EmptyResponse* resp) {
+  auto Authenticate(
+    const std::string& pub_key,
+    const std::string& device_hash,
+    client_library::EmptyResponse* resp
+  ) {
     std::promise<void> promise;
     auto future = promise.get_future();
 
     {
       std::lock_guard<std::mutex> l{mu_};
       pub_key_ = pub_key;
+      device_hash_ = device_hash;
       session_token_ = "";
       uuid_ = "";
     }
@@ -260,6 +364,17 @@ public:
     future.wait();
   }
 
+  /**
+   * Checks if the client is authenticated.
+   *
+   * @returns true when there is successfully authenticated session, false otherwise.
+   */
+  auto HasSession() -> bool {
+    std::lock_guard<std::mutex> l{mu_};
+
+    return session_token_.size() != 0;
+  }
+
   /**
    * Retrieve the restful-api base path.
    *
@@ -270,10 +385,12 @@ public:
   }
 
 private:
-  auto hasSession() -> bool {
+
+  void destroySession() {
     std::lock_guard<std::mutex> l{mu_};
 
-    return session_token_.size() != 0;
+    session_token_ = "";
+    uuid_ = "";
   }
 
   void updateSession(const std::string& token, const std::string& uuid) {
@@ -287,6 +404,8 @@ private:
     std::lock_guard<std::mutex> l{mu_};
 
     return detail::Session{
+      pub_key_,
+      device_hash_,
       session_token_,
       uuid_
     };
@@ -317,7 +436,8 @@ private:
       std::move(req),
       std::move(resp),
       std::vector<vereign::restapi::HttpHeader>{
-        {"publicKey", GetPubKey()},
+        {"publicKey", session.PublicKey},
+        {"deviceHash", session.DeviceHash},
         {"token", session.Token},
         {"uuid", session.UUID}
       },
@@ -356,7 +476,8 @@ private:
       std::move(req),
       std::move(resp),
       std::vector<vereign::restapi::HttpHeader>{
-        {"publicKey", GetPubKey()},
+        {"publicKey", session.PublicKey},
+        {"deviceHash", session.DeviceHash},
         {"token", session.Token},
         {"uuid", session.UUID}
       },
@@ -372,7 +493,10 @@ private:
   ) {
     using namespace std::placeholders;
     auto& resp = result.Response;
+
     if (resp->code() == "400" && resp->status() == "Bad session") {
+      destroySession();
+
       withAuthentication(
         std::bind(
           &ClientSession::authPostRetryAsync<RequestPtr, ResponsePtr, CompletionFunc>,
@@ -396,7 +520,8 @@ private:
     asio::post(
       client_.GetExecutor(),
       [this, cf = std::move(cf)]() mutable {
-        if (hasSession()) {
+        auto session = getSession();
+        if (!session.Token.empty()) {
           cf(detail::NoAuthError());
           return;
         }
@@ -410,7 +535,8 @@ private:
           std::make_unique<client_library::EmptyRequest>(),
           std::make_unique<client_library::LoginFormPreviousAddedDeviceResponse>(),
           std::vector<vereign::restapi::HttpHeader>{
-            {"publicKey", GetPubKey()}
+            {"publicKey", session.PublicKey},
+            {"deviceHash", session.DeviceHash}
           },
           [this, cf = std::move(cf)] (ResultType&& result) mutable {
             if (result.Response->code() != "200") {
@@ -456,9 +582,11 @@ private:
   // session token
   std::string session_token_;
 
-
   // public key used for creating the authenticated sessions
   std::string pub_key_;
+
+  // public key hash
+  std::string device_hash_;
 };
 
 } // namespace vereign
diff --git a/cpp/src/vereign/restapi/detail/post_task.hh b/cpp/src/vereign/restapi/detail/post_task.hh
index 74f1ea70ae57b105207529f64c86aa27aaaa48bc..88b84794f1329bf69743cf4d69c6bab4472fe104 100644
--- a/cpp/src/vereign/restapi/detail/post_task.hh
+++ b/cpp/src/vereign/restapi/detail/post_task.hh
@@ -12,8 +12,7 @@
 #include <google/protobuf/util/json_util.h>
 #include <fmt/core.h>
 
-namespace vereign {
-namespace restapi {
+namespace vereign::restapi {
 
 namespace beast = boost::beast;
 
@@ -109,6 +108,8 @@ public:
 
   void Complete(const PostError& err) override {
     if (err) {
+      resp_->set_code("500");
+      resp_->set_status("http client error");
       resp_->set_error(err.value());
     }
 
@@ -125,6 +126,14 @@ public:
 private:
 
   void decodeErrorResponse(const HttpResponse& httpResp) {
+    if (httpResp.result() == beast::http::status::not_found) {
+      resp_->set_code("404");
+      resp_->set_status(httpResp.body());
+      resp_->set_error(httpResp.body());
+
+      return;
+    }
+
     google::protobuf::util::JsonParseOptions options;
 
     client_library::ErrorStringResponse stringResp;
@@ -168,7 +177,6 @@ private:
 };
 
 } // namespace detail
-} // namespace restapi
 } // namespace vereign
 
 #endif // __VEREIGN_RESTAPI_DETAIL_POST_TASK_HH
diff --git a/cpp/src/vereign/service/identity_service.cc b/cpp/src/vereign/service/identity_service.cc
index fe39c2be506f67faa36dff971006e9780f35a641..9a23b616b7b843afaf409989f530c6fd5a68faeb 100644
--- a/cpp/src/vereign/service/identity_service.cc
+++ b/cpp/src/vereign/service/identity_service.cc
@@ -28,7 +28,7 @@ void IdentityService::LoginWithExistingPubKey(
   const client_library::LoginWithExistingPubKeyForm* req,
   client_library::EmptyResponse* resp
 ) {
-  client_session_.Authenticate(req->pubkey(), resp);
+  client_session_.Authenticate(req->pubkey(), "", resp);
 }
 
 void IdentityService::LoginWithNewDevice(
@@ -50,7 +50,7 @@ void IdentityService::LoginWithNewDevice(
   result.wait();
 
   if (resp->code() == "200") {
-    client_session_.SetPubKey(public_key);
+    client_session_.SetPubKey(public_key, identity_provider_.GetDeviceHash());
   }
 }
 
@@ -60,7 +60,7 @@ void IdentityService::LoginWithPreviouslyAddedDevice(
   client_library::EmptyResponse* resp
 ) {
   auto public_key = identity_provider_.LoadIdentity(req->pin());
-  client_session_.Authenticate(public_key, resp);
+  client_session_.Authenticate(public_key, identity_provider_.GetDeviceHash(), resp);
 }
 
 } // namespace vereign
diff --git a/cpp/src/vereign/sync/channel.hh b/cpp/src/vereign/sync/channel.hh
index ff6ff439de10fa058ee1a80460cfc8feb1e69dd2..0e1544b29957820a0bd9218b2a8736618c3b9d27 100644
--- a/cpp/src/vereign/sync/channel.hh
+++ b/cpp/src/vereign/sync/channel.hh
@@ -1,13 +1,12 @@
 #ifndef __VEREIGN_SYNC_CHANNEL_HH
 #define __VEREIGN_SYNC_CHANNEL_HH
 
-#include <deque>
+#include <vereign/container/bounded_queue.hh>
+
 #include <condition_variable>
 #include <boost/optional.hpp>
-#include <iostream>
 
-namespace vereign {
-namespace sync {
+namespace vereign::sync {
 
 /**
  * ChannelAddResult is used as return value by Channel TryAdd methods.
@@ -20,7 +19,8 @@ public:
   /**
    * Default constructor - the result is ok.
    */
-  ChannelAddResult(): closed_(false), full_(false) {}
+  ChannelAddResult() = default;
+
   /**
    * Creates ChannelAddResult.
    *
@@ -34,7 +34,7 @@ public:
    *
    * @returns true when the add operation was successful.
    */
-  bool IsOk() const noexcept {
+  auto IsOk() const noexcept -> bool {
     return !closed_ && !full_;
   }
 
@@ -52,7 +52,7 @@ public:
    *
    * @returns true when the channel was closed.
    */
-  bool IsClosed() const noexcept {
+  auto IsClosed() const noexcept -> bool {
     return closed_;
   }
 
@@ -63,13 +63,13 @@ public:
    *
    * @returns true when the channel was full.
    */
-  bool IsFull() const noexcept {
+  auto IsFull() const noexcept -> bool {
     return full_;
   }
 
 private:
-  bool closed_;
-  bool full_;
+  bool closed_ = false;
+  bool full_ = false;
 };
 
 /**
@@ -102,31 +102,31 @@ public:
 
   // The Channel Value is only move constructible and move assignable.
   ChannelValue(ChannelValue&&) = default;
-  ChannelValue& operator=(ChannelValue&&) = default;
+  auto operator=(ChannelValue&&) -> ChannelValue& = default;
   ChannelValue(const ChannelValue&) = delete;
-  ChannelValue& operator=(const ChannelValue&) = delete;
+  auto operator=(const ChannelValue&) -> ChannelValue& = delete;
 
   operator bool() const noexcept {
     return HasValue();
   }
 
-  bool HasValue() const noexcept {
+  auto HasValue() const noexcept -> bool {
     return value_.has_value();
   }
 
-  const ValueType& Value() const {
+  auto Value() const -> const ValueType& {
     return value_.value();
   }
 
-  ValueType& Value() {
+  auto Value() -> ValueType& {
     return value_.value();
   }
 
-  bool IsClosed() const noexcept {
+  auto IsClosed() const noexcept -> bool {
     return closed_;
   }
 
-  bool IsEmpty() const noexcept {
+  auto IsEmpty() const noexcept -> bool {
     return empty_;
   }
 
@@ -164,7 +164,7 @@ public:
    */
   explicit Channel(std::size_t capacity)
     : closed_{false},
-      capacity_{capacity}
+      queue_{capacity}
   {
   }
 
@@ -177,17 +177,17 @@ public:
    * @param value The value is moved into the new element.
    * @returns false when the channel is closed, and the value could not be pushed.
    */
-  bool Add(ValueType&& value) {
+  auto Add(ValueType&& value) -> bool {
     std::unique_lock<std::mutex> lock(mu_);
     writers_cv_.wait(lock, [this]() {
-      return queue_.size() < capacity_ || closed_;
+      return !queue_.IsFull() || closed_;
     });
 
     if (closed_) {
       return false;
     }
 
-    queue_.push_back(std::move(value));
+    queue_.PushBack(std::move(value));
     lock.unlock();
 
     readers_cv_.notify_one();
@@ -204,17 +204,17 @@ public:
    * @param value The value is copied and added to the channel.
    * @returns false when the channel is closed, and the value could not be pushed.
    */
-  bool Add(const ValueType& value) {
+  auto Add(const ValueType& value) -> bool {
     std::unique_lock<std::mutex> lock(mu_);
     writers_cv_.wait(lock, [this]() {
-      return queue_.size() < capacity_ || closed_;
+      return !queue_.IsFull() || closed_;
     });
 
     if (closed_) {
       return false;
     }
 
-    queue_.push_back(value);
+    queue_.PushBack(value);
     lock.unlock();
 
     readers_cv_.notify_one();
@@ -233,16 +233,16 @@ public:
    * @returns a add result, that can be used to check if the operation was
    *    successful, and what was the channel state - closed, full.
    */
-  ChannelAddResult TryAdd(ValueType&& value) {
+  auto TryAdd(ValueType&& value) -> ChannelAddResult {
     ChannelAddResult result;
     {
       std::lock_guard<std::mutex> lock(mu_);
-      result = ChannelAddResult{closed_, queue_.size() >= capacity_};
+      result = ChannelAddResult{closed_, queue_.IsFull()};
       if (!result) {
         return result;
       }
 
-      queue_.push_back(std::move(value));
+      queue_.PushBack(std::move(value));
     }
 
     readers_cv_.notify_one();
@@ -261,16 +261,16 @@ public:
    * @returns a add result, that can be used to check if the operation was
    *    successful, and what was the channel state - closed, full.
    */
-  ChannelAddResult TryAdd(const ValueType& value) {
+  auto TryAdd(const ValueType& value) -> ChannelAddResult {
     ChannelAddResult result;
     {
       std::lock_guard<std::mutex> lock(mu_);
-      result = ChannelAddResult{closed_, queue_.size() >= capacity_};
+      result = ChannelAddResult{closed_, queue_.IsFull()};
       if (!result) {
         return result;
       }
 
-      queue_.push_back(value);
+      queue_.PushBack(value);
     }
 
     readers_cv_.notify_one();
@@ -280,30 +280,31 @@ public:
 
   /**
    * Get retrieves a value from the channel.
-   * If the channel is empty, this call blocks until there is something added
-   * into the channel or the channel is empty but closed.
+   *
+   * If the channel is empty, this call blocks until there is something added into the channel or
+   * the channel is empty but closed.
    *
    * @returns the retrieved value.
    *    The retrieved value has optional semantics.
    *    One must check if the value exists before using it.
    *    See the ChannelValue class docs.
    */
-  ChannelValue<ValueType> Get() {
+  auto Get() -> ChannelValue<ValueType> {
     std::unique_lock<std::mutex> lock(mu_);
     readers_cv_.wait(lock, [this]() {
-      return queue_.size() > 0 || closed_;
+      return !queue_.IsEmpty() || closed_;
     });
 
-    if (queue_.size() == 0) {
-      return ChannelValue<ValueType>{closed_, queue_.size() == 0};
+    if (queue_.IsEmpty()) {
+      return ChannelValue<ValueType>{closed_, true};
     }
 
     auto result = ChannelValue<ValueType>{
-      std::move(queue_.front()),
+      std::move(queue_.Front()),
       closed_,
-      queue_.size() == 0,
+      queue_.IsEmpty(),
     };
-    queue_.pop_front();
+    queue_.PopFront();
 
     lock.unlock();
     writers_cv_.notify_one();
@@ -339,16 +340,10 @@ private:
   // signify if the channel is closed.
   bool closed_;
 
-  // maximum number of elements buffered inside the channel.
-  std::size_t capacity_;
-
   // the internal queue used by the channel.
-  // TODO: replace the deque with circular buffer since the channel's capacity
-  //       is fixed and known during the construction time.
-  std::deque<ValueType> queue_;
+  container::BoundedQueue<ValueType> queue_;
 };
 
-}
-}
+} // namespace vereign::sync
 
-#endif
+#endif // __VEREIGN_SYNC_CHANNEL_HH
diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt
index 62bd6f48b027be1b1605a1f083be7c52d0ecaf00..c7bbbfcf2f6008b9cefbca1edce83566eed8f885 100644
--- a/cpp/tests/CMakeLists.txt
+++ b/cpp/tests/CMakeLists.txt
@@ -1,7 +1,20 @@
 
 include_directories(
   ${CMAKE_CURRENT_SOURCE_DIR}
-  "${VENDOR_INSTALL_DIR}/boost/include"
+)
+
+set(TESTUTIL_SRC
+  experiment/array.cc
+
+  testutil/protobuf.cc
+  testutil/golden.cc
+)
+add_library(testutil STATIC ${TESTUTIL_SRC})
+target_link_libraries(testutil PRIVATE
+  vereignlib
+)
+target_include_directories(testutil PRIVATE
+  ${CMAKE_SOURCE_DIR}/src
 )
 
 add_subdirectory("protobuf")
diff --git a/cpp/tests/integration/integration_test.cc b/cpp/tests/integration/integration_test.cc
index e3db4498e0af99e6d0e70d425f56ee9a55933a91..482e13d76750910dcf20cf7a3c6e23142142d27a 100644
--- a/cpp/tests/integration/integration_test.cc
+++ b/cpp/tests/integration/integration_test.cc
@@ -85,8 +85,8 @@ TEST_CASE("C API integration", "[.integration]") {
 
   // std::cout << vereign::test::ProtobufToJson(getDIDsResp) << std::endl;
 
-  grpc_shutdown();
-  google::protobuf::ShutdownProtobufLibrary();
+  // grpc_shutdown();
+  // google::protobuf::ShutdownProtobufLibrary();
 }
 
 TEST_CASE("vereign_service_start") {
diff --git a/cpp/tests/util/env.hh b/cpp/tests/testutil/env.hh
similarity index 73%
rename from cpp/tests/util/env.hh
rename to cpp/tests/testutil/env.hh
index d32fd97f8d66d0f25e76758425e426ae8b449079..fdb3d27635542fc84c448408fdd2ab152872f11e 100644
--- a/cpp/tests/util/env.hh
+++ b/cpp/tests/testutil/env.hh
@@ -1,10 +1,10 @@
-#ifndef __VEREIGN_TEST_UTIL_ENV_HH
-#define __VEREIGN_TEST_UTIL_ENV_HH
+#ifndef __VEREIGN_TESTUTIL_ENV_HH
+#define __VEREIGN_TESTUTIL_ENV_HH
 
 #include <string>
 #include <fmt/core.h>
 
-namespace vereign::test {
+namespace vereign::testutil {
 
 inline auto RequireEnv(const std::string& name) -> std::string {
   auto var = std::getenv(name.c_str());
@@ -24,6 +24,6 @@ inline auto GetEnv(const std::string& name, const std::string& default_) -> std:
   return var;
 }
 
-} // namespace test
+} // namespace vereign::testutil
 
-#endif // __VEREIGN_TEST_UTIL_ENV_HH
+#endif // __VEREIGN_TESTUTIL_ENV_HH
diff --git a/cpp/tests/util/error.hh b/cpp/tests/testutil/error.hh
similarity index 53%
rename from cpp/tests/util/error.hh
rename to cpp/tests/testutil/error.hh
index 85d7951d333c60aa7c222cab272ff6737798fd95..90a1fdb1beead8a790005b22c57baefc8cfcf08f 100644
--- a/cpp/tests/util/error.hh
+++ b/cpp/tests/testutil/error.hh
@@ -1,9 +1,9 @@
-#ifndef __VEREIGN_TEST_UTIL_ERROR_HH
-#define __VEREIGN_TEST_UTIL_ERROR_HH
+#ifndef __VEREIGN_TESTUTIL_ERROR_HH
+#define __VEREIGN_TESTUTIL_ERROR_HH
 
 #include <optional>
 
-namespace vereign::test {
+namespace vereign::testutil {
 
 template <class Error, class Fun>
 auto CatchError(Fun fn) -> std::optional<Error> {
@@ -17,6 +17,6 @@ auto CatchError(Fun fn) -> std::optional<Error> {
   }
 }
 
-} // namespace test
+} // namespace vereign::testutil
 
-#endif // __VEREIGN_TEST_UTIL_ERROR_HH
+#endif // __VEREIGN_TESTUTIL_ERROR_HH
diff --git a/cpp/tests/testutil/golden.cc b/cpp/tests/testutil/golden.cc
new file mode 100644
index 0000000000000000000000000000000000000000..e5a011263d8cf9211b0082da94c5b67c898e8204
--- /dev/null
+++ b/cpp/tests/testutil/golden.cc
@@ -0,0 +1,26 @@
+#include <testutil/golden.hh>
+
+#include <vereign/fs/util.hh>
+#include <testutil/env.hh>
+
+#include <catch2/catch.hpp>
+
+namespace {
+  constexpr const char* GoldenUpdateEnvVar = "GOLDEN_UPDATE";
+}
+
+namespace vereign::testutil::golden {
+
+void Assert(const std::string& golden_file_path, bytes::View src) {
+  auto update = vereign::testutil::GetEnv(GoldenUpdateEnvVar, "");
+  if (!update.empty()) {
+    fs::WriteFile(golden_file_path, src);
+  }
+
+  auto data = fs::ReadFile(golden_file_path);
+
+  // TODO: create nice patch like diff on failure
+  CHECK(data.View().String() == src.String());
+}
+
+} // namespace vereign::test
diff --git a/cpp/tests/testutil/golden.hh b/cpp/tests/testutil/golden.hh
new file mode 100644
index 0000000000000000000000000000000000000000..2fb33bc97c6a742d35a6f569fa026431ece91a11
--- /dev/null
+++ b/cpp/tests/testutil/golden.hh
@@ -0,0 +1,13 @@
+#ifndef __VEREIGN_TESTUTIL_GOLDEN_HH
+#define __VEREIGN_TESTUTIL_GOLDEN_HH
+
+#include <vereign/bytes/view.hh>
+#include <string>
+
+namespace vereign::testutil::golden {
+
+void Assert(const std::string& golden_file, bytes::View src);
+
+} // namespace vereign::test
+
+#endif // __VEREIGN_TESTUTIL_GOLDEN_HH
diff --git a/cpp/tests/util/protobuf.cc b/cpp/tests/testutil/protobuf.cc
similarity index 74%
rename from cpp/tests/util/protobuf.cc
rename to cpp/tests/testutil/protobuf.cc
index 05e1c791a91878f1d09a4c0068227de89e7937e1..c6ecf9991f052196651cb71af76dbf7598b1ac94 100644
--- a/cpp/tests/util/protobuf.cc
+++ b/cpp/tests/testutil/protobuf.cc
@@ -1,12 +1,11 @@
 #include <vereign/grpc/json/encoder.hh>
-#include <util/protobuf.hh>
+#include <testutil/protobuf.hh>
 
 #include <google/protobuf/util/json_util.h>
 
 #include <catch2/catch.hpp>
 
-namespace vereign {
-namespace test {
+namespace vereign::testutil {
 
 auto ProtobufToJson(const google::protobuf::Message& msg) -> std::string {
   std::string result;
@@ -15,5 +14,4 @@ auto ProtobufToJson(const google::protobuf::Message& msg) -> std::string {
   return result;
 }
 
-} // namespace vereign
-} // namespace test
+} // namespace vereign::testutil
diff --git a/cpp/tests/testutil/protobuf.hh b/cpp/tests/testutil/protobuf.hh
new file mode 100644
index 0000000000000000000000000000000000000000..264f7f6e51011c8678e68e0d943176ab0217e497
--- /dev/null
+++ b/cpp/tests/testutil/protobuf.hh
@@ -0,0 +1,13 @@
+#ifndef __VEREIGN_TEST_UTIL_PROTOBUF_HH
+#define __VEREIGN_TEST_UTIL_PROTOBUF_HH
+
+#include <string>
+#include <google/protobuf/message.h>
+
+namespace vereign::testutil {
+
+auto ProtobufToJson(const google::protobuf::Message& msg) -> std::string;
+
+} // namespace vereign::testutil
+
+#endif // __VEREIGN_TESTUTIL_PROTOBUF_HH
diff --git a/cpp/tests/util/protobuf.hh b/cpp/tests/util/protobuf.hh
deleted file mode 100644
index 09ca9be1f1b36d9d1444160333d0017ffc396b8e..0000000000000000000000000000000000000000
--- a/cpp/tests/util/protobuf.hh
+++ /dev/null
@@ -1,15 +0,0 @@
-#ifndef __VEREIGN_TEST_UTIL_PROTOBUF_HH
-#define __VEREIGN_TEST_UTIL_PROTOBUF_HH
-
-#include <string>
-#include <google/protobuf/message.h>
-
-namespace vereign {
-namespace test {
-
-std::string ProtobufToJson(const google::protobuf::Message& msg);
-
-} // namespace vereign
-} // namespace test
-
-#endif // __VEREIGN_TEST_UTIL_PROTOBUF_HH
diff --git a/cpp/tests/vereign/CMakeLists.txt b/cpp/tests/vereign/CMakeLists.txt
index 0bcfbc6e919333d7cae2525229b4c53b0cf21247..913e3148a3e61291ac1214e1bf07aa0bfdb7a10d 100644
--- a/cpp/tests/vereign/CMakeLists.txt
+++ b/cpp/tests/vereign/CMakeLists.txt
@@ -12,12 +12,12 @@ include_directories(
 
 list(APPEND TESTS_SRC
   init_tests.cc
-  ../util/protobuf.cc
-  ../experiment/array.cc
   test/device.cc
   test/service_context.cc
 
+  core/time_test.cc
   sync/channel_test.cc
+  container/bounded_queue_test.cc
 
   encoding/base64_test.cc
   encoding/hex_test.cc
@@ -25,9 +25,11 @@ list(APPEND TESTS_SRC
   bytes/buffer_test.cc
   bytes/view_test.cc
 
+  crypto/errors_test.cc
   crypto/aes_test.cc
   crypto/rsa_test.cc
   crypto/digest_test.cc
+  crypto/cert_test.cc
 
   restapi/client_test.cc
   restapi/client_session_test.cc
@@ -44,6 +46,7 @@ list(APPEND TESTS_SRC
   grpc/server_test.cc
   grpc/json/encoder_test.cc
   grpc/identity_api_test.cc
+  grpc/event_api_test.cc
 )
 
 if (WIN32)
@@ -64,7 +67,7 @@ target_proto_generate(
 
 target_link_libraries(tests
   vereignlib
-  Threads::Threads
+  testutil
 )
 
 if (VEREIGN_ENABLE_BENCHMARKING)
diff --git a/cpp/tests/vereign/container/bounded_queue_test.cc b/cpp/tests/vereign/container/bounded_queue_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..c4aff6ddeb20233180c8e05bdb26b45b94e692b8
--- /dev/null
+++ b/cpp/tests/vereign/container/bounded_queue_test.cc
@@ -0,0 +1,81 @@
+#include <vereign/container/bounded_queue.hh>
+
+#include <catch2/catch.hpp>
+
+using namespace vereign;
+
+TEST_CASE("container::BoundedQueue", "[vereign/container]") {
+  // a test type, that can confirm that the queue properly calls its destructor and there
+  // are no memory leaks
+  class Value {
+  public:
+    Value(int v) : v_{std::make_unique<int>(v)} {}
+
+    auto Get() -> int {
+      return *v_;
+    }
+  private:
+    std::unique_ptr<int> v_;
+  };
+
+  auto q = container::BoundedQueue<Value>{3};
+
+  CHECK(q.Size() == 0);
+  CHECK(q.IsEmpty() == true);
+  CHECK(q.IsFull() == false);
+
+  q.PushBack(1);
+  CHECK(q.Size() == 1);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+
+  q.PushBack(2);
+  CHECK(q.Size() == 2);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+
+  q.PushBack(3);
+  CHECK(q.Size() == 3);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == true);
+
+  auto v = std::move(q.Front());
+  q.PopFront();
+  CHECK(v.Get() == 1);
+  CHECK(q.Size() == 2);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+
+  v = std::move(q.Front());
+  q.PopFront();
+  CHECK(v.Get() == 2);
+  CHECK(q.Size() == 1);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+
+  q.PushBack(4);
+  CHECK(q.Size() == 2);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+
+  v = std::move(q.Front());
+  q.PopFront();
+  CHECK(v.Get() == 3);
+  CHECK(q.Size() == 1);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+
+  v = std::move(q.Front());
+  q.PopFront();
+  CHECK(v.Get() == 4);
+  CHECK(q.Size() == 0);
+  CHECK(q.IsEmpty() == true);
+  CHECK(q.IsFull() == false);
+
+  // leave something in the queue, for memory sanitizer check of the queue destruction
+  q.PushBack(5);
+  CHECK(q.Size() == 1);
+  CHECK(q.IsEmpty() == false);
+  CHECK(q.IsFull() == false);
+}
+
diff --git a/cpp/tests/vereign/core/time_test.cc b/cpp/tests/vereign/core/time_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..09637b9fb892cc8a86a27281587d003326c15642
--- /dev/null
+++ b/cpp/tests/vereign/core/time_test.cc
@@ -0,0 +1,42 @@
+#include <vereign/core/time.hh>
+
+#include <catch2/catch.hpp>
+
+using namespace vereign;
+
+TEST_CASE("time::MakePosixTime", "[vereign/time][vereign/core]") {
+  auto ptime = time::MakePosixTime(2020, 1, 1, 0, 1, 1);
+
+  CHECK(ptime.date().year() == 2020);
+  CHECK(ptime.date().month() == boost::gregorian::Jan);
+  CHECK(ptime.date().day() == 1);
+  CHECK(ptime.time_of_day().hours() == 0);
+  CHECK(ptime.time_of_day().minutes() == 1);
+  CHECK(ptime.time_of_day().seconds() == 1);
+
+  CHECK(time::PosixTimeToTime(ptime) == 1577836861);
+  CHECK(time::MakeTimeUTCFromISO("20200101T000101") == time::PosixTimeToTime(ptime));
+
+  ptime = time::MakePosixTime(2020, 1, 1, 24, 60, 60);
+  CHECK(boost::posix_time::to_iso_string(ptime) == "20200102T010100");
+}
+
+TEST_CASE("time::MakeTimeUTC", "[vereign/time][vereign/core]") {
+  auto timestamp = time::MakeTimeUTC(2020, 1, 1, 0, 1, 1);
+
+  CHECK(timestamp == 1577836861);
+  CHECK(time::MakeTimeUTCFromISO("20200101T000101") == timestamp);
+}
+
+TEST_CASE("time::MakeTimeUTCFromString", "[vereign/time][vereign/core]") {
+  auto timestamp = time::MakeTimeUTCFromString("2020-1-1 0:1:1");
+
+  CHECK(timestamp == 1577836861);
+  CHECK(time::MakeTimeUTCFromISO("20200101T000101") == timestamp);
+}
+
+TEST_CASE("time::MakeTimeUTCFromISO", "[vereign/time][vereign/core]") {
+  auto timestamp = time::MakeTimeUTCFromISO("20200101T000101");
+
+  CHECK(timestamp == 1577836861);
+}
diff --git a/cpp/tests/vereign/crypto/aes_test.cc b/cpp/tests/vereign/crypto/aes_test.cc
index cfbb694c4e2eaddfd922670ad528a6984ebb0850..b9f4c41bd0334cd29d4a75e839572bf1f4ea8ba5 100644
--- a/cpp/tests/vereign/crypto/aes_test.cc
+++ b/cpp/tests/vereign/crypto/aes_test.cc
@@ -8,7 +8,7 @@
 
 using namespace vereign;
 
-TEST_CASE("aes::GCM256Encrypt", "[vereign/crypto/aes][vereign/crypto]") {
+TEST_CASE("crypto::aes::GCM256Encrypt", "[vereign/crypto/aes][vereign/crypto]") {
   const std::string data{"foo bar"};
   auto key = crypto::Rand(32);
 
diff --git a/cpp/tests/vereign/crypto/cert_test.cc b/cpp/tests/vereign/crypto/cert_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..da6bec076b79072fff0f925e9f20965b3ec84927
--- /dev/null
+++ b/cpp/tests/vereign/crypto/cert_test.cc
@@ -0,0 +1,84 @@
+#include <vereign/crypto/cert.hh>
+
+#include <vereign/core/time.hh>
+#include <vereign/crypto/rsa.hh>
+#include <vereign/crypto/rand.hh>
+#include <vereign/crypto/bio.hh>
+#include <vereign/bytes/view_dump.hh>
+#include <vereign/fs/path.hh>
+#include <vereign/fs/util.hh>
+#include <testutil/golden.hh>
+
+#include <catch2/catch.hpp>
+
+#include <iostream>
+#include <fstream>
+
+using namespace vereign;
+
+namespace {
+   const std::string testDataPath = fs::path::Join("..", "tests", "vereign", "crypto", "test_data");
+}
+
+TEST_CASE("crypto::cert::CreateSelfSignedCert", "[vereign/crypto/cert][vereign/crypto]") {
+  auto key = crypto::rsa::ImportPrivateKeyFromPEM(
+    fs::ReadFile(fs::path::Join(testDataPath, "ca_key.pem")).View()
+  );
+
+  crypto::cert::CertData cert_data{};
+  cert_data.Email = "ca@example.com";
+  cert_data.Subject.CommonName = "ca-cn.example.com";
+  cert_data.Subject.Country = "BG";
+  cert_data.Subject.State = "Sofia";
+  cert_data.Subject.Locality = "Sofia";
+  cert_data.Subject.Organization = "Vereign AG";
+  cert_data.Subject.OrganizationUnit = "CA";
+  cert_data.Url = "ca.example.com";
+  cert_data.Validity.ValidYears = 5;
+  cert_data.SerialNumber = 42;
+  cert_data.Validity.NotBefore = vereign::time::MakeTimeUTCFromString("2020-07-01 00:00:00");
+  cert_data.IsCA = true;
+
+  auto cert = crypto::cert::CreateSelfSignedCert(cert_data, key.get());
+
+  std::stringstream ss;
+  crypto::cert::PrintCert(ss, cert.get());
+  testutil::golden::Assert(fs::path::Join(testDataPath, "ca_cert.txt"), bytes::View(ss.str()));
+
+  auto pem = crypto::cert::ExportCertToPEM(cert.get());
+  testutil::golden::Assert(fs::path::Join(testDataPath, "ca.crt"), crypto::bio::View(pem.get()));
+}
+
+TEST_CASE("crypto::cert::CreateCert", "[vereign/crypto/cert][vereign/crypto]") {
+  auto ca_key = crypto::rsa::ImportPrivateKeyFromPEM(
+    fs::ReadFile(fs::path::Join(testDataPath, "ca_key.pem")).View()
+  );
+
+  auto ca_cert = crypto::cert::ImportCertFromPEM(
+    fs::ReadFile(fs::path::Join(testDataPath, "ca.crt")).View()
+  );
+
+  auto key = crypto::rsa::ImportPrivateKeyFromPEM(
+    fs::ReadFile(fs::path::Join(testDataPath, "leaf_key.pem")).View()
+  );
+
+  crypto::cert::CertData cert_data{};
+  cert_data.Email = "test@example.com";
+  cert_data.Subject.CommonName = "example.com";
+  cert_data.Subject.Country = "BG";
+  cert_data.Subject.State = "Sofia";
+  cert_data.Subject.Locality = "Sofia";
+  cert_data.Subject.Organization = "Vereign AG";
+  cert_data.Subject.OrganizationUnit = "Engineering";
+  cert_data.Url = "api.example.com";
+  cert_data.Validity.ValidYears = 5;
+  cert_data.SerialNumber = 73;
+  cert_data.Validity.NotBefore = vereign::time::MakeTimeUTCFromString("2020-07-02 00:00:00");
+
+  auto cert = crypto::cert::CreateCert(cert_data, ca_cert.get(), ca_key.get(), key.get());
+
+  std::stringstream ss;
+  crypto::cert::PrintCert(ss, cert.get());
+
+  testutil::golden::Assert(fs::path::Join(testDataPath, "leaf_cert.txt"), bytes::View(ss.str()));
+}
diff --git a/cpp/tests/vereign/crypto/digest_test.cc b/cpp/tests/vereign/crypto/digest_test.cc
index 0bf6018f3bcc7c1120dc699e8d80024f2c4f3cbb..cea92371b19c2290900f0047a587a54522ad537b 100644
--- a/cpp/tests/vereign/crypto/digest_test.cc
+++ b/cpp/tests/vereign/crypto/digest_test.cc
@@ -9,7 +9,7 @@
 
 using namespace vereign;
 
-TEST_CASE("digest::sha1", "[vereign/crypto/digest][vereign/crypto]") {
+TEST_CASE("crypto::digest::sha1", "[vereign/crypto/digest][vereign/crypto]") {
   const std::string data{"foo bar"};
 
   bytes::Buffer hash;
diff --git a/cpp/tests/vereign/crypto/errors_test.cc b/cpp/tests/vereign/crypto/errors_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..1b9f32a3523e27b14a9985047ccb0caca2f1dd46
--- /dev/null
+++ b/cpp/tests/vereign/crypto/errors_test.cc
@@ -0,0 +1,24 @@
+#include <vereign/crypto/errors.hh>
+#include <openssl/evp.h>
+
+#include <catch2/catch.hpp>
+
+using namespace vereign;
+
+TEST_CASE("crypto::OpenSSLError", "[vereign/crypto]") {
+  SECTION("it must return error with message that included the last OpenSSL error") {
+    EVP_PKEY_CTX* ctx = nullptr;
+    auto r = EVP_PKEY_encrypt_init(ctx);
+    CHECK(r == 0);
+
+    auto err = crypto::OpenSSLError{"pkey encrypt failed"};
+    CHECK(std::string(err.what()) == "pkey encrypt failed: OPERATION_NOT_SUPPORTED_FOR_THIS_KEYTYPE");
+  }
+
+  SECTION("when there was no OpenSSL error,"
+          " it must create an error with `no openssl error in the error queue`") {
+    auto err = crypto::OpenSSLError{"test error"};
+
+    CHECK(std::string(err.what()) == "test error: no openssl error in the error queue");
+  }
+}
diff --git a/cpp/tests/vereign/crypto/test_data/.gitattributes b/cpp/tests/vereign/crypto/test_data/.gitattributes
new file mode 100644
index 0000000000000000000000000000000000000000..fa1385d99a319b43c06f5309d1aae9fdd3adea46
--- /dev/null
+++ b/cpp/tests/vereign/crypto/test_data/.gitattributes
@@ -0,0 +1 @@
+* -text
diff --git a/cpp/tests/vereign/crypto/test_data/ca.crt b/cpp/tests/vereign/crypto/test_data/ca.crt
new file mode 100644
index 0000000000000000000000000000000000000000..affc0044449d58fa031c69a13afa931baf587d3e
--- /dev/null
+++ b/cpp/tests/vereign/crypto/test_data/ca.crt
@@ -0,0 +1,28 @@
+-----BEGIN CERTIFICATE-----
+MIIEvDCCA6SgAwIBAgIBKjANBgkqhkiG9w0BAQsFADCBijEaMBgGA1UEAwwRY2Et
+Y24uZXhhbXBsZS5jb20xCzAJBgNVBAYTAkJHMQ4wDAYDVQQHDAVTb2ZpYTEOMAwG
+A1UECAwFU29maWExEzARBgNVBAoMClZlcmVpZ24gQUcxCzAJBgNVBAsMAkNBMR0w
+GwYJKoZIhvcNAQkBFg5jYUBleGFtcGxlLmNvbTAeFw0yMDA3MDEwMDAwMDBaFw0y
+NTA2MzAwMDAwMDBaMIGKMRowGAYDVQQDDBFjYS1jbi5leGFtcGxlLmNvbTELMAkG
+A1UEBhMCQkcxDjAMBgNVBAcMBVNvZmlhMQ4wDAYDVQQIDAVTb2ZpYTETMBEGA1UE
+CgwKVmVyZWlnbiBBRzELMAkGA1UECwwCQ0ExHTAbBgkqhkiG9w0BCQEWDmNhQGV4
+YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxINphs0i
+1HrDcvsG1mUnNrcnwhyitN5ZX8FDgtk+r5RXNk+zRvmlDz+kKq/UNXgIH5v1LbRi
+Vcmh9njnLqh4uPW2Khkxna3yjnWF3NqV780uOCkUEZp0CC2B2RRXCjmxF3wBtggb
+FjBSoh5nhYUtJZkeDgY+AaFlfBKHWWQpzLJWxdHFW/Ej33MubilPTKOaP2Nh9U9x
+bxiwAHS3ijnhpW2Xh6uwf1XMQVwwsbPtEd81BBU86KIeQzOQrYkUEl81nqOzt9Ct
+67fUJs0yPPTtoEekWAYCdFD0hrVLxz1ilQtYF+shXL/6GyrKz0putQhO2wOqINXV
+rHUv1SgEMwD/TwIDAQABo4IBKTCCASUwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B
+Af8EBAMCAYYwKQYDVR0RBCIwIIEOY2FAZXhhbXBsZS5jb22CDmNhLmV4YW1wbGUu
+Y29tMB0GA1UdDgQWBBR+ivGqlvajtAUhWQthghHP6zZrdjCBtwYDVR0jBIGvMIGs
+gBR+ivGqlvajtAUhWQthghHP6zZrdqGBkKSBjTCBijEaMBgGA1UEAwwRY2EtY24u
+ZXhhbXBsZS5jb20xCzAJBgNVBAYTAkJHMQ4wDAYDVQQHDAVTb2ZpYTEOMAwGA1UE
+CAwFU29maWExEzARBgNVBAoMClZlcmVpZ24gQUcxCzAJBgNVBAsMAkNBMR0wGwYJ
+KoZIhvcNAQkBFg5jYUBleGFtcGxlLmNvbYIBKjANBgkqhkiG9w0BAQsFAAOCAQEA
+wHKy0Gmj5Gs+DglsqzOfmUeqy8EsmaS6ufxV084umpnNmaQrdBSQFH0P8sQJROpH
+1bgibRLQFwQLKvAC6dxCvnH6njW0lCZ/53OkrNyky53eZ6I6DHrT0kjkC9KqShEf
++MhHBniVCX2w8HxFgZgMKGiwL+D8qo5QzHmR+AzXAZGlKKZReKuPUehfO02cVWPH
+uz/dSl2gd7xdAz3lwayVPiNNCu4r0/gwvEiHuwCvjIQPIxsUBlm5BwB0ga0OlMJu
+nGVv3dNkS2KjIEBms4jvXxk9w51OQqrcVUhuj8qHenTtM6ho5QTtDi2z/NbM0oW1
+zAmLUoPW97IgXtp4ghOQ7w==
+-----END CERTIFICATE-----
diff --git a/cpp/tests/vereign/crypto/test_data/ca_cert.txt b/cpp/tests/vereign/crypto/test_data/ca_cert.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9ed51e64c82cf5d0d352d31b632f74c18daa62e8
--- /dev/null
+++ b/cpp/tests/vereign/crypto/test_data/ca_cert.txt
@@ -0,0 +1,63 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 42 (0x2a)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: CN=ca-cn.example.com, C=BG, L=Sofia, ST=Sofia, O=Vereign AG, OU=CA/emailAddress=ca@example.com
+        Validity
+            Not Before: Jul  1 00:00:00 2020 GMT
+            Not After : Jun 30 00:00:00 2025 GMT
+        Subject: CN=ca-cn.example.com, C=BG, L=Sofia, ST=Sofia, O=Vereign AG, OU=CA/emailAddress=ca@example.com
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:c4:83:69:86:cd:22:d4:7a:c3:72:fb:06:d6:65:
+                    27:36:b7:27:c2:1c:a2:b4:de:59:5f:c1:43:82:d9:
+                    3e:af:94:57:36:4f:b3:46:f9:a5:0f:3f:a4:2a:af:
+                    d4:35:78:08:1f:9b:f5:2d:b4:62:55:c9:a1:f6:78:
+                    e7:2e:a8:78:b8:f5:b6:2a:19:31:9d:ad:f2:8e:75:
+                    85:dc:da:95:ef:cd:2e:38:29:14:11:9a:74:08:2d:
+                    81:d9:14:57:0a:39:b1:17:7c:01:b6:08:1b:16:30:
+                    52:a2:1e:67:85:85:2d:25:99:1e:0e:06:3e:01:a1:
+                    65:7c:12:87:59:64:29:cc:b2:56:c5:d1:c5:5b:f1:
+                    23:df:73:2e:6e:29:4f:4c:a3:9a:3f:63:61:f5:4f:
+                    71:6f:18:b0:00:74:b7:8a:39:e1:a5:6d:97:87:ab:
+                    b0:7f:55:cc:41:5c:30:b1:b3:ed:11:df:35:04:15:
+                    3c:e8:a2:1e:43:33:90:ad:89:14:12:5f:35:9e:a3:
+                    b3:b7:d0:ad:eb:b7:d4:26:cd:32:3c:f4:ed:a0:47:
+                    a4:58:06:02:74:50:f4:86:b5:4b:c7:3d:62:95:0b:
+                    58:17:eb:21:5c:bf:fa:1b:2a:ca:cf:4a:6e:b5:08:
+                    4e:db:03:aa:20:d5:d5:ac:75:2f:d5:28:04:33:00:
+                    ff:4f
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Basic Constraints: critical
+                CA:TRUE
+            X509v3 Key Usage: critical
+                Digital Signature, Certificate Sign, CRL Sign
+            X509v3 Subject Alternative Name: 
+                email:ca@example.com, DNS:ca.example.com
+            X509v3 Subject Key Identifier: 
+                7E:8A:F1:AA:96:F6:A3:B4:05:21:59:0B:61:82:11:CF:EB:36:6B:76
+            X509v3 Authority Key Identifier: 
+                keyid:7E:8A:F1:AA:96:F6:A3:B4:05:21:59:0B:61:82:11:CF:EB:36:6B:76
+                DirName:/CN=ca-cn.example.com/C=BG/L=Sofia/ST=Sofia/O=Vereign AG/OU=CA/emailAddress=ca@example.com
+                serial:2A
+
+    Signature Algorithm: sha256WithRSAEncryption
+         c0:72:b2:d0:69:a3:e4:6b:3e:0e:09:6c:ab:33:9f:99:47:aa:
+         cb:c1:2c:99:a4:ba:b9:fc:55:d3:ce:2e:9a:99:cd:99:a4:2b:
+         74:14:90:14:7d:0f:f2:c4:09:44:ea:47:d5:b8:22:6d:12:d0:
+         17:04:0b:2a:f0:02:e9:dc:42:be:71:fa:9e:35:b4:94:26:7f:
+         e7:73:a4:ac:dc:a4:cb:9d:de:67:a2:3a:0c:7a:d3:d2:48:e4:
+         0b:d2:aa:4a:11:1f:f8:c8:47:06:78:95:09:7d:b0:f0:7c:45:
+         81:98:0c:28:68:b0:2f:e0:fc:aa:8e:50:cc:79:91:f8:0c:d7:
+         01:91:a5:28:a6:51:78:ab:8f:51:e8:5f:3b:4d:9c:55:63:c7:
+         bb:3f:dd:4a:5d:a0:77:bc:5d:03:3d:e5:c1:ac:95:3e:23:4d:
+         0a:ee:2b:d3:f8:30:bc:48:87:bb:00:af:8c:84:0f:23:1b:14:
+         06:59:b9:07:00:74:81:ad:0e:94:c2:6e:9c:65:6f:dd:d3:64:
+         4b:62:a3:20:40:66:b3:88:ef:5f:19:3d:c3:9d:4e:42:aa:dc:
+         55:48:6e:8f:ca:87:7a:74:ed:33:a8:68:e5:04:ed:0e:2d:b3:
+         fc:d6:cc:d2:85:b5:cc:09:8b:52:83:d6:f7:b2:20:5e:da:78:
+         82:13:90:ef
diff --git a/cpp/tests/vereign/crypto/test_data/ca_key.pem b/cpp/tests/vereign/crypto/test_data/ca_key.pem
new file mode 100644
index 0000000000000000000000000000000000000000..2c73e6ead4478b7c4d32a2ad3b3031c7eff054e0
--- /dev/null
+++ b/cpp/tests/vereign/crypto/test_data/ca_key.pem
@@ -0,0 +1,29 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDEg2mGzSLUesNy
++wbWZSc2tyfCHKK03llfwUOC2T6vlFc2T7NG+aUPP6Qqr9Q1eAgfm/UttGJVyaH2
+eOcuqHi49bYqGTGdrfKOdYXc2pXvzS44KRQRmnQILYHZFFcKObEXfAG2CBsWMFKi
+HmeFhS0lmR4OBj4BoWV8EodZZCnMslbF0cVb8SPfcy5uKU9Mo5o/Y2H1T3FvGLAA
+dLeKOeGlbZeHq7B/VcxBXDCxs+0R3zUEFTzooh5DM5CtiRQSXzWeo7O30K3rt9Qm
+zTI89O2gR6RYBgJ0UPSGtUvHPWKVC1gX6yFcv/obKsrPSm61CE7bA6og1dWsdS/V
+KAQzAP9PAgMBAAECggEAOBj9hGZF9ZsP9WBUrNpWbeeuGVscVX6Ny+h7UbybiPrT
+RKVO28mDRY2Y2zizXwojY4adYI04bx3uttH/yNF+GOrHtE/Z2pXgAqvo6Uma+Mg9
+U/niCT5qtjTrduE4Eaqzc61KPcElnllwFWiRi8ufPjbuHfnJNKTyuA8ABVQzHIQi
+Z+Rhw/IASS2aMuD1A4XicneZ43Vo83mz7xfiebVW7WRyIIcxlWruHQM+BYCTR+y5
+vGdq+P0e4ZjAdwudxB6MfmF5lO16M8CiH5SAJghmBkbEpwdkZuMX6YQLGnw2+C6m
+NHRRb0WSGX12PEJcxA4mvM4L2+m7ihZTSJg5fYSLAQKBgQDmV77TXc6xAfq1O3es
+AV69a5rNgtJ3I7wnv42ZLpQLVSVQ1eqRzhaMIItbX3hwb9STWagIuuGbTG590k00
+lsHRD0/Z4mapyk43b9Zs90ug1L0ojqrVfmjb5uJDzj4fbHO1QtJ2Dt2llMveGCaE
+j5LYz+AKdBjZdxxU6NDws8W1AQKBgQDaZwQjMAx7usp+prVVe33QchTxeziaFh+L
+q53zimoCsXjTSz040dkdk+yhj0DOT3z4Of41+PkWmbLoZh1ar9yFSQPC3fVvz4Sx
+mY5gCIy1D9a3SSPYy6+V6PiCSkYB3JsGONKARl1oxNswewGcBVSxEYMH0INGccoc
+Aqv0WYokTwKBgE/QtdHd3oIdUnc8HPKgIuj2AVUW8MDRxB/t0y3yIuBuZ5jEFxzg
+0ZVKrZ9CzKQBTCKm3X/w6b37VQoKNjBz0MMVUDEKr1JERKgW+7EUW8NmFAmarf1+
+aO1R6rNeIs6WsN1tQyofapQ2pBHYQniKm3K/TT2yDu5DxLBzX/sX//kBAoGAfNsL
+uHoNiOZ+QO1ZGSdiA7pUUsDY2O3w1s4jnEjy1dtTcae/ti3xa0FXs/zLVaaHkPjG
+ZWCSNeNkLgvCmEeVT/Uv20U8vNDr0QQyKqB4oEIPw/RiM9wt4wuulf2UDBhr36C4
+ZnDTkcOIcyy+/MznX9rdB6iig7Elk+HLIAPYP20CgYAL+/AizILmdI7lBvderLfw
+2oslABhYJCjaFX5XWTvOJpPdZPZSqZUgSvzgBM57f1gtRmn6NS1oM6qagoYlOpF4
+rjIcuz86j8tFTp45TjMz1Sf3afyi+iiFhdjTeDTdyjcWK0sBghyljc4jq5scQsYH
+SUOdYY70Kenpzx6iOaWe/g==
+-----END PRIVATE KEY-----
+
diff --git a/cpp/tests/vereign/crypto/test_data/leaf_cert.txt b/cpp/tests/vereign/crypto/test_data/leaf_cert.txt
new file mode 100644
index 0000000000000000000000000000000000000000..fb718f515d1240b1ea27e79859552b2d87d7c06e
--- /dev/null
+++ b/cpp/tests/vereign/crypto/test_data/leaf_cert.txt
@@ -0,0 +1,65 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 73 (0x49)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: CN=ca-cn.example.com, C=BG, L=Sofia, ST=Sofia, O=Vereign AG, OU=CA/emailAddress=ca@example.com
+        Validity
+            Not Before: Jul  2 00:00:00 2020 GMT
+            Not After : Jul  1 00:00:00 2025 GMT
+        Subject: CN=example.com, C=BG, L=Sofia, ST=Sofia, O=Vereign AG, OU=Engineering/emailAddress=test@example.com
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:be:ab:51:25:32:3c:75:bb:49:25:1b:b9:9f:6e:
+                    af:ff:38:07:f0:2c:49:e7:3b:c8:3a:19:02:f1:aa:
+                    96:8d:9d:bd:37:b0:97:2a:11:05:71:65:94:ee:32:
+                    6b:4b:d7:94:43:a1:8c:bb:ed:fe:94:d6:93:f8:7b:
+                    ca:48:7d:fc:99:00:88:d5:d1:73:55:31:ca:eb:69:
+                    5f:0d:1b:6a:24:39:db:b9:f1:7a:34:c7:1f:8e:a4:
+                    12:ab:1d:e8:29:b6:aa:05:d3:fb:7d:bb:7e:2b:db:
+                    c8:18:15:ba:8e:6d:b4:df:98:3c:34:6c:50:07:e9:
+                    7a:7a:57:99:8b:cf:6d:d7:c3:e1:c5:90:ae:d3:94:
+                    dc:b9:fc:90:20:ec:63:24:e5:8b:42:d3:bb:4d:e6:
+                    e2:f0:b6:48:a9:c0:27:2e:89:bc:45:db:b6:25:be:
+                    7d:55:40:6d:72:e4:5b:49:81:d4:aa:e0:3a:33:93:
+                    f9:4b:b2:82:b4:45:e1:5e:0c:14:b0:6e:78:f9:1a:
+                    98:72:8c:ef:c5:10:ab:7f:9d:e7:9b:68:17:30:21:
+                    f0:95:bf:ab:c6:45:bb:3c:cf:f1:96:1f:8d:22:88:
+                    72:b3:52:ff:20:c0:40:5b:11:0d:62:24:ec:5c:53:
+                    81:fa:ca:ec:a5:00:b8:7c:1e:e2:a8:b7:01:42:5e:
+                    1d:01
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Basic Constraints: critical
+                CA:FALSE
+            X509v3 Key Usage: critical
+                Digital Signature, Non Repudiation, Key Encipherment, Data Encipherment
+            X509v3 Extended Key Usage: 
+                E-mail Protection
+            X509v3 Subject Alternative Name: 
+                email:test@example.com, DNS:api.example.com
+            X509v3 Subject Key Identifier: 
+                A5:BD:8A:35:84:98:B6:6C:13:51:A2:4A:5D:C9:51:C6:61:7E:5D:AE
+            X509v3 Authority Key Identifier: 
+                keyid:7E:8A:F1:AA:96:F6:A3:B4:05:21:59:0B:61:82:11:CF:EB:36:6B:76
+                DirName:/CN=ca-cn.example.com/C=BG/L=Sofia/ST=Sofia/O=Vereign AG/OU=CA/emailAddress=ca@example.com
+                serial:2A
+
+    Signature Algorithm: sha256WithRSAEncryption
+         8d:90:e7:a7:a6:57:ec:b3:db:38:37:87:bc:1e:0b:e2:bd:c4:
+         88:e3:95:f0:35:01:a7:fe:51:92:b1:4c:37:d7:21:cf:80:77:
+         51:98:62:61:2c:99:58:60:89:01:35:3e:56:8c:dd:07:21:ad:
+         a1:e3:e5:02:3c:e5:cf:c8:ee:46:11:07:16:f5:37:8c:75:22:
+         e0:af:85:e2:85:31:21:a2:f3:e4:b5:1b:de:ef:31:86:88:84:
+         18:11:bc:67:5a:a7:0b:d1:16:fc:5c:12:51:8d:1c:26:d5:45:
+         f8:55:28:7f:85:a2:c7:a7:e4:bc:6b:98:b4:c4:d9:3e:f6:72:
+         a8:f9:24:f5:9b:38:bf:aa:76:9c:68:c8:67:4f:fa:79:9c:7c:
+         52:f2:45:6c:05:26:4f:64:4c:ea:52:d0:c2:75:45:21:e5:8e:
+         d7:60:0a:b2:6c:d2:f8:1c:01:9b:b1:72:1d:4e:95:79:18:34:
+         b6:1c:bc:ea:5d:12:1f:33:90:63:dc:01:b9:9b:33:dd:62:31:
+         69:c8:b2:1d:ce:3d:38:57:60:c5:75:7d:84:95:3d:db:36:0a:
+         a3:77:7c:67:b6:e3:7a:c0:b4:a5:51:05:78:6b:29:28:43:b6:
+         74:d8:74:18:8e:4f:44:62:55:49:bb:f9:f3:1b:37:63:49:ff:
+         c9:2e:73:a2
diff --git a/cpp/tests/vereign/crypto/test_data/leaf_key.pem b/cpp/tests/vereign/crypto/test_data/leaf_key.pem
new file mode 100644
index 0000000000000000000000000000000000000000..5d83ade2c640588ac9df74b38aa7dd62dc23600c
--- /dev/null
+++ b/cpp/tests/vereign/crypto/test_data/leaf_key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC+q1ElMjx1u0kl
+G7mfbq//OAfwLEnnO8g6GQLxqpaNnb03sJcqEQVxZZTuMmtL15RDoYy77f6U1pP4
+e8pIffyZAIjV0XNVMcrraV8NG2okOdu58Xo0xx+OpBKrHegptqoF0/t9u34r28gY
+FbqObbTfmDw0bFAH6Xp6V5mLz23Xw+HFkK7TlNy5/JAg7GMk5YtC07tN5uLwtkip
+wCcuibxF27Ylvn1VQG1y5FtJgdSq4Dozk/lLsoK0ReFeDBSwbnj5GphyjO/FEKt/
+neebaBcwIfCVv6vGRbs8z/GWH40iiHKzUv8gwEBbEQ1iJOxcU4H6yuylALh8HuKo
+twFCXh0BAgMBAAECggEAAuEIMLktYl4MLh2DpBrng5bMwI1mpfFy5fUD5J1zDbCJ
++iSLj8oh1yhrGIgAiJdjfibwaVO1Z6Qa0rfJzOZBcWz+4/L4RxshSYSLN8yIFy5x
+eSwNMJf+IROsqCf43g0LobAQU4HIggXc+Qd2TEQZ+lNXO2Jpo0jyUr/DtNxSYrLk
+PQOiZUeTRApEFMH/KR7E3CINnFWEsyBktr4UGNT+KbuS+h24QFgeMZS03ELCER/E
+GtYgjYE8Njb+LLC82ZB0jVobo7sXqEu4xhunsysJMID7XF7cRbVH4jS4owBik+MM
+0C4x3IWcUlPTRwCwz7ThdNmuaWysdcm6YKmDTjkg7QKBgQDgObGyYADOBVC986xD
+rHQ6HsHjJoS7UA98OKtpwUfoHgmzCn787bL+28payYLjBQg8Lu0V4wz6IzryCjEY
+DBDWT376lsIElYQBVO80JR+sQHveRKHj3nKxin8RFv+P1iR5l06MLFuI7lpgnfjf
+zAJAMtob6hXTfhKd6ttw3LMJtQKBgQDZsEsv3iWEtJOlEk32b+cBtC9m3+3BRrsl
+vhMqJRRiz3q8ESw4UeYnjHTMJPKixsNCTwJ+swLz1HzPjAPOn8J5g+ehx9szx04g
++SOjO0hYxiuMyEY1RcgQDz8W2eDIY79vQHK6eIdsDX9aDM8TKl0SVkX8yeZVH/l4
+ZrS0GcIlnQKBgHwO78YlX8ydHEM8or7+l3/grJ2lHiMU9LxiDlUA32wJ0owA7qm6
+SiLAMIFypLw+eIPz4rENYoyzGVX5VZhcdKReb37uEoK+xr6F46MSNyfytZYQmxGc
+bhDU8+DcvKE0dhHgKVFc6XgliFOUk7IfCc/DUvHG3jSZuGsvGiKjuVtRAoGAL5DF
+3P+9UvHiEP+e6WoqH8UvVls3qDO8UNLnfR39Blovh0h5URUUJTAblT+9hLPJloON
+GAIlcwHLA86kRlHCPKsFwePZkcbK24mUr+YUONGUUn6wWJOW02uBbYzOAPtKqRR7
+/SK49HM+kH6cAKQREiCnykSTf6Ys7JIEihXWxkECgYEAo/PjT9L+QuYn9muv4p50
+UMJLGQ6kE+63n2dvcEUhXRtEx/i4KzgTkdJ0gBOiaBhJYjzI4BZt/DgtSrzqNC1Y
+UUk5i9sJJF5Ypw0n3dX1mYEFlsJ1xFKDU+lwMakdY+X6b+VECKGIQZsh5DOrFRrv
+QzRzF4x09HztPUADv4gCL40=
+-----END PRIVATE KEY-----
diff --git a/cpp/tests/vereign/grpc/event_api_test.cc b/cpp/tests/vereign/grpc/event_api_test.cc
new file mode 100644
index 0000000000000000000000000000000000000000..a1220024fe11cb51c2af899fc2779420e178f928
--- /dev/null
+++ b/cpp/tests/vereign/grpc/event_api_test.cc
@@ -0,0 +1,96 @@
+#include <vereign/grpc/server.hh>
+
+#include <vereign/kvstore/sqlite_storage.hh>
+#include <vereign/grpc/error_code.hh>
+#include <vereign/core/scope_guard.hh>
+#include <vereign/client_library/types.gen.pb.h>
+#include <vereign/client_library/passport_api.gen.grpc.pb.h>
+#include <vereign/client_library/identity_api.gen.grpc.pb.h>
+#include <vereign/client_library/event_api.gen.grpc.pb.h>
+#include <vereign/service/identity_service.hh>
+#include <vereign/fs/util.hh>
+#include <vereign/fs/path.hh>
+#include <vereign/test/device.hh>
+#include <vereign/test/service_context.hh>
+
+#ifdef _WIN32
+# include <vereign/ncrypt/rsa.hh>
+#endif
+
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
+#include <grpcpp/create_channel.h>
+
+#include <catch2/catch.hpp>
+
+TEST_CASE("grpc::EventAPI::GetNewEvents", "[vereign/grpc][.integration]") {
+  SECTION("get events while not logged in") {
+    auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+    const std::string pin = "foo";
+    auto storage_path = vereign::fs::TempDir("test_db_");
+    auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
+
+    vereign::grpc::Server server{"localhost:", host, port, storage_path};
+    auto on_exit = vereign::core::MakeScopeGuard([&server] {
+      server.Shutdown();
+    });
+
+    auto channel = ::grpc::CreateChannel(
+      "localhost:" + std::to_string(server.SelectedPort()),
+      ::grpc::InsecureChannelCredentials()
+    );
+
+    // start listening for events, and exit when the device is confirmed
+    std::thread t1([&channel]() {
+      auto event_client = vereign::client_library::EventAPI::NewStub(channel);
+      auto req = vereign::client_library::GetNewEventsForm{};
+      auto ctx = ::grpc::ClientContext{};
+      auto resp_stream = event_client->GetNewEvents(&ctx, req);
+
+      auto resp = vereign::client_library::GetNewEventsFormResponse{};
+      for (;;) {
+        resp.Clear();
+        auto ok = resp_stream->Read(&resp);
+        if (!ok) {
+          break;
+        }
+
+        REQUIRE(resp.data().size() > 0);
+        for (auto& event : resp.data()) {
+          if (event.type() == "DeviceConfirmed") {
+            return;
+          }
+        }
+
+        // std::cout << vereign::testutil::ProtobufToJson(resp) << std::endl;
+      }
+    });
+
+    auto identity_client = vereign::client_library::IdentityAPI::NewStub(channel);
+    auto login_req = vereign::client_library::LoginFormNewDevice{};
+    auto login_resp = vereign::client_library::LoginFormNewDeviceResponse{};
+    login_req.set_pin(pin);
+
+    ::grpc::ClientContext login_ctx;
+    identity_client->LoginWithNewDevice(&login_ctx, login_req, &login_resp);
+
+    CHECK(login_resp.error() == "");
+    CHECK(login_resp.status() == "OK");
+    REQUIRE(login_resp.code() == "200");
+
+    // the old device is used for new device confirmation and authorization
+    auto old_storage_path = vereign::fs::TempFilePath("test_db_");
+    auto rm_old_storage_path = vereign::fs::RemoveFileGuard{old_storage_path};
+    auto old_device_ctx = vereign::test::ServiceContext{host, port, old_storage_path};
+    auto old_device = vereign::test::Device{old_device_ctx};
+    old_device.Login(public_key);
+
+    // confirm the new device using an old device
+    old_device.ConfirmNewDevice(login_resp.data().qrcode(), login_resp.data().actionid());
+
+    t1.join();
+  }
+}
diff --git a/cpp/tests/vereign/grpc/identity_api_test.cc b/cpp/tests/vereign/grpc/identity_api_test.cc
index 516aa71338dbf10660e61d48b0451f55cb647fc6..2593b8f0781a19ee0886945e5ec15d8f1e23aca5 100644
--- a/cpp/tests/vereign/grpc/identity_api_test.cc
+++ b/cpp/tests/vereign/grpc/identity_api_test.cc
@@ -15,8 +15,8 @@
 # include <vereign/ncrypt/rsa.hh>
 #endif
 
-#include <util/env.hh>
-#include <util/protobuf.hh>
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
 #include <grpcpp/create_channel.h>
 
 #include <catch2/catch.hpp>
@@ -24,8 +24,8 @@
 TEST_CASE("grpc::IdentityAPI::LoginWithPreviouslyAddedDevice", "[vereign/grpc][.integration]") {
 #ifdef _WIN32
   SECTION("when the device is not registered yet, it must fail with DeviceNotRegistered error") {
-    auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-    auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
     auto storage_path = vereign::fs::TempDir("test_db_");
     auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
@@ -64,9 +64,9 @@ TEST_CASE("grpc::IdentityAPI::LoginWithPreviouslyAddedDevice", "[vereign/grpc][.
   }
 
   SECTION("when the device key has been changed, it must fail with InvalidIdentity error") {
-    auto public_key = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-    auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-    auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+    auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
     auto storage_path = vereign::fs::TempDir("test_db_");
     auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
@@ -113,8 +113,8 @@ TEST_CASE("grpc::IdentityAPI::LoginWithPreviouslyAddedDevice", "[vereign/grpc][.
   }
 #else
   SECTION("when the device is not registered yet, it must fail with DeviceNotRegistered error") {
-    auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-    auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
     auto storage_path = vereign::fs::TempDir("test_db_");
     auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
@@ -145,9 +145,9 @@ TEST_CASE("grpc::IdentityAPI::LoginWithPreviouslyAddedDevice", "[vereign/grpc][.
   }
 
   SECTION("when the pin is wrong, it must fail with InvalidPinCode error") {
-    auto public_key = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-    auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-    auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+    auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
     const std::string pin = "foo";
     auto storage_path = vereign::fs::TempDir("test_db_");
@@ -180,9 +180,9 @@ TEST_CASE("grpc::IdentityAPI::LoginWithPreviouslyAddedDevice", "[vereign/grpc][.
   }
 
   SECTION("when the pin is empty, it must fail with InvalidPinCode error") {
-    auto public_key = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-    auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-    auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+    auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
     const std::string pin = "foo";
     auto storage_path = vereign::fs::TempDir("test_db_");
@@ -217,9 +217,9 @@ TEST_CASE("grpc::IdentityAPI::LoginWithPreviouslyAddedDevice", "[vereign/grpc][.
 #endif
 
   SECTION("when the device is not authorized, it must fail with DeviceNotRegistered error") {
-    auto public_key = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-    auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-    auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+    auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
     // the old device is used later for new device confirmation and authorization
     auto old_storage_path = vereign::fs::TempFilePath("test_db_");
@@ -303,8 +303,8 @@ TEST_CASE("grpc::IdentityAPI::LoginWithNewDevice", "[vereign/grpc][.integration]
 #ifdef _WIN32
 #else
   SECTION("when the pin is empty, it must fail with InvalidPinCode error") {
-    auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-    auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+    auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+    auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
     auto storage_path = vereign::fs::TempDir("test_db_");
     auto rm_storage_path = vereign::fs::RemoveAllGuard(storage_path);
diff --git a/cpp/tests/vereign/grpc/server_test.cc b/cpp/tests/vereign/grpc/server_test.cc
index 0f3a18589adfb98ebe6d39bb064c91257439b7f5..de3c47db053a0494fbaeb059d1f3535fc0bdcaee 100644
--- a/cpp/tests/vereign/grpc/server_test.cc
+++ b/cpp/tests/vereign/grpc/server_test.cc
@@ -10,16 +10,16 @@
 #include <vereign/test/device.hh>
 #include <vereign/test/service_context.hh>
 
-#include <util/env.hh>
-#include <util/protobuf.hh>
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
 #include <grpcpp/create_channel.h>
 
 #include <catch2/catch.hpp>
 
 TEST_CASE("grpc::Server", "[vereign/grpc/server][.integration]") {
-  auto public_key = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-  auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-  auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+  auto public_key = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
   const std::string pin = "foo";
   auto storage_path = vereign::fs::TempDir("test_db_");
@@ -84,6 +84,6 @@ TEST_CASE("grpc::Server", "[vereign/grpc/server][.integration]") {
 
   // std::cout << vereign::test::ProtobufToJson(getDIDsResp) << std::endl;
 
-  grpc_shutdown();
-  google::protobuf::ShutdownProtobufLibrary();
+  // grpc_shutdown();
+  // google::protobuf::ShutdownProtobufLibrary();
 }
diff --git a/cpp/tests/vereign/identity/provider_test.cc b/cpp/tests/vereign/identity/provider_test.cc
index f34a8450b557e96bbf996aca0e2e43bf6d1481a4..c4de48c2568c74315d2894410f4ff301b939d207 100644
--- a/cpp/tests/vereign/identity/provider_test.cc
+++ b/cpp/tests/vereign/identity/provider_test.cc
@@ -5,15 +5,24 @@
 #include <vereign/kvstore/crypto_storage.hh>
 #include <vereign/crypto/rand.hh>
 #include <vereign/bytes/view_dump.hh>
+#include <vereign/test/device.hh>
+#include <vereign/test/service_context.hh>
+#include <vereign/service/gen/passport_service.hh>
+#include <vereign/crypto/cert.hh>
+#include <vereign/crypto/rsa.hh>
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
 
 #include <catch2/catch.hpp>
 #include <iostream>
 
 using namespace vereign;
 
-TEST_CASE("Provider::RecreateIdentity", "[vereign/identity]") {
+TEST_CASE("identity::Provider::RecreateIdentity", "[vereign/identity]") {
   auto storage_path = fs::TempFilePath("test_db_");
-  fs::RemoveFileGuard rm{storage_path};
+  auto rm_storage_path = fs::RemoveFileGuard{storage_path};
+
+  auto device_ctx = test::ServiceContext{"", "", storage_path};
 
   std::string expected;
   std::string actual;
@@ -22,7 +31,7 @@ TEST_CASE("Provider::RecreateIdentity", "[vereign/identity]") {
     kvstore::SqliteStorage kvstorage{storage_path};
     kvstore::CryptoStorage storage{kvstorage, true};
 
-    identity::Provider provider{storage};
+    identity::Provider provider{device_ctx.ClientSession(), storage};
     expected = provider.RecreateIdentity("foo");
   }
 
@@ -30,7 +39,7 @@ TEST_CASE("Provider::RecreateIdentity", "[vereign/identity]") {
     kvstore::SqliteStorage kvstorage{storage_path};
     kvstore::CryptoStorage storage{kvstorage, true};
 
-    identity::Provider provider{storage};
+    identity::Provider provider{device_ctx.ClientSession(), storage};
     actual = provider.LoadIdentity("foo");
   }
 
@@ -38,3 +47,169 @@ TEST_CASE("Provider::RecreateIdentity", "[vereign/identity]") {
   CHECK(actual.size() > 0);
   CHECK(expected == actual);
 }
+
+TEST_CASE("identity::Provider::GetProfileCertificate", "[vereign/identity][.integration]") {
+  auto public_key = testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+  // prepare new device
+  auto old_storage_path = fs::TempFilePath("test_db_");
+  auto rm_old_storage_path = fs::RemoveFileGuard{old_storage_path};
+  auto old_device_ctx = test::ServiceContext{host, port, old_storage_path};
+  auto old_device = test::Device{old_device_ctx};
+  old_device.Login(public_key);
+
+  auto storage_path = fs::TempFilePath("test_db_");
+  auto rm_storage_path = fs::RemoveFileGuard{storage_path};
+  auto service_context = test::ServiceContext{host, port, storage_path};
+  old_device.CreateNewDevice(service_context, "pin");
+
+  // retrieve a profile uuid
+  auto list_req = std::make_unique<vereign::client_library::ListPassportsForm>();
+  auto list_resp = std::make_unique<vereign::client_library::ListPassportsFormResponse>();
+
+  auto passport_service = service::gen::PassportService{service_context.ClientSession()};
+  auto list_result = passport_service.ListPassports(list_req.get(), list_resp.get());
+
+  auto result = list_result.get();
+  auto& list = result.Response;
+  REQUIRE(list->code() == "200");
+  REQUIRE(list->data().size() > 0);
+
+  auto profileUUID = list->data().at(0).uuid();
+
+  auto& identity_provider = service_context.IdentityProvider();
+
+  auto cert = identity_provider.GetProfileCertificate(profileUUID);
+
+  REQUIRE(cert.get() != nullptr);
+  CHECK(cert->PrivateKeyPEM.Size() > 0);
+  CHECK(cert->CertificatePEM.Size() > 0);
+  CHECK(cert->CertificateUUID.size() > 0);
+  CHECK(cert->Chain.size() == 2);
+
+  SECTION("the certificate private key must be importable") {
+    bssl::UniquePtr<EVP_PKEY> cert_pkey;
+    CHECK_NOTHROW(
+      cert_pkey = crypto::rsa::ImportPrivateKeyFromPEM(cert->PrivateKeyPEM.View())
+    );
+    CHECK(cert_pkey.get() != nullptr);
+  }
+
+  SECTION("the certificate and the certificates from the chain must be importable") {
+    bssl::UniquePtr<X509> x509_cert;
+    CHECK_NOTHROW(
+      x509_cert = crypto::cert::ImportCertFromPEM(cert->CertificatePEM.View())
+    );
+    CHECK(x509_cert.get() != nullptr);
+
+    for (auto& cert_pem : cert->Chain) {
+      x509_cert.reset();
+
+      CHECK_NOTHROW(
+        x509_cert = crypto::cert::ImportCertFromPEM(cert_pem.View())
+      );
+      CHECK(x509_cert.get() != nullptr);
+    }
+  }
+
+
+  SECTION("it must reuse already created profile certificates") {
+    auto new_cert = identity_provider.GetProfileCertificate(profileUUID);
+
+    REQUIRE(new_cert.get() != nullptr);
+    CHECK(new_cert->PrivateKeyPEM.View() == cert->PrivateKeyPEM.View());
+    CHECK(new_cert->CertificatePEM.View() == cert->CertificatePEM.View());
+    CHECK(new_cert->CertificateUUID == cert->CertificateUUID);
+    REQUIRE(new_cert->Chain.size() == cert->Chain.size());
+    for (std::size_t i = 0; i < new_cert->Chain.size(); i++) {
+      CHECK(new_cert->Chain[i].View() == cert->Chain[i].View());
+    }
+  }
+}
+
+TEST_CASE("identity::Provider::GetProfileOneTimeCertificate", "[vereign/identity][.integration]") {
+  auto public_key = testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
+
+  // prepare new device
+  auto old_storage_path = fs::TempFilePath("test_db_");
+  auto rm_old_storage_path = fs::RemoveFileGuard{old_storage_path};
+  auto old_device_ctx = test::ServiceContext{host, port, old_storage_path};
+  auto old_device = test::Device{old_device_ctx};
+  old_device.Login(public_key);
+
+  auto storage_path = fs::TempFilePath("test_db_");
+  auto rm_storage_path = fs::RemoveFileGuard{storage_path};
+  auto service_context = test::ServiceContext{host, port, storage_path};
+  old_device.CreateNewDevice(service_context, "pin");
+
+  // retrieve a profile uuid
+  auto list_req = std::make_unique<vereign::client_library::ListPassportsForm>();
+  auto list_resp = std::make_unique<vereign::client_library::ListPassportsFormResponse>();
+
+  auto passport_service = service::gen::PassportService{service_context.ClientSession()};
+  auto list_result = passport_service.ListPassports(list_req.get(), list_resp.get());
+
+  auto result = list_result.get();
+  auto& list = result.Response;
+  REQUIRE(list->code() == "200");
+  REQUIRE(list->data().size() > 0);
+
+  auto profileUUID = list->data().at(0).uuid();
+
+  auto& identity_provider = service_context.IdentityProvider();
+
+  auto cert = identity_provider.GetProfileOneTimeCertificate(profileUUID, "foo@example.com");
+
+  REQUIRE(cert.get() != nullptr);
+  CHECK(cert->PrivateKeyPEM.Size() > 0);
+  CHECK(cert->CertificatePEM.Size() > 0);
+  CHECK(cert->CertificateUUID.size() == 0);
+  CHECK(cert->Chain.size() == 3);
+
+  SECTION("the certificate private key must be importable") {
+    bssl::UniquePtr<EVP_PKEY> cert_pkey;
+    CHECK_NOTHROW(
+      cert_pkey = crypto::rsa::ImportPrivateKeyFromPEM(cert->PrivateKeyPEM.View())
+    );
+    CHECK(cert_pkey.get() != nullptr);
+  }
+
+  // fs::WriteFile("onetime", cert->CertificatePEM.View());
+  // fs::WriteFile("userdevice", cert->Chain[0].View());
+  // fs::WriteFile("serverside", cert->Chain[1].View());
+  // fs::WriteFile("vereign", cert->Chain[2].View());
+
+  SECTION("the certificate and the certificates from the chain must be importable") {
+    bssl::UniquePtr<X509> x509_cert;
+    CHECK_NOTHROW(
+      x509_cert = crypto::cert::ImportCertFromPEM(cert->CertificatePEM.View())
+    );
+    CHECK(x509_cert.get() != nullptr);
+
+    for (auto& cert_pem : cert->Chain) {
+      x509_cert.reset();
+
+      CHECK_NOTHROW(
+        x509_cert = crypto::cert::ImportCertFromPEM(cert_pem.View())
+      );
+      CHECK(x509_cert.get() != nullptr);
+    }
+  }
+
+  SECTION("it must create new certificate all the time, but the issuers chain must be the same") {
+    auto new_cert = identity_provider.GetProfileOneTimeCertificate(profileUUID, "bar@example.com");
+
+    REQUIRE(new_cert.get() != nullptr);
+    CHECK(new_cert->PrivateKeyPEM.View() != cert->PrivateKeyPEM.View());
+    CHECK(new_cert->CertificatePEM.View() != cert->CertificatePEM.View());
+    CHECK(new_cert->CertificateUUID.size() == 0);
+    REQUIRE(new_cert->Chain.size() == cert->Chain.size());
+    for (std::size_t i = 0; i < new_cert->Chain.size(); i++) {
+      CHECK(new_cert->Chain[i].View() == cert->Chain[i].View());
+    }
+  }
+}
diff --git a/cpp/tests/vereign/kvstore/crypto_storage_test.cc b/cpp/tests/vereign/kvstore/crypto_storage_test.cc
index 8f9d305d0488004739afe3ae142e982ab7405630..01524db2f8dc171002b49e71addfa33289b20737 100644
--- a/cpp/tests/vereign/kvstore/crypto_storage_test.cc
+++ b/cpp/tests/vereign/kvstore/crypto_storage_test.cc
@@ -70,10 +70,10 @@ TEST_CASE("kvstore::CryptoStorage::GetBytes", "[vereign/kvstore]") {
 
   storage.Recreate("foo");
 
-  SECTION("when the value does not exists, it must throw ValueNotFoundError") {
+  SECTION("when the value does not exists, it must return false") {
     bytes::Buffer v;
 
-    CHECK_THROWS_AS(storage.GetBytes("does_not_exists", v), kvstore::ValueNotFoundError);
+    CHECK(storage.GetBytes("does_not_exists", v) == false);
 
     CHECK(v.Size() == 0);
   }
diff --git a/cpp/tests/vereign/kvstore/lock_test.cc b/cpp/tests/vereign/kvstore/lock_test.cc
index d15ed5ef9da02727c382e713c2401e2083ce7b5d..7dd70ed7943d1ec7400c2e3db7534875d979a6a0 100644
--- a/cpp/tests/vereign/kvstore/lock_test.cc
+++ b/cpp/tests/vereign/kvstore/lock_test.cc
@@ -8,7 +8,7 @@
 #include <vereign/core/scope_guard.hh>
 #include <vereign/crypto/rand.hh>
 #include <vereign/sqlite/errors.hh>
-#include <util/error.hh>
+#include <testutil/error.hh>
 #include <sqlite3.h>
 
 #include <catch2/catch.hpp>
diff --git a/cpp/tests/vereign/kvstore/sqlite_storage_test.cc b/cpp/tests/vereign/kvstore/sqlite_storage_test.cc
index f747ef2e4225b2b4c38b68551ab5cec2fcb538ae..dfe2d3d117038493c6338a3e202b8a1a2e8eec99 100644
--- a/cpp/tests/vereign/kvstore/sqlite_storage_test.cc
+++ b/cpp/tests/vereign/kvstore/sqlite_storage_test.cc
@@ -7,7 +7,7 @@
 #include <vereign/core/scope_guard.hh>
 #include <vereign/crypto/rand.hh>
 #include <vereign/sqlite/errors.hh>
-#include <util/error.hh>
+#include <testutil/error.hh>
 #include <sqlite3.h>
 
 #include <catch2/catch.hpp>
@@ -23,11 +23,12 @@ TEST_CASE("kvstore::SqliteStorage::GetBytes", "[vereign/kvstore]") {
 
   auto kvstorage = kvstore::SqliteStorage(storage_path);
 
-  SECTION("when the value does not exists, it must throw ValueNotFoundError") {
+  SECTION("when the value does not exists, it must return false") {
     bytes::Buffer v;
 
-    CHECK_THROWS_AS(kvstorage.GetBytes("does_not_exists", v), kvstore::ValueNotFoundError);
+    auto found = kvstorage.GetBytes("does_not_exists", v);
 
+    CHECK(found == false);
     CHECK(v.Size() == 0);
   }
 
@@ -35,8 +36,9 @@ TEST_CASE("kvstore::SqliteStorage::GetBytes", "[vereign/kvstore]") {
     kvstorage.PutBytes("foo", bytes::View("bar"));
 
     bytes::Buffer v;
-    kvstorage.GetBytes("foo", v);
+    auto found = kvstorage.GetBytes("foo", v);
 
+    CHECK(found == true);
     CHECK(v.View() == bytes::View("bar"));
   }
 }
@@ -47,15 +49,21 @@ TEST_CASE("kvstore::SqliteStorage::GetInt64", "[vereign/kvstore]") {
 
   auto kvstorage = kvstore::SqliteStorage(storage_path);
 
-  SECTION("when the value does not exists, it must throw ValueNotFoundError") {
-    CHECK_THROWS_AS(kvstorage.GetInt64("does_not_exists"), kvstore::ValueNotFoundError);
+  SECTION("when the value does not exists, it must return false") {
+    int64_t value = 0;
+    auto found = kvstorage.GetInt64("does_not_exists", value);
+
+    CHECK(found == false);
+    CHECK(value == 0);
   }
 
   SECTION("when the value exists, it must retrieve the value") {
     kvstorage.PutInt64("foo", 42);
 
-    auto v = kvstorage.GetInt64("foo");
+    int64_t v = 0;
+    auto result = kvstorage.GetInt64("foo", v);
 
+    CHECK(result == true);
     CHECK(v == 42);
   }
 }
@@ -69,13 +77,16 @@ TEST_CASE("kvstore::SqliteStorage::DeleteAll", "[vereign/kvstore]") {
   kvstorage.PutInt64("foo", 42);
   kvstorage.PutInt64("bar", 422);
 
-  CHECK(kvstorage.GetInt64("foo") == 42);
-  CHECK(kvstorage.GetInt64("bar") == 422);
+  int64_t v = 0;
+  kvstorage.GetInt64("foo", v);
+  CHECK(v == 42);
+  kvstorage.GetInt64("bar", v);
+  CHECK(v == 422);
 
   kvstorage.DeleteAll();
 
-  CHECK_THROWS_AS(kvstorage.GetInt64("foo"), kvstore::ValueNotFoundError);
-  CHECK_THROWS_AS(kvstorage.GetInt64("bar"), kvstore::ValueNotFoundError);
+  CHECK(kvstorage.GetInt64("foo", v) == false);
+  CHECK(kvstorage.GetInt64("bar", v) == false);
 }
 
 TEST_CASE("kvstore::SqliteStorage::Lock", "[vereign/kvstore]") {
@@ -98,8 +109,12 @@ TEST_CASE("kvstore::SqliteStorage::Lock", "[vereign/kvstore]") {
 
       kvstore::Lock l{kvstorage};
 
-      CHECK(kvstorage.GetInt64("foo") == 42);
-      CHECK(kvstorage.GetInt64("bar") == 422);
+      int64_t v = 0;
+      kvstorage.GetInt64("foo", v);
+      CHECK(v == 42);
+
+      kvstorage.GetInt64("bar", v);
+      CHECK(v == 422);
     }
   }
 
@@ -119,8 +134,12 @@ TEST_CASE("kvstore::SqliteStorage::Lock", "[vereign/kvstore]") {
       auto kvstorage = kvstore::SqliteStorage(storage_path);
       kvstorage.Lock();
 
-      CHECK(kvstorage.GetInt64("foo") == 42);
-      CHECK(kvstorage.GetInt64("bar") == 422);
+      int64_t v = 0;
+      kvstorage.GetInt64("foo", v);
+      CHECK(v == 42);
+
+      kvstorage.GetInt64("bar", v);
+      CHECK(v == 422);
     }
   }
 
diff --git a/cpp/tests/vereign/restapi/client_session_test.cc b/cpp/tests/vereign/restapi/client_session_test.cc
index 36a824999a5c725e0d5443d91c91369fac619c51..1730d834665459f72009e2b08e1ce702fa24f7cc 100644
--- a/cpp/tests/vereign/restapi/client_session_test.cc
+++ b/cpp/tests/vereign/restapi/client_session_test.cc
@@ -2,8 +2,8 @@
 #include <vereign/restapi/client_session.hh>
 #include <vereign/core/scope_guard.hh>
 #include <vereign/client_library/types.gen.pb.h>
-#include <util/env.hh>
-#include <util/protobuf.hh>
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
 
 #include <catch2/catch.hpp>
 
@@ -11,22 +11,20 @@
 #include <boost/asio/ssl/context.hpp>
 #include <fmt/core.h>
 
-TEST_CASE("ClientSession::Post", "[vereign/restapi/client_session][.integration]") {
+TEST_CASE("restapi::ClientSession::Post", "[vereign/restapi/client_session][.integration]") {
   namespace asio = boost::asio;
   namespace beast = boost::beast;
   asio::io_context ioc;
   auto work_guard = boost::asio::make_work_guard(ioc);
   asio::ssl::context ctx(asio::ssl::context::tlsv12_client);
 
-  auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-  auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-  auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
-
-  // Verify the remote server's certificate
-  // ctx.set_verify_mode(ssl::verify_peer);
+  auto publicKey = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
   vereign::restapi::Client client{ioc, ctx, host, port};
-  vereign::restapi::ClientSession client_session{client, publicKey};
+  vereign::restapi::ClientSession client_session{client};
+  client_session.SetPubKey(publicKey, "");
 
   std::thread ioc_thread([&ioc]{
     ioc.run();
diff --git a/cpp/tests/vereign/restapi/client_test.cc b/cpp/tests/vereign/restapi/client_test.cc
index c0c1a17198abca5ee1e50f85fe57675308d4d597..27b71a716c6d4a7c22e3e8fbef88f8ae4dd83147 100644
--- a/cpp/tests/vereign/restapi/client_test.cc
+++ b/cpp/tests/vereign/restapi/client_test.cc
@@ -4,7 +4,7 @@
 #include <vereign/client_library/identity_types.pb.h>
 #include <vereign/client_library/passport_api.gen.pb.h>
 #include <vereign/core/scope_guard.hh>
-#include <util/env.hh>
+#include <testutil/env.hh>
 
 #include "boost/asio/executor_work_guard.hpp"
 #include "boost/beast/core/error.hpp"
@@ -25,9 +25,9 @@ TEST_CASE("Client::Post", "[vereign/restapi/client][.integration]") {
   auto work_guard = boost::asio::make_work_guard(ioc);
   asio::ssl::context ctx(asio::ssl::context::tlsv12_client);
 
-  auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-  auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-  auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+  auto publicKey = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
   // Verify the remote server's certificate
   // ctx.set_verify_mode(ssl::verify_peer);
@@ -102,9 +102,9 @@ TEST_CASE("Client load test", "[vereign/restapi/client][.bench]") {
   auto work_guard = boost::asio::make_work_guard(ioc);
   asio::ssl::context ctx(asio::ssl::context::tlsv12_client);
 
-  auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-  auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-  auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+  auto publicKey = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
   host = "localhost";
   port = "9292";
diff --git a/cpp/tests/vereign/service/gen/passport_service_test.cc b/cpp/tests/vereign/service/gen/passport_service_test.cc
index bbe2df7c0aaf5bb31dad33039778e439b6606ce6..2953fe0e466c60bba2797104b7e556b970d21bf4 100644
--- a/cpp/tests/vereign/service/gen/passport_service_test.cc
+++ b/cpp/tests/vereign/service/gen/passport_service_test.cc
@@ -8,8 +8,8 @@
 #include <vereign/service/identity_service.hh>
 #include <vereign/identity/provider.hh>
 
-#include <util/env.hh>
-#include <util/protobuf.hh>
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
 
 #include <catch2/catch.hpp>
 #include <boost/asio/io_context.hpp>
@@ -23,9 +23,9 @@ TEST_CASE("PassportService::ListPassports", "[vereign/service/gen][.integration]
   auto work_guard = boost::asio::make_work_guard(ioc);
   asio::ssl::context ctx(asio::ssl::context::tlsv12_client);
 
-  auto publicKey = vereign::test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-  auto host = vereign::test::RequireEnv("TEST_VEREIGN_API_HOST");
-  auto port = vereign::test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+  auto publicKey = vereign::testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = vereign::testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = vereign::testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
   // Verify the remote server's certificate
   // ctx.set_verify_mode(ssl::verify_peer);
@@ -39,7 +39,7 @@ TEST_CASE("PassportService::ListPassports", "[vereign/service/gen][.integration]
 
   auto kvstorage = vereign::kvstore::SqliteStorage(storage_path);
   vereign::kvstore::CryptoStorage storage{kvstorage};
-  vereign::identity::Provider provider{storage};
+  vereign::identity::Provider provider{client_session, storage};
   vereign::service::IdentityService idenity_service{client_session, provider};
 
   std::thread ioc_thread([&ioc]{
diff --git a/cpp/tests/vereign/service/identity_service_test.cc b/cpp/tests/vereign/service/identity_service_test.cc
index 4fa38954ab5c0ec1af69addc32ffccd0a23c8e5a..4f48b025e33140f7a77bd8897ed393f7476c5ed3 100644
--- a/cpp/tests/vereign/service/identity_service_test.cc
+++ b/cpp/tests/vereign/service/identity_service_test.cc
@@ -10,17 +10,17 @@
 #include <vereign/test/service_context.hh>
 #include <vereign/fs/util.hh>
 
-#include <util/env.hh>
-#include <util/protobuf.hh>
+#include <testutil/env.hh>
+#include <testutil/protobuf.hh>
 
 #include <catch2/catch.hpp>
 
 using namespace vereign;
 
 TEST_CASE("service::IdentityService::LoginWithNewDevice", "[vereign/service][.integration]") {
-  auto public_key = test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-  auto host = test::RequireEnv("TEST_VEREIGN_API_HOST");
-  auto port = test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+  auto public_key = testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
   // the old device is used later for new device confirmation and authorization
   auto old_storage_path = fs::TempFilePath("test_db_");
@@ -75,9 +75,9 @@ TEST_CASE("service::IdentityService::LoginWithNewDevice", "[vereign/service][.in
 }
 
 TEST_CASE("service::IdentityService::LoginWithPreviouslyAddedDevice", "[vereign/service][.integration]") {
-  auto public_key = test::RequireEnv("TEST_VEREIGN_PUB_KEY");
-  auto host = test::RequireEnv("TEST_VEREIGN_API_HOST");
-  auto port = test::GetEnv("TEST_VEREIGN_API_PORT", "https");
+  auto public_key = testutil::RequireEnv("TEST_VEREIGN_PUB_KEY");
+  auto host = testutil::RequireEnv("TEST_VEREIGN_API_HOST");
+  auto port = testutil::GetEnv("TEST_VEREIGN_API_PORT", "https");
 
   auto storage_path = fs::TempFilePath("test_db_");
   auto rm_storage_path = fs::RemoveFileGuard{storage_path};
diff --git a/cpp/tests/vereign/test/service_context.cc b/cpp/tests/vereign/test/service_context.cc
index a6b5fc85a675920a667cd0890ffe0b632cda516a..934ff9084eb440a3c536d4e0f58baaf9c3518b0a 100644
--- a/cpp/tests/vereign/test/service_context.cc
+++ b/cpp/tests/vereign/test/service_context.cc
@@ -23,7 +23,7 @@ ServiceContext::ServiceContext(
     storage_path_{std::move(storage_path)},
     sqlite_storage_{std::make_unique<kvstore::SqliteStorage>(storage_path_)},
     storage_{std::make_unique<kvstore::CryptoStorage>(*sqlite_storage_, true)},
-    identity_provider_{std::make_unique<identity::Provider>(*storage_)}
+    identity_provider_{std::make_unique<identity::Provider>(*client_session_, *storage_)}
 {
   service_thread_ = std::thread([this]() {
     ioc_.run();