Implement waiting 2 minutes for potential UPDATE messages 02/310102/21
authorKrzysztof Malysa <k.malysa@samsung.com>
Mon, 22 Apr 2024 11:57:29 +0000 (13:57 +0200)
committerKrzysztof Malysa <k.malysa@samsung.com>
Wed, 15 May 2024 13:51:18 +0000 (15:51 +0200)
Change-Id: I2077c8033496d6121bac9b92354e55366ab7f52e

45 files changed:
srcs/cbor_encoding.cpp
srcs/common.h
srcs/ctap_message_processor.cpp
srcs/ctap_message_processor.h
srcs/encrypted_tunnel.cpp
srcs/encrypted_tunnel.h
srcs/exception.h
srcs/log/log.h
srcs/message.h
srcs/qr_code_shower.cpp
srcs/qr_code_shower.h
srcs/qr_transaction.cpp
srcs/qr_transaction.h
srcs/request_handler.cpp
srcs/request_handler.h
srcs/state_assisted_transaction.cpp
srcs/state_assisted_transaction.h
srcs/to_wauthn_const_buff.h
srcs/transaction.h
srcs/tunnel.cpp
srcs/tunnel.h
srcs/webauthn_ble.cpp
srcs/websockets.cpp
srcs/websockets.h
tests/CMakeLists.txt
tests/api_tests.cpp [deleted file]
tests/buffer.h [new file with mode: 0644]
tests/buffer_view.h [new file with mode: 0644]
tests/ctap_message_processor_tests.cpp
tests/encrypted_tunnel_tests.cpp
tests/handshake_tests.cpp
tests/man_tests.cpp
tests/message_examples.h [new file with mode: 0644]
tests/message_tests.cpp
tests/qr_code_shower_tests.cpp
tests/qr_transaction_tests.cpp
tests/request_handler_tests.cpp [new file with mode: 0644]
tests/state_assisted_transaction_tests.cpp
tests/test_cancel_from_the_other_thread.h
tests/transaction_test.h
tests/transaction_tester.h
tests/tunnel/auto_tests.cpp
tests/tunnel/auto_tests.h
tests/tunnel/manual_tests.cpp
tests/wauthn_const_buff_equals.h [new file with mode: 0644]

index 17a607b739654f988de4625d79edb4728bf9c807..5ba634f13e1f8c379e398fe4da908e7aca922fe4 100644 (file)
@@ -18,6 +18,7 @@
 #include "crypto/random.h"
 #include "exception.h"
 #include "log/log.h"
+#include "lowercase_hex_string_of.h"
 #include "tunnel_server_domain.h"
 
 #include <chrono>
@@ -125,7 +126,9 @@ void Cbor::EncodeQRContents(const CryptoBuffer &publicKey,
     }
 
     buffer.resize(encoder.GetBufferSize());
+    LogDebug("QR code CBOR buffer before encoding: " << LowercaseHexStringOf(buffer));
     fidoUri = "FIDO:/" + DigitEncode(buffer);
+    LogDebug("QR code contents encoded: " << fidoUri);
 }
 
 void Container::CloseContainer(const CborEncoder &mapEncoder) noexcept
index fb7dfe2dccefa48ef101bb6c5f63963c0039462c..29c1d7861364d77f8f6e00d856e7963900021235 100644 (file)
@@ -40,19 +40,3 @@ private:
 
 typedef std::vector<uint8_t> Buffer;
 typedef std::basic_string_view<uint8_t> BufferView;
-
-inline Buffer ToBuffer(const BufferView &bv) { return Buffer(bv.begin(), bv.end()); }
-
-template <class T>
-BufferView ToBufferView(T &buffer) noexcept
-{
-    static_assert(sizeof(decltype(*buffer.data())) == 1, "for reinterpret_cast below");
-    return BufferView{reinterpret_cast<const uint8_t *>(buffer.data()), buffer.size()};
-}
-
-// Use with string literals only, because it assumes the input array to be null-terminated.
-template <size_t N>
-BufferView BUFFER_VIEW(const char (&data)[N])
-{
-    return BufferView(reinterpret_cast<const uint8_t *>(data), N - 1);
-}
index dceed736bbca2fd50e1120226a2531b0eb02e5cc..f5cde5fc14f5b845ab3224b0d144a51a538e9fc1 100644 (file)
@@ -31,7 +31,7 @@ CtapMessageProcessor::MakeCredential(const wauthn_client_data_s &clientData,
                                      const wauthn_pubkey_cred_creation_options_s &options)
 {
     if (!options.rp)
-        THROW_INVALID_PARAM("options.rp is null");
+        THROW_INVALID_PARAM("options.rp is NULL");
     return DoCommand<MakeCredentialCommand, MCResult>(
         "MakeCredential", clientData, options, options.rp->id);
 }
@@ -95,12 +95,30 @@ Result CtapMessageProcessor::DoCommand(const char *commandName,
     Buffer shutdownMsg;
     shutdown.Serialize(shutdownMsg);
     m_encryptedTunnel->WriteBinary(shutdownMsg);
-
-    // TODO: implement 2 minute waiting feature
-
     return res;
 }
 
+void CtapMessageProcessor::ProcessFollowingUpdateMsgs(
+    std::function<void(UpdateMessage &&)> updateMsgCallback)
+{
+    m_encryptedTunnel->CloseConnectionAfter(120e6);
+    try {
+        for (;;) {
+            auto msg = m_encryptedTunnel->ReadBinary();
+            LogDebug("Decrypted message: " << LowercaseHexStringOf(msg));
+            if (msg.empty() || msg[0] != static_cast<uint8_t>(MessageType::UPDATE))
+                THROW_UNKNOWN("Received message is not an UPDATE message");
+
+            auto msgBv = BufferView{msg.data() + 1, msg.size() - 1};
+            UpdateMessage upMsg;
+            upMsg.Deserialize(msgBv);
+            updateMsgCallback(std::move(upMsg));
+        }
+    } catch (const ExceptionTunnelClosed &) {
+        LogDebug("Tunnel has closed, stopping reading.");
+    }
+}
+
 void VerifyRpIdHash(const char *rpId, const BufferView &rpIdHash)
 {
     assert(rpId);
index dcf0d849e7511ce69eed7b1c4ba277e7ccccd7a6..f3c92073d84d3a2204692918ce3b67648f217079 100644 (file)
 #include "encrypted_tunnel.h"
 #include "message.h"
 
+#include <functional>
 #include <memory>
 #include <webauthn-types.h>
 
 class ICtapMessageProcessor {
 public:
-    struct MCResult {
+    template <class Response>
+    struct Result {
         PostHandshakeResponse getInfo;
         UpdateMessage upMsg;
-        MakeCredentialResponse response;
-    };
-
-    struct GAResult {
-        PostHandshakeResponse getInfo;
-        UpdateMessage upMsg;
-        GetAssertionResponse response;
+        Response response;
     };
 
     virtual void SetEncryptedTunnel(std::unique_ptr<IEncryptedTunnel> encryptedTunnel) = 0;
 
+    using MCResult = Result<MakeCredentialResponse>;
+
     virtual MCResult MakeCredential(const wauthn_client_data_s &clientData,
                                     const wauthn_pubkey_cred_creation_options_s &options) = 0;
 
+    using GAResult = Result<GetAssertionResponse>;
+
     virtual GAResult GetAssertion(const wauthn_client_data_s &clientData,
                                   const wauthn_pubkey_cred_request_options_s &options) = 0;
 
+    virtual void
+    ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)> updateMsgCallback) = 0;
+
     // May be called from the other thread.
     virtual void Cancel() = 0;
 
@@ -60,6 +63,9 @@ public:
     GAResult GetAssertion(const wauthn_client_data_s &clientData,
                           const wauthn_pubkey_cred_request_options_s &options) override;
 
+    void
+    ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)> updateMsgCallback) override;
+
     void Cancel() override;
 
 private:
index 3a7ad3a891e91880379134486e0f7b48e73f0486..b4d31a82ca4ce4085e41ac0b141ab30249d8fdd5 100644 (file)
@@ -55,4 +55,9 @@ void EncryptedTunnel::WriteBinary(CryptoBuffer msg)
     m_tunnel->WriteBinary(ciphertext);
 }
 
+void EncryptedTunnel::CloseConnectionAfter(uint32_t usecs)
+{
+    m_tunnel->CloseConnectionAfter(usecs);
+}
+
 void EncryptedTunnel::Cancel() { m_tunnel->Cancel(); }
index 0f31c952e692cd95a86b07811dae4fa88ba9a95b..93ecb2408c2ca144b229c8b7565712eabd1f1472 100644 (file)
@@ -28,11 +28,13 @@ public:
 
     virtual void WriteBinary(CryptoBuffer msg) = 0;
 
+    virtual void CloseConnectionAfter(uint32_t usecs) = 0;
+
     // May be called from the other thread.
     virtual void Cancel() = 0;
 
     // Needed as derived classes are handled with pointers to this interface
-    virtual ~IEncryptedTunnel(){};
+    virtual ~IEncryptedTunnel() = default;
 };
 
 class EncryptedTunnel : public IEncryptedTunnel {
@@ -47,6 +49,8 @@ public:
 
     void WriteBinary(CryptoBuffer msg) override;
 
+    void CloseConnectionAfter(uint32_t usecs) override;
+
     void Cancel() override;
 
 private:
index 20853029ea7bfe0f8a1b3f71c7efd5d8174de521..07b5dcd10b14221e4893ea9575e88388e18872c8 100644 (file)
@@ -59,13 +59,14 @@ typedef Exception<WAUTHN_ERROR_TIMEOUT> Timeout;
 
 } // namespace Exception
 
-#define THROW_UNKNOWN(...) LOGGED_THROW(::Exception::Unknown, __VA_ARGS__)
-#define THROW_INVALID_PARAM(...) LOGGED_THROW(::Exception::InvalidParam, __VA_ARGS__)
-#define THROW_PERMISSION_DENIED(...) LOGGED_THROW(::Exception::PermissionDenied, __VA_ARGS__)
-#define THROW_UNSUPPORTED(...) LOGGED_THROW(::Exception::NotSupported, __VA_ARGS__)
-#define THROW_NOT_ALLOWED(...) LOGGED_THROW(::Exception::NotAllowed, __VA_ARGS__)
-#define THROW_INVALID_STATE(...) LOGGED_THROW(::Exception::InvalidState, __VA_ARGS__)
-#define THROW_ENCODING(...) LOGGED_THROW(::Exception::EncodingFailed, __VA_ARGS__)
-#define THROW_MEMORY() LOGGED_THROW(::Exception::MemoryError, "Memory error")
-#define THROW_CANCELLED() LOGGED_THROW(::Exception::Cancelled, "Operation cancelled")
-#define THROW_TIMEOUT(...) LOGGED_THROW(::Exception::Timeout, __VA_ARGS__)
+#define THROW_UNKNOWN(...) ERROR_LOGGED_THROW(::Exception::Unknown, __VA_ARGS__)
+#define THROW_INVALID_PARAM(...) ERROR_LOGGED_THROW(::Exception::InvalidParam, __VA_ARGS__)
+#define THROW_PERMISSION_DENIED(...) ERROR_LOGGED_THROW(::Exception::PermissionDenied, __VA_ARGS__)
+#define THROW_UNSUPPORTED(...) ERROR_LOGGED_THROW(::Exception::NotSupported, __VA_ARGS__)
+#define THROW_NOT_ALLOWED(...) ERROR_LOGGED_THROW(::Exception::NotAllowed, __VA_ARGS__)
+#define THROW_INVALID_STATE(...) ERROR_LOGGED_THROW(::Exception::InvalidState, __VA_ARGS__)
+#define THROW_ENCODING(...) ERROR_LOGGED_THROW(::Exception::EncodingFailed, __VA_ARGS__)
+#define THROW_MEMORY() ERROR_LOGGED_THROW(::Exception::MemoryError, "Memory error")
+#define THROW_TIMEOUT(...) ERROR_LOGGED_THROW(::Exception::Timeout, __VA_ARGS__)
+
+#define THROW_CANCELLED() DEBUG_LOGGED_THROW(::Exception::Cancelled, "Operation cancelled")
index 3a1e8c336cb4be3c8821baefbf2f9dd1930766ad..f84bfd726e14174059e29e88cd22fa195b5e0b57 100644 (file)
@@ -123,10 +123,18 @@ public:
     } catch (...) {               \
     }
 
-#define LOGGED_THROW(exceptionType, msg)                    \
+#define ERROR_LOGGED_THROW(exceptionType, msg)              \
     do {                                                    \
         std::ostringstream oss;                             \
         oss << msg;                                         \
         TRY_LOG_ERROR("Throwing exception: " << oss.str()); \
         throw exceptionType(oss.str());                     \
     } while (false)
+
+#define DEBUG_LOGGED_THROW(exceptionType, msg)              \
+    do {                                                    \
+        std::ostringstream oss;                             \
+        oss << msg;                                         \
+        TRY_LOG_DEBUG("Throwing exception: " << oss.str()); \
+        throw exceptionType(oss.str());                     \
+    } while (false)
index 3fbba44a9d742947c3edbbb3165d357bc8e21821..3d4d489773fb9ec79104820c7c32290bad3ec1fc 100644 (file)
@@ -211,7 +211,7 @@ public:
         Buffer m_handshakeSignature;
     };
 
-    const std::optional<LinkData> &GetLinkData() const { return m_linkData; }
+    const std::optional<LinkData> &GetLinkData() const noexcept { return m_linkData; }
 
 private:
     std::optional<LinkData> m_linkData;
index 84b6161064cad7e836547057c2c85633e7f3aa91..94090815cd78ef786462cc7379e2f30bdd68967a 100644 (file)
@@ -21,11 +21,10 @@ void QrCodeShower::ShowQrCode(const CryptoBuffer &qrSecret,
                               const CryptoBuffer &identityKeyCompressed,
                               const Hint &hint,
                               bool stateAssisted,
-                              wauthn_cb_display_qrcode displayQrCodeCallback,
-                              void *displayQrCodeCallbackUserData)
+                              std::function<void(std::string &&)> &&qrCodeCallback)
 {
     std::string fidoUri;
     CborEncoding::Cbor cbor;
     cbor.EncodeQRContents(identityKeyCompressed, qrSecret, hint, stateAssisted, fidoUri);
-    displayQrCodeCallback(fidoUri.c_str(), displayQrCodeCallbackUserData);
+    qrCodeCallback(std::move(fidoUri));
 }
index 10568dd6ba0512fee76f8e88a144b375552d8933..2d1345bc477e36c285b46f6e61aadb63233ee4ee 100644 (file)
@@ -19,7 +19,8 @@
 #include "common.h"
 #include "crypto/common.h"
 
-#include <webauthn-hal.h>
+#include <functional>
+#include <string>
 
 class IQrCodeShower {
 public:
@@ -35,8 +36,7 @@ public:
                             const CryptoBuffer &identityKeyCompressed,
                             const Hint &hint,
                             bool stateAssisted,
-                            wauthn_cb_display_qrcode displayQrCodeCallback,
-                            void *displayQrCodeCallbackUserData) = 0;
+                            std::function<void(std::string &&)> &&qrCodeCallback) = 0;
 };
 
 class QrCodeShower : public IQrCodeShower {
@@ -45,6 +45,5 @@ public:
                     const CryptoBuffer &identityKeyCompressed,
                     const Hint &hint,
                     bool stateAssisted,
-                    wauthn_cb_display_qrcode displayQrCodeCallback,
-                    void *displayQrCodeCallbackUserData) override;
+                    std::function<void(std::string &&)> &&qrCodeCallback) override;
 };
index bbb4ea6aa9f1522a56d482d00307984ed0fb04fa..bb8fef3ba419324544784dd2607b4aa5ab35744c 100644 (file)
 
 #include <utility>
 
-QrTransaction::QrTransaction(wauthn_cb_display_qrcode displayQrCodeCallback,
-                             void *displayQrCodeCallbackUserData,
-                             Hint hint,
-                             std::unique_ptr<IQrCodeShower> qrCodeShower,
-                             IBtAdvertScannerUPtr btAdvertScanner,
-                             std::unique_ptr<IHandshake> handshake,
-                             std::unique_ptr<ICtapMessageProcessor> ctapMessageProcessor)
-: m_displayQrCodeCallback{displayQrCodeCallback},
-  m_displayQrCodeCallbackUserData{displayQrCodeCallbackUserData},
-  m_hint{std::move(hint)},
-  m_qrCodeShower{std::move(qrCodeShower)},
-  m_btAdvertScanner{std::move(btAdvertScanner)},
-  m_handshake{std::move(handshake)},
-  m_ctapMessageProcessor{std::move(ctapMessageProcessor)}
+void QrTransaction::Initialize(Hint &&hint,
+                               std::function<void(std::string &&)> &&qrCodeCallback,
+                               std::unique_ptr<IQrCodeShower> &&qrCodeShower,
+                               IBtAdvertScannerUPtr &&btAdvertScanner,
+                               std::unique_ptr<IHandshake> &&handshake,
+                               std::unique_ptr<ICtapMessageProcessor> &&ctapMessageProcessor)
 {
+    m_hint = std::move(hint);
+    m_qrCodeCallback = std::move(qrCodeCallback);
+    m_qrCodeShower = std::move(qrCodeShower);
+    m_btAdvertScanner = std::move(btAdvertScanner);
+    m_handshake = std::move(handshake);
+    m_ctapMessageProcessor = std::move(ctapMessageProcessor);
 }
 
-ITransaction::MCResult
-QrTransaction::PerformTransaction(const wauthn_client_data_s &clientData,
-                                  const wauthn_pubkey_cred_creation_options_s &options)
+ITransaction::MCResult QrTransaction::Perform(const wauthn_client_data_s &clientData,
+                                              const wauthn_pubkey_cred_creation_options_s &options)
 {
     return DoPerformTransaction<ITransaction::MCResult>(
         &ICtapMessageProcessor::MakeCredential, clientData, options);
 }
 
-ITransaction::GAResult
-QrTransaction::PerformTransaction(const wauthn_client_data_s &clientData,
-                                  const wauthn_pubkey_cred_request_options_s &options)
+ITransaction::GAResult QrTransaction::Perform(const wauthn_client_data_s &clientData,
+                                              const wauthn_pubkey_cred_request_options_s &options)
 {
     return DoPerformTransaction<ITransaction::GAResult>(
         &ICtapMessageProcessor::GetAssertion, clientData, options);
 }
 
+void QrTransaction::ProcessFollowingUpdateMsgs(
+    std::function<void(UpdateMessage &&)> updateMsgCallback)
+{
+    m_ctapMessageProcessor->ProcessFollowingUpdateMsgs(std::move(updateMsgCallback));
+}
+
 template <class Result, class CmdResult, class Options>
 Result QrTransaction::DoPerformTransaction(
     CmdResult (ICtapMessageProcessor::*cmd)(const wauthn_client_data_s &, const Options &),
@@ -70,12 +72,8 @@ Result QrTransaction::DoPerformTransaction(
 
     CryptoBuffer qrSecret = Crypto::RandomBytes(QR_SECRET_LEN);
     Crypto::X9_62_P_256_Key identityKey = Crypto::X9_62_P_256_Key::Create();
-    m_qrCodeShower->ShowQrCode(qrSecret,
-                               identityKey.ExportPublicKey(true),
-                               m_hint,
-                               true,
-                               m_displayQrCodeCallback,
-                               m_displayQrCodeCallbackUserData);
+    m_qrCodeShower->ShowQrCode(
+        qrSecret, identityKey.ExportPublicKey(true), m_hint, true, std::move(m_qrCodeCallback));
 
     UpdateStateAndCheckForCancel(State::AWAITING_BLE_ADVERT);
 
@@ -109,8 +107,8 @@ Result QrTransaction::DoPerformTransaction(
 
     UpdateStateAndCheckForCancel(State::CTAP_MESSAGE_PROCESSING);
 
-    auto cmdRes = ((*m_ctapMessageProcessor).*cmd)(clientData, options);
-    const auto &linkData = cmdRes.upMsg.GetLinkData();
+    auto ctapRes = ((*m_ctapMessageProcessor).*cmd)(clientData, options);
+    const auto &linkData = ctapRes.upMsg.GetLinkData();
     if (linkData) {
         auto pubKey = Crypto::X9_62_P_256_Key::ImportPublicKey(linkData->m_authenticatorPublicKey);
         if (!verifySignature(
@@ -120,11 +118,7 @@ Result QrTransaction::DoPerformTransaction(
 
     UpdateStateAndCheckForCancel(State::NOT_IN_PROGRESS);
 
-    return {std::move(cmdRes.getInfo),
-            std::move(cmdRes.upMsg),
-            std::move(cmdRes.response),
-            identityKey.ExportPrivateKey(),
-            std::move(tunnelServerDomain)};
+    return {std::move(ctapRes), identityKey.ExportPrivateKey(), std::move(tunnelServerDomain)};
 }
 
 void QrTransaction::UpdateStateAndCheckForCancel(State state)
index a71c176f690392fc8fe6a9eca157ab3231d5c63d..1e9aa3bc1be0b0a8f00e55d7b7c5e654488e4923 100644 (file)
 #include "qr_code_shower.h"
 #include "transaction.h"
 
-class QrTransaction : public ITransaction {
+class IQrTransaction : public ITransaction {
 public:
-    QrTransaction(wauthn_cb_display_qrcode displayQrCodeCallback,
-                  void *displayQrCodeCallbackUserData,
-                  Hint hint,
-                  std::unique_ptr<IQrCodeShower> qrCodeShower = std::make_unique<QrCodeShower>(),
-                  IBtAdvertScannerUPtr btAdvertScanner = std::make_unique<BtAdvertScanner>(),
-                  std::unique_ptr<IHandshake> handshake = std::make_unique<Handshake>(),
-                  std::unique_ptr<ICtapMessageProcessor> ctapMessageProcessor =
-                      std::make_unique<CtapMessageProcessor>());
+    virtual void
+    Initialize(Hint &&hint,
+               std::function<void(std::string &&)> &&qrCodeCallback,
+               std::unique_ptr<IQrCodeShower> &&qrCodeShower = std::make_unique<QrCodeShower>(),
+               IBtAdvertScannerUPtr &&btAdvertScanner = std::make_unique<BtAdvertScanner>(),
+               std::unique_ptr<IHandshake> &&handshake = std::make_unique<Handshake>(),
+               std::unique_ptr<ICtapMessageProcessor> &&ctapMessageProcessor =
+                   std::make_unique<CtapMessageProcessor>()) = 0;
+};
+
+class QrTransaction : public IQrTransaction {
+public:
+    QrTransaction() noexcept = default;
+
+    void Initialize(Hint &&hint,
+                    std::function<void(std::string &&)> &&qrCodeCallback,
+                    std::unique_ptr<IQrCodeShower> &&qrCodeShower,
+                    IBtAdvertScannerUPtr &&btAdvertScanner,
+                    std::unique_ptr<IHandshake> &&handshake,
+                    std::unique_ptr<ICtapMessageProcessor> &&ctapMessageProcessor) override;
+
+    MCResult Perform(const wauthn_client_data_s &clientData,
+                     const wauthn_pubkey_cred_creation_options_s &options) override;
 
-    MCResult PerformTransaction(const wauthn_client_data_s &clientData,
-                                const wauthn_pubkey_cred_creation_options_s &options) override;
+    GAResult Perform(const wauthn_client_data_s &clientData,
+                     const wauthn_pubkey_cred_request_options_s &options) override;
 
-    GAResult PerformTransaction(const wauthn_client_data_s &clientData,
-                                const wauthn_pubkey_cred_request_options_s &options) override;
+    void
+    ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)> updateMsgCallback) override;
 
     void Cancel() override;
 
@@ -48,9 +63,8 @@ private:
         const wauthn_client_data_s &clientData,
         const Options &options);
 
-    wauthn_cb_display_qrcode m_displayQrCodeCallback;
-    void *m_displayQrCodeCallbackUserData;
-    Hint m_hint;
+    Hint m_hint{};
+    std::function<void(std::string &&)> m_qrCodeCallback;
     std::unique_ptr<IQrCodeShower> m_qrCodeShower;
     IBtAdvertScannerUPtr m_btAdvertScanner;
     std::unique_ptr<IHandshake> m_handshake;
index b1ba1fc16810576f93b4ac8699b4dbf274610a15..207f862216e04644f9f52ac87441e61e5abcbf2e 100644 (file)
@@ -42,6 +42,30 @@ wauthn_error_e RequestHandler::Cancel() noexcept
     }
 }
 
+wauthn_hybrid_linked_data_s *
+RequestHandler::AssignAndGetLinkedData(ForCallbacksCommon &forCallbacks) noexcept
+{
+    const auto &linkData = forCallbacks.upMsg.GetLinkData();
+    if (linkData) {
+        auto &ld = forCallbacks.linkedData;
+        Assign(ld.contact_id, forCallbacks.ldContactId, linkData->m_contactId);
+        Assign(ld.link_id, forCallbacks.ldLinkId, linkData->m_linkId);
+        Assign(ld.link_secret, forCallbacks.ldLinkSecret, linkData->m_linkSecret);
+        Assign(ld.authenticator_pubkey,
+               forCallbacks.ldAuthenticatorPubKey,
+               linkData->m_authenticatorPublicKey);
+        Assign(
+            ld.authenticator_name, forCallbacks.ldAuthenticatorName, linkData->m_authenticatorName);
+        Assign(ld.signature, forCallbacks.ldHandshakeSignature, linkData->m_handshakeSignature);
+        Assign(ld.tunnel_server_domain,
+               forCallbacks.ldTunnelServerDomain,
+               forCallbacks.tunnelServerDomain);
+        Assign(ld.identity_key, forCallbacks.ldIdentityKey, forCallbacks.clientPlatformPrivKey);
+        return &ld;
+    }
+    return nullptr;
+}
+
 RequestHandler::RequestHandler() noexcept
 {
     int err = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, nullptr);
index 134354c42bda35993ca49937a54ee69c07fa9f90..2039d516b087bd51d0d1fdd421f8e15ee0e2a8fb 100644 (file)
 #pragma once
 
 #include "base64.h"
-#include "bluetooth_advert.h"
 #include "common.h"
 #include "exception.h"
-#include "handshake.h"
-#include "log/log.h"
-#include "qr_code_shower.h"
 #include "qr_transaction.h"
 #include "state_assisted_transaction.h"
 #include "to_wauthn_const_buff.h"
 
 #include <memory>
 #include <mutex>
-#include <webauthn-hal.h>
+#include <webauthn-types.h>
 
-struct RequestMC {
-    const wauthn_pubkey_cred_creation_options_s *options;
-    wauthn_mc_callbacks_s *callbacks;
+enum class RequestKind {
+    MC,
+    GA,
 };
 
-struct RequestGA {
-    const wauthn_pubkey_cred_request_options_s *options;
-    wauthn_ga_callbacks_s *callbacks;
+template <RequestKind>
+struct Request {};
+
+template <>
+struct Request<RequestKind::MC> {
+    static constexpr auto KIND = RequestKind::MC;
+    static constexpr Hint HINT = {'m', 'c'};
+
+    const wauthn_pubkey_cred_creation_options_s *options = nullptr;
+    const wauthn_mc_callbacks_s *callbacks = nullptr;
 };
 
-[[nodiscard]] constexpr Hint ToHint(const RequestMC &) noexcept { return {'m', 'c'}; }
+template <>
+struct Request<RequestKind::GA> {
+    static constexpr auto KIND = RequestKind::GA;
+    static constexpr Hint HINT = {'g', 'a'};
 
-[[nodiscard]] constexpr Hint ToHint(const RequestGA &) noexcept { return {'g', 'a'}; }
+    const wauthn_pubkey_cred_request_options_s *options = nullptr;
+    const wauthn_ga_callbacks_s *callbacks = nullptr;
+};
 
-enum class RequestKind {
-    MC,
-    GA,
+template <RequestKind>
+struct ForCallbacks {};
+
+struct ForCallbacksCommon {
+    UpdateMessage upMsg;
+    Buffer clientPlatformPrivKey;
+    std::string tunnelServerDomain;
+
+    wauthn_const_buffer_s responseAuthenticatorData = {};
+
+    wauthn_const_buffer_s credentialRawId = {};
+    std::string credentialIdInBase64;
+    wauthn_const_buffer_s credentialId = {};
+
+    wauthn_const_buffer_s ldContactId = {};
+    wauthn_const_buffer_s ldLinkId = {};
+    wauthn_const_buffer_s ldLinkSecret = {};
+    wauthn_const_buffer_s ldAuthenticatorPubKey = {};
+    wauthn_const_buffer_s ldAuthenticatorName = {};
+    wauthn_const_buffer_s ldHandshakeSignature = {};
+    wauthn_const_buffer_s ldTunnelServerDomain = {};
+    wauthn_const_buffer_s ldIdentityKey = {};
+
+    wauthn_hybrid_linked_data_s linkedData = {};
 };
 
-[[nodiscard]] constexpr RequestKind ToKind(const RequestMC &) noexcept { return RequestKind::MC; }
+template <>
+struct ForCallbacks<RequestKind::MC> : public ForCallbacksCommon {
+    MakeCredentialResponse ctapResponse;
+
+    wauthn_const_buffer_s responseAttestationObject = {};
+    wauthn_const_buffer_s responseSubjectPubkeyInfo = {};
 
-[[nodiscard]] constexpr RequestKind ToKind(const RequestGA &) noexcept { return RequestKind::GA; }
+    wauthn_authenticator_attestation_response_s response = {};
+
+    wauthn_pubkey_credential_attestation_s ca = {};
+};
 
+template <>
+struct ForCallbacks<RequestKind::GA> : public ForCallbacksCommon {
+    GetAssertionResponse ctapResponse;
+
+    wauthn_const_buffer_s responseSignature = {};
+    wauthn_const_buffer_s responseUserHandle = {};
+
+    wauthn_authenticator_assertion_response_s response = {};
+
+    wauthn_pubkey_credential_assertion_s ca = {};
+};
+
+// Information gained from tests with real authenticators:
+// - iPhone and Android allows two tunnels connected to it simultaneously. Test was performed this
+// way:
+//   1. QR-initiated MakeCredential
+//   2. Started awaiting UPDATE messages for MakeCredential connection
+//   3. In case of iPhone QR-initiated GetAssertion, in case of Android state-assisted GetAssertion
+//   4. Started awaiting UPDATE messages for GetAssertion connection
+//   5. After approx. 2 minutes iPhone closed MakeCredential connection
+//   6. Just after that authenticator closed GetAssertion connection.
 class RequestHandler {
 public:
     static RequestHandler &Instance() noexcept;
 
     wauthn_error_e Cancel() noexcept;
 
-    template <class Request>
-    void Process(
-        const wauthn_client_data_s *clientData,
-        Request request,
-        // If null, default implementations will be set in the function body to control exceptions.
-        std::unique_ptr<IQrCodeShower> qrCodeShower = nullptr,
-        IBtAdvertScannerUPtr btAdvertScanner = nullptr,
-        std::unique_ptr<IHandshake> handshake = nullptr,
-        std::unique_ptr<ICtapMessageProcessor> ctapMessageProcessor = nullptr) noexcept
+    template <RequestKind REQUEST_KIND>
+    void Process(const wauthn_client_data_s *clientData,
+                 Request<REQUEST_KIND> request,
+                 IQrTransaction &&qrTransaction,
+                 IStateAssistedTransaction &&stateAssistedTransaction) noexcept
     {
-        using namespace Exception;
+        if (!request.callbacks) {
+            TRY_LOG_ERROR("Missing callbacks");
+            return; // No way to report the result, so we do nothing.
+        }
+        if (!request.callbacks->response_callback) {
+            TRY_LOG_ERROR("Missing response_callback");
+            return; // No way to report the result, so we do nothing.
+        }
 
-        wauthn_error_e result;
-        try {
-            if (!request.callbacks || !request.callbacks->response_callback)
-                LogError("Missing response callback");
-
-            if (clientData == nullptr || request.options == nullptr)
-                throw InvalidParam{};
-
-            if (!qrCodeShower)
-                qrCodeShower = std::make_unique<QrCodeShower>();
-            if (!btAdvertScanner)
-                btAdvertScanner = std::make_unique<BtAdvertScanner>();
-            if (!handshake)
-                handshake = std::make_unique<Handshake>();
-            if (!ctapMessageProcessor)
-                ctapMessageProcessor = std::make_unique<CtapMessageProcessor>();
-
-            {
-                std::lock_guard<std::mutex> lock(m_mutex);
-                if (m_currentTransaction != nullptr) {
-                    LogError("New request received while processing another one");
-                    throw NotAllowed{}; // Server should not allow it
-                }
+        static constexpr auto SUCCESSFUL_RESULT = WAUTHN_ERROR_NONE;
+        ForCallbacks<REQUEST_KIND> forCallbacks;
+        static_assert(std::is_nothrow_default_constructible_v<decltype(forCallbacks)>,
+                      "Needed as it is not guarded by try-catch");
+        auto result = SUCCESSFUL_RESULT;
+        ITransaction *transaction = nullptr;
+        if (!request.callbacks->linked_data_callback) {
+            TRY_LOG_ERROR("Missing linked_data_callback");
+            result = WAUTHN_ERROR_INVALID_PARAMETER;
+        } else if (!clientData) {
+            TRY_LOG_ERROR("Missing client_data");
+            result = WAUTHN_ERROR_INVALID_PARAMETER;
+        } else if (!request.options) {
+            TRY_LOG_ERROR("Missing request options");
+            result = WAUTHN_ERROR_INVALID_PARAMETER;
+        } else if (!request.options->linked_device && !request.callbacks->qrcode_callback) {
+            TRY_LOG_ERROR("Missing qrcode_callback");
+            result = WAUTHN_ERROR_INVALID_PARAMETER;
+        } else {
+            try {
                 if (request.options->linked_device) {
-                    m_currentTransaction =
-                        std::make_unique<StateAssistedTransaction>(ToHint(request),
-                                                                   std::move(handshake),
-                                                                   std::move(btAdvertScanner),
-                                                                   std::move(ctapMessageProcessor));
+                    transaction = &stateAssistedTransaction;
+                    stateAssistedTransaction.Initialize(Hint{Request<REQUEST_KIND>::HINT});
                 } else {
-                    if (!request.callbacks || !request.callbacks->qrcode_callback) {
-                        LogError("Missing QR callback");
-                        throw InvalidParam{};
-                    }
-                    m_currentTransaction =
-                        std::make_unique<QrTransaction>(request.callbacks->qrcode_callback,
-                                                        request.callbacks->user_data,
-                                                        ToHint(request),
-                                                        std::move(qrCodeShower),
-                                                        std::move(btAdvertScanner),
-                                                        std::move(handshake),
-                                                        std::move(ctapMessageProcessor));
+                    transaction = &qrTransaction;
+                    qrTransaction.Initialize(
+                        Hint{Request<REQUEST_KIND>::HINT}, [&](std::string &&qrCodeContents) {
+                            try {
+                                request.callbacks->qrcode_callback(qrCodeContents.c_str(),
+                                                                   request.callbacks->user_data);
+                            } catch (const std::exception &e) {
+                                TRY_LOG_ERROR(
+                                    "qrcode_callback has thrown an exception: " << e.what());
+                                throw; // safe - it will be caught below
+                            } catch (...) {
+                                TRY_LOG_ERROR("qrcode_callback has thrown an unknown exception");
+                                throw; // safe - it will be caught below
+                            }
+                        });
                 }
+                SetCurrentTransaction(*transaction);
+                ProcessTransactionResult(clientData->client_data_json,
+                                         forCallbacks,
+                                         transaction->Perform(*clientData, *request.options));
+            } catch (const Exception::ExceptionBase &e) {
+                result = e.Code();
+            } catch (const std::bad_alloc &) {
+                result = WAUTHN_ERROR_MEMORY;
+            } catch (const std::exception &e) {
+                TRY_LOG_ERROR("Caught unexpected exception: " << e.what());
+                result = WAUTHN_ERROR_UNKNOWN;
+            } catch (...) {
+                TRY_LOG_ERROR("Caught unknown exception");
+                result = WAUTHN_ERROR_UNKNOWN;
             }
-
-            auto cleanup = OnScopeExit([&] {
-                try {
-                    std::lock_guard<std::mutex> lock(m_mutex);
-                    m_currentTransaction = nullptr;
-                } catch (...) {
-                    TRY_LOG_ERROR("Mutex lock failed");
-                    result = WAUTHN_ERROR_UNKNOWN;
-                }
-            });
-
-            auto transactionRes =
-                m_currentTransaction->PerformTransaction(*clientData, *request.options);
-            if (request.callbacks && request.callbacks->response_callback) {
-                ProcessTransactionResult(transactionRes,
-                                         *clientData,
-                                         request.callbacks->response_callback,
-                                         request.callbacks->user_data);
-                return;
-            }
-        } catch (const ExceptionBase &ex) {
-            result = ex.Code();
-        } catch (const std::bad_alloc &) {
-            result = WAUTHN_ERROR_MEMORY;
+        }
+        // Reset m_currentTransaction so that other requests can be handled
+        try {
+            UnsetCurrentTransaction();
         } catch (...) {
+            TRY_LOG_ERROR("Mutex lock failed");
             result = WAUTHN_ERROR_UNKNOWN;
         }
-
-        if (request.callbacks && request.callbacks->response_callback) {
+        // response_callback
+        bool responseCallbackHasThrown = false;
+        try {
+            request.callbacks->response_callback(result == SUCCESSFUL_RESULT ? &forCallbacks.ca
+                                                                             : nullptr,
+                                                 result,
+                                                 request.callbacks->user_data);
+        } catch (const std::exception &e) {
+            TRY_LOG_ERROR("response_callback has thrown an exception: " << e.what());
+            responseCallbackHasThrown = true;
+        } catch (...) {
+            TRY_LOG_ERROR("response_callback has thrown an unknown exception");
+            responseCallbackHasThrown = true;
+        }
+        if (result != SUCCESSFUL_RESULT)
+            return;
+        // linked_data_callback
+        result = WAUTHN_ERROR_NONE;
+        if (responseCallbackHasThrown)
+            result = WAUTHN_ERROR_UNKNOWN;
+        else {
             try {
-                request.callbacks->response_callback(nullptr, result, request.callbacks->user_data);
+                transaction->ProcessFollowingUpdateMsgs([&](UpdateMessage &&upMsg) {
+                    forCallbacks.upMsg = std::move(upMsg);
+                    try {
+                        request.callbacks->linked_data_callback(
+                            AssignAndGetLinkedData(forCallbacks),
+                            WAUTHN_ERROR_NONE_AND_WAIT,
+                            request.callbacks->user_data);
+                    } catch (const std::exception &e) {
+                        TRY_LOG_ERROR("linked_data_callback has thrown an exception: " << e.what());
+                        throw; // safe - it will be caught below
+                    } catch (...) {
+                        LogError("linked_data_callback has thrown an unknown exception");
+                        throw; // safe - it will be caught below
+                    }
+                });
+            } catch (const Exception::ExceptionBase &e) {
+                result = e.Code();
+            } catch (const std::bad_alloc &) {
+                result = WAUTHN_ERROR_MEMORY;
+            } catch (const std::exception &e) {
+                TRY_LOG_ERROR("Caught unexpected exception: " << e.what());
+                result = WAUTHN_ERROR_UNKNOWN;
             } catch (...) {
-                TRY_LOG_ERROR("Response callback has thrown an exception");
+                result = WAUTHN_ERROR_UNKNOWN;
             }
         }
+        // The final call of linked_data_callback
+        try {
+            request.callbacks->linked_data_callback(nullptr, result, request.callbacks->user_data);
+        } catch (const std::exception &e) {
+            TRY_LOG_ERROR("linked_data_callback has thrown an exception: " << e.what());
+        } catch (...) {
+            TRY_LOG_ERROR("linked_data_callback has thrown an exception");
+        }
     }
 
-    template <class TransactionRes, class Callback>
-    void ProcessTransactionResult(const TransactionRes &transactionRes,
-                                  const wauthn_client_data_s &clientData,
-                                  Callback &&callback,
-                                  void *callbackUserData)
+private:
+    void SetCurrentTransaction(ITransaction &transaction)
     {
-        static constexpr bool isMC = std::is_same_v<TransactionRes, ITransaction::MCResult>;
-        auto dependent = [&] {
-            if constexpr (isMC) {
-                struct {
-                    wauthn_const_buffer_s attestationObject;
-                    wauthn_const_buffer_s subjectPubkeyInfo;
-                    wauthn_authenticator_attestation_response_s response;
-                    wauthn_pubkey_credential_attestation_s ca;
-                } val{};
-                return val;
-            } else {
-                struct {
-                    wauthn_const_buffer_s responseSignature;
-                    wauthn_const_buffer_s responseUserHandle;
-                    wauthn_authenticator_assertion_response_s response;
-                    wauthn_pubkey_credential_assertion_s ca;
-                } val{};
-                return val;
-            }
-        }();
-
-        auto assign =
-            [](wauthn_const_buffer_s *&dest, wauthn_const_buffer_s &localVar, auto &&value) {
-                localVar = ToWauthnConstBuff(std::forward<decltype(value)>(value));
-                dest = &localVar;
-            };
-
-        wauthn_const_buffer_s responseAuthenticatorData;
-        wauthn_const_buffer_s rawId;
+        auto lock = std::lock_guard{m_mutex};
+        if (m_currentTransaction) {
+            THROW_NOT_ALLOWED("New request received while processing another one.");
+        }
+        m_currentTransaction = &transaction;
+    }
 
-        if constexpr (isMC) {
-            auto &attestationData = transactionRes.response.m_authData.m_attestationData.value();
+    void UnsetCurrentTransaction()
+    {
+        auto lock = std::lock_guard{m_mutex};
+        m_currentTransaction = nullptr;
+    }
 
-            assign(dependent.response.attestation_object,
-                   dependent.attestationObject,
-                   transactionRes.response.m_attestationObject);
-            dependent.response.transports = WAUTHN_TRANSPORT_NONE;
-            for (const auto &transport : transactionRes.getInfo.m_transports) {
+    template <RequestKind REQUEST_KIND, class TransactionResult>
+    void ProcessTransactionResult(wauthn_const_buffer_s *clientDataJson,
+                                  ForCallbacks<REQUEST_KIND> &forCallbacks,
+                                  TransactionResult &&transactionRes)
+    {
+        forCallbacks.upMsg = std::move(transactionRes.ctapResult.upMsg);
+        forCallbacks.clientPlatformPrivKey = std::move(transactionRes.clientPlatformPrivKey);
+        forCallbacks.tunnelServerDomain = std::move(transactionRes.tunnelServerDomain);
+        forCallbacks.ctapResponse = std::move(transactionRes.ctapResult.response);
+
+        forCallbacks.response.client_data_json = clientDataJson;
+        Assign(forCallbacks.response.authenticator_data,
+               forCallbacks.responseAuthenticatorData,
+               forCallbacks.ctapResponse.m_authDataRaw);
+
+        if constexpr (REQUEST_KIND == RequestKind::MC) {
+            Assign(forCallbacks.response.attestation_object,
+                   forCallbacks.responseAttestationObject,
+                   forCallbacks.ctapResponse.m_attestationObject);
+
+            auto &responseTransports = forCallbacks.response.transports;
+            responseTransports = WAUTHN_TRANSPORT_NONE;
+            for (const auto &transport : transactionRes.ctapResult.getInfo.m_transports) {
                 if (transport == "internal")
-                    dependent.response.transports |= WAUTHN_TRANSPORT_INTERNAL;
+                    responseTransports |= WAUTHN_TRANSPORT_INTERNAL;
                 else if (transport == "hybrid")
-                    dependent.response.transports |= WAUTHN_TRANSPORT_HYBRID;
+                    responseTransports |= WAUTHN_TRANSPORT_HYBRID;
                 else
                     THROW_UNKNOWN("Unexpected transport: " << transport);
             }
-            assign(dependent.response.subject_pubkey_info,
-                   dependent.subjectPubkeyInfo,
-                   attestationData.m_publicKeyDer);
-            dependent.response.pubkey_alg = attestationData.m_alg;
 
-            assign(dependent.ca.rawId, rawId, attestationData.m_credentialId);
-        } else {
-            dependent.response.attestation_object = nullptr;
-            assign(dependent.response.signature,
-                   dependent.responseSignature,
-                   transactionRes.response.m_signature);
+            auto &attestationData = forCallbacks.ctapResponse.m_authData.m_attestationData.value();
 
-            dependent.responseUserHandle = ToWauthnConstBuff(transactionRes.response.m_userId);
-            dependent.response.user_handle =
-                dependent.responseUserHandle.size != 0 ? &dependent.responseUserHandle : nullptr;
+            Assign(forCallbacks.response.subject_pubkey_info,
+                   forCallbacks.responseSubjectPubkeyInfo,
+                   attestationData.m_publicKeyDer);
+            forCallbacks.response.pubkey_alg = attestationData.m_alg;
 
-            assign(dependent.ca.rawId, rawId, transactionRes.response.m_credentialId);
+            Assign(forCallbacks.ca.rawId,
+                   forCallbacks.credentialRawId,
+                   attestationData.m_credentialId);
+        } else {
+            forCallbacks.response.attestation_object = nullptr;
+            Assign(forCallbacks.response.signature,
+                   forCallbacks.responseSignature,
+                   forCallbacks.ctapResponse.m_signature);
+
+            forCallbacks.responseUserHandle = ToWauthnConstBuff(forCallbacks.ctapResponse.m_userId);
+            forCallbacks.response.user_handle = forCallbacks.responseUserHandle.size != 0
+                ? &forCallbacks.responseUserHandle
+                : nullptr;
+
+            Assign(forCallbacks.ca.rawId,
+                   forCallbacks.credentialRawId,
+                   forCallbacks.ctapResponse.m_credentialId);
         }
 
-        dependent.response.client_data_json = clientData.client_data_json;
-        assign(dependent.response.authenticator_data,
-               responseAuthenticatorData,
-               transactionRes.response.m_authDataRaw);
-
-        auto idInBase64 = Base64UrlEncode(BufferView{rawId.data, rawId.size});
-        wauthn_const_buffer_s id;
-        wauthn_const_buffer_s ldContactId;
-        wauthn_const_buffer_s ldLinkId;
-        wauthn_const_buffer_s ldLinkSecret;
-        wauthn_const_buffer_s ldAuthenticatorPubKey;
-        wauthn_const_buffer_s ldAuthenticatorName;
-        wauthn_const_buffer_s ldHandshakeSignature;
-        wauthn_const_buffer_s ldTunnelServerDomain;
-        wauthn_const_buffer_s ldIdentityKey;
-
-        wauthn_hybrid_linked_data_s linkedDevice;
-        const auto &linkData = transactionRes.upMsg.GetLinkData();
-        if (linkData) {
-            assign(linkedDevice.contact_id, ldContactId, linkData->m_contactId);
-            assign(linkedDevice.link_id, ldLinkId, linkData->m_linkId);
-            assign(linkedDevice.link_secret, ldLinkSecret, linkData->m_linkSecret);
-            assign(linkedDevice.authenticator_pubkey,
-                   ldAuthenticatorPubKey,
-                   linkData->m_authenticatorPublicKey);
-            assign(linkedDevice.authenticator_name,
-                   ldAuthenticatorName,
-                   linkData->m_authenticatorName);
-            assign(linkedDevice.signature, ldHandshakeSignature, linkData->m_handshakeSignature);
-            assign(linkedDevice.tunnel_server_domain,
-                   ldTunnelServerDomain,
-                   transactionRes.tunnelServerDomain);
-            assign(linkedDevice.identity_key, ldIdentityKey, transactionRes.clientPlatformPrivKey);
-        }
+        forCallbacks.credentialIdInBase64 = Base64UrlEncode(
+            BufferView{forCallbacks.credentialRawId.data, forCallbacks.credentialRawId.size});
 
-        assign(dependent.ca.id, id, idInBase64);
-        dependent.ca.type = PCT_PUBLIC_KEY;
-        dependent.ca.response = &dependent.response;
-        dependent.ca.authenticator_attachment = AA_CROSS_PLATFORM;
-        dependent.ca.extensions = nullptr;
-        dependent.ca.linked_device = linkData ? &linkedDevice : nullptr;
+        Assign(forCallbacks.ca.id, forCallbacks.credentialId, forCallbacks.credentialIdInBase64);
+        forCallbacks.ca.type = PCT_PUBLIC_KEY;
+        forCallbacks.ca.response = &forCallbacks.response;
+        forCallbacks.ca.authenticator_attachment = AA_CROSS_PLATFORM;
+        forCallbacks.ca.extensions = nullptr;
+        forCallbacks.ca.linked_device = AssignAndGetLinkedData(forCallbacks);
+    }
 
-        try {
-            std::forward<Callback>(callback)(&dependent.ca, WAUTHN_ERROR_NONE, callbackUserData);
-        } catch (...) {
-            TRY_LOG_ERROR("Response callback has thrown an exception");
-        }
+    template <class T>
+    void Assign(wauthn_const_buffer_s *&dest, wauthn_const_buffer_s &storageVar, T &&value) noexcept
+    {
+        storageVar = ToWauthnConstBuff(std::forward<T>(value));
+        dest = &storageVar;
     }
 
-private:
+    wauthn_hybrid_linked_data_s *AssignAndGetLinkedData(ForCallbacksCommon &forCallbacks) noexcept;
+
     RequestHandler() noexcept;
 
     std::mutex m_mutex;
-    std::unique_ptr<ITransaction> m_currentTransaction;
+    ITransaction *m_currentTransaction = nullptr;
 };
index d28677159a84b31d4c3733e3aa177c7b1dc2ddb0..7fe2345509b1f4de7a5902399ea297529905e7eb 100644 (file)
 #include "state_assisted_transaction.h"
 #include "verify_signature.h"
 
-StateAssistedTransaction::StateAssistedTransaction(
-    Hint hint,
-    std::unique_ptr<IHandshake> handshake,
-    IBtAdvertScannerUPtr btAdvertScanner,
-    std::unique_ptr<ICtapMessageProcessor> ctapMessageProcessor)
-: m_hint{std::move(hint)},
-  m_handshake{std::move(handshake)},
-  m_btAdvertScanner{std::move(btAdvertScanner)},
-  m_ctapMessageProcessor{std::move(ctapMessageProcessor)}
+void StateAssistedTransaction::Initialize(
+    Hint &&hint,
+    std::unique_ptr<IHandshake> &&handshake,
+    IBtAdvertScannerUPtr &&btAdvertScanner,
+    std::unique_ptr<ICtapMessageProcessor> &&ctapMessageProcessor)
 {
+    m_hint = std::move(hint);
+    m_handshake = std::move(handshake);
+    m_btAdvertScanner = std::move(btAdvertScanner);
+    m_ctapMessageProcessor = std::move(ctapMessageProcessor);
 }
 
 ITransaction::MCResult
-StateAssistedTransaction::PerformTransaction(const wauthn_client_data_s &clientData,
-                                             const wauthn_pubkey_cred_creation_options_s &options)
+StateAssistedTransaction::Perform(const wauthn_client_data_s &clientData,
+                                  const wauthn_pubkey_cred_creation_options_s &options)
 {
     return DoPerformTransaction<ITransaction::MCResult>(
         &ICtapMessageProcessor::MakeCredential, clientData, options);
 }
 
 ITransaction::GAResult
-StateAssistedTransaction::PerformTransaction(const wauthn_client_data_s &clientData,
-                                             const wauthn_pubkey_cred_request_options_s &options)
+StateAssistedTransaction::Perform(const wauthn_client_data_s &clientData,
+                                  const wauthn_pubkey_cred_request_options_s &options)
 {
     return DoPerformTransaction<ITransaction::GAResult>(
         &ICtapMessageProcessor::GetAssertion, clientData, options);
 }
 
+void StateAssistedTransaction::ProcessFollowingUpdateMsgs(
+    std::function<void(UpdateMessage &&)> updateMsgCallback)
+{
+    m_ctapMessageProcessor->ProcessFollowingUpdateMsgs(std::move(updateMsgCallback));
+}
+
 template <class Result, class CmdResult, class Options>
 Result StateAssistedTransaction::DoPerformTransaction(
     CmdResult (ICtapMessageProcessor::*cmd)(const wauthn_client_data_s &, const Options &),
@@ -57,17 +63,17 @@ Result StateAssistedTransaction::DoPerformTransaction(
     const Options &options)
 {
     if (!options.linked_device)
-        THROW_INVALID_PARAM("linked_device is null");
+        THROW_INVALID_PARAM("linked_device is NULL");
     const wauthn_hybrid_linked_data_s &linkedDevice = *options.linked_device;
 
     if (!linkedDevice.tunnel_server_domain)
-        THROW_INVALID_PARAM("linked_device->tunnel_server_domain is null");
+        THROW_INVALID_PARAM("linked_device->tunnel_server_domain is NULL");
     auto tunnelServerDomain =
         std::string(reinterpret_cast<const char *>(linkedDevice.tunnel_server_domain->data),
                     linkedDevice.tunnel_server_domain->size);
 
     if (!linkedDevice.contact_id)
-        THROW_INVALID_PARAM("linked_device->contact_id is null");
+        THROW_INVALID_PARAM("linked_device->contact_id is NULL");
     auto contactId = BufferView{linkedDevice.contact_id->data, linkedDevice.contact_id->size};
 
     Buffer clientPayload(33, '\0');
@@ -76,7 +82,7 @@ Result StateAssistedTransaction::DoPerformTransaction(
     {
         auto map = encoder.OpenMap(3);
         if (!linkedDevice.link_id)
-            THROW_INVALID_PARAM("linked_device->link_id is null");
+            THROW_INVALID_PARAM("linked_device->link_id is NULL");
         map.AppendByteStringAt(1, *linkedDevice.link_id);
         map.AppendByteStringAt(2, clientNonce);
         map.AppendTextStringAt(3, std::string_view{m_hint.data(), m_hint.size()});
@@ -93,7 +99,7 @@ Result StateAssistedTransaction::DoPerformTransaction(
 
     CryptoBuffer decryptedBleAdvert;
     if (!linkedDevice.link_secret)
-        THROW_INVALID_PARAM("linked_device->link_secret is null");
+        THROW_INVALID_PARAM("linked_device->link_secret is NULL");
     auto linkSecret = Buffer{linkedDevice.link_secret->data,
                              linkedDevice.link_secret->data + linkedDevice.link_secret->size};
     auto eidKey = DeriveKey(linkSecret, clientNonce, KeyPurpose::EIDKey, EID_KEY_LEN);
@@ -101,7 +107,7 @@ Result StateAssistedTransaction::DoPerformTransaction(
 
     auto psk = DeriveKey(linkSecret, decryptedBleAdvert, KeyPurpose::PSK, PSK_LEN);
     if (!linkedDevice.authenticator_pubkey)
-        THROW_INVALID_PARAM("linked_device->authenticator_pubkey is null");
+        THROW_INVALID_PARAM("linked_device->authenticator_pubkey is NULL");
     auto authenticatorPubKey = CryptoBuffer{linkedDevice.authenticator_pubkey->data,
                                             linkedDevice.authenticator_pubkey->data +
                                                 linkedDevice.authenticator_pubkey->size};
@@ -115,13 +121,13 @@ Result StateAssistedTransaction::DoPerformTransaction(
     UpdateStateAndCheckForCancel(State::CTAP_MESSAGE_PROCESSING);
 
     if (!linkedDevice.identity_key)
-        THROW_INVALID_PARAM("linked_device->identity_key is null");
+        THROW_INVALID_PARAM("linked_device->identity_key is NULL");
     auto clientPlatformPrivKey =
         Buffer{linkedDevice.identity_key->data,
                linkedDevice.identity_key->data + linkedDevice.identity_key->size};
 
-    auto cmdRes = ((*m_ctapMessageProcessor).*cmd)(clientData, options);
-    const auto &linkData = cmdRes.upMsg.GetLinkData();
+    auto ctapRes = ((*m_ctapMessageProcessor).*cmd)(clientData, options);
+    const auto &linkData = ctapRes.upMsg.GetLinkData();
     if (linkData) {
         LogWarning("Unexpected UPDATE message in state-assisted transaction: handling it normally");
         auto identityKey = Crypto::X9_62_P_256_Key::ImportPrivateKey(clientPlatformPrivKey);
@@ -133,11 +139,7 @@ Result StateAssistedTransaction::DoPerformTransaction(
 
     UpdateStateAndCheckForCancel(State::NOT_IN_PROGRESS);
 
-    return {std::move(cmdRes.getInfo),
-            std::move(cmdRes.upMsg),
-            std::move(cmdRes.response),
-            std::move(clientPlatformPrivKey),
-            std::move(tunnelServerDomain)};
+    return {std::move(ctapRes), std::move(clientPlatformPrivKey), std::move(tunnelServerDomain)};
 }
 
 void StateAssistedTransaction::UpdateStateAndCheckForCancel(State state)
index 5889fec0c87ca63b01ab85e154864d494f32b1e4..e68e6d1b9f80d67bf59371a8c110b4364e045438 100644 (file)
 #include "handshake.h"
 #include "transaction.h"
 
-class StateAssistedTransaction : public ITransaction {
+class IStateAssistedTransaction : public ITransaction {
 public:
-    StateAssistedTransaction(
-        Hint hint,
-        std::unique_ptr<IHandshake> handshake = std::make_unique<Handshake>(),
-        IBtAdvertScannerUPtr btAdvertScanner = std::make_unique<BtAdvertScanner>(),
-        std::unique_ptr<ICtapMessageProcessor> ctapMessageProcessor =
-            std::make_unique<CtapMessageProcessor>());
+    virtual void
+    Initialize(Hint &&hint,
+               std::unique_ptr<IHandshake> &&handshake = std::make_unique<Handshake>(),
+               IBtAdvertScannerUPtr &&btAdvertScanner = std::make_unique<BtAdvertScanner>(),
+               std::unique_ptr<ICtapMessageProcessor> &&ctapMessageProcessor =
+                   std::make_unique<CtapMessageProcessor>()) = 0;
+};
+
+class StateAssistedTransaction : public IStateAssistedTransaction {
+public:
+    StateAssistedTransaction() noexcept = default;
+
+    void Initialize(Hint &&hint,
+                    std::unique_ptr<IHandshake> &&handshake,
+                    IBtAdvertScannerUPtr &&btAdvertScanner,
+                    std::unique_ptr<ICtapMessageProcessor> &&ctapMessageProcessor) override;
+
+    MCResult Perform(const wauthn_client_data_s &clientData,
+                     const wauthn_pubkey_cred_creation_options_s &options) override;
 
-    MCResult PerformTransaction(const wauthn_client_data_s &clientData,
-                                const wauthn_pubkey_cred_creation_options_s &options) override;
+    GAResult Perform(const wauthn_client_data_s &clientData,
+                     const wauthn_pubkey_cred_request_options_s &options) override;
 
-    GAResult PerformTransaction(const wauthn_client_data_s &clientData,
-                                const wauthn_pubkey_cred_request_options_s &options) override;
+    void
+    ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)> updateMsgCallback) override;
 
     void Cancel() override;
 
@@ -45,7 +58,7 @@ private:
         const wauthn_client_data_s &clientData,
         const Options &options);
 
-    Hint m_hint;
+    Hint m_hint{};
     std::unique_ptr<IHandshake> m_handshake;
     IBtAdvertScannerUPtr m_btAdvertScanner;
     std::unique_ptr<ICtapMessageProcessor> m_ctapMessageProcessor;
index d8ec4f8fbd18641a2046fd055553f87d8651fbb5..442fb035097ad83cc0b9628120946a33d870c8e6 100644 (file)
@@ -18,6 +18,7 @@
 
 #include "webauthn-types.h"
 
+#include <cstdint>
 #include <cstring>
 
 template <class T>
index ff1826eb4d57ba4ec96f44fceca03702777c2834..e20b320099b639ab099a1049b51e8de40dd0892f 100644 (file)
 
 #pragma once
 
+#include "ctap_message_processor.h"
 #include "message.h"
 
+#include <functional>
 #include <string>
 #include <webauthn-types.h>
 
@@ -31,26 +33,27 @@ public:
 
     virtual ~ITransaction() = default;
 
-    template <class Response>
+    template <class CtapResult>
     struct Result {
-        PostHandshakeResponse getInfo;
-        UpdateMessage upMsg;
-        Response response;
+        CtapResult ctapResult;
         Buffer clientPlatformPrivKey;
         std::string tunnelServerDomain;
     };
 
-    using MCResult = Result<MakeCredentialResponse>;
+    using MCResult = Result<ICtapMessageProcessor::MCResult>;
 
     // Throws Cancelled on cancel.
-    virtual MCResult PerformTransaction(const wauthn_client_data_s &clientData,
-                                        const wauthn_pubkey_cred_creation_options_s &options) = 0;
+    virtual MCResult Perform(const wauthn_client_data_s &clientData,
+                             const wauthn_pubkey_cred_creation_options_s &options) = 0;
 
-    using GAResult = Result<GetAssertionResponse>;
+    using GAResult = Result<ICtapMessageProcessor::GAResult>;
 
     // Throws Cancelled on cancel.
-    virtual GAResult PerformTransaction(const wauthn_client_data_s &clientData,
-                                        const wauthn_pubkey_cred_request_options_s &options) = 0;
+    virtual GAResult Perform(const wauthn_client_data_s &clientData,
+                             const wauthn_pubkey_cred_request_options_s &options) = 0;
+
+    virtual void
+    ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)> updateMsgCallback) = 0;
 
     // May be called from other threads.
     virtual void Cancel() = 0;
index 32e54333a009b2c824c9c3ff1f7345fc42d5f379..55f19646e6531060541e0ca54703ce772b48d473 100644 (file)
@@ -62,6 +62,7 @@ const std::unordered_map<enum lws_callback_reasons, std::string> REASON2STR = {
     PAIR(LWS_CALLBACK_EVENT_WAIT_CANCELLED),
     PAIR(LWS_CALLBACK_CONNECTING),
     PAIR(LWS_CALLBACK_VHOST_CERT_AGING),
+    PAIR(LWS_CALLBACK_TIMER),
 };
 
 const std::unordered_map<enum lws_close_status, std::string> CLOSE_STATUS2STR = {
@@ -181,6 +182,14 @@ std::vector<uint8_t> Tunnel::ReadBinary()
     return msg;
 }
 
+void Tunnel::CloseConnectionAfter(uint32_t usecs)
+{
+    TRY_LOG_DEBUG("setting close connection after to " << usecs << " microseconds.");
+    if (!m_connection)
+        THROW_INVALID_STATE("BUG: no active connection to schedule close for");
+    m_ws->SetTimer(m_connection, usecs);
+}
+
 void Tunnel::Disconnect()
 {
     if (!m_context) {
@@ -231,11 +240,9 @@ bool Tunnel::HandleEvent(Lws *wsi, enum lws_callback_reasons reason, void *in, s
             break;
 
         case LWS_CALLBACK_VHOST_CERT_AGING:
-            if (m_state != State::CONNECTED && m_state != State::WRITING) {
-                LogError("Unexpected event");
-                m_state = State::FAILED;
-                return true;
-            }
+            // Was seen happening as soon as just after LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED,
+            // as well as after the connection is fully connected. Therefore ignore this event
+            // completely as it may happen in any m_state.
             break;
 
         case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE: {
@@ -249,18 +256,18 @@ bool Tunnel::HandleEvent(Lws *wsi, enum lws_callback_reasons reason, void *in, s
                 if (csit != CLOSE_STATUS2STR.end())
                     description = csit->second;
 
-                LogError("Peer initiated close. Code: " << code << " (" << description << ")");
+                LogWarning("Peer initiated close. Code: " << code << " (" << description << ")");
             } else {
-                LogError("Peer initiated close");
+                LogWarning("Peer initiated close");
             }
 
-            m_state = State::FAILED;
+            m_state = State::WILL_DISCONNECT;
             // returning false will echo the close and then close the connection
             return false;
         }
 
         case LWS_CALLBACK_CLIENT_CLOSED: {
-            m_state = State::FAILED;
+            m_state = State::WILL_DISCONNECT;
             return true;
         }
 
@@ -397,6 +404,15 @@ bool Tunnel::HandleEvent(Lws *wsi, enum lws_callback_reasons reason, void *in, s
             break;
         }
 
+        case LWS_CALLBACK_TIMER:
+            if (m_context) {
+                // Tunnel timeout, close the connection.
+                m_state = State::WILL_DISCONNECT;
+                // Simply returning true is not enough for this event :(
+                m_ws->CancelService(m_context);
+            }
+            return true;
+
         case LWS_CALLBACK_EVENT_WAIT_CANCELLED: {
             std::unique_lock<std::mutex> lock(m_mutex);
             if (m_cancelled) {
@@ -404,6 +420,8 @@ bool Tunnel::HandleEvent(Lws *wsi, enum lws_callback_reasons reason, void *in, s
                 m_state = State::FAILED;
                 return true;
             }
+            if (m_state == State::WILL_DISCONNECT)
+                return true;
             // LWS_CALLBACK_EVENT_WAIT_CANCELLED may happen during successful connecting
             if (m_state != State::DISCONNECTED) {
                 LogError("Unexpected event");
@@ -415,7 +433,11 @@ bool Tunnel::HandleEvent(Lws *wsi, enum lws_callback_reasons reason, void *in, s
 
         case LWS_CALLBACK_WSI_DESTROY:
         case LWS_CALLBACK_PROTOCOL_DESTROY:
-            if (m_state == State::DISCONNECTED) {
+            switch (m_state) {
+            case State::CONNECTED:
+            case State::WILL_DISCONNECT:
+            case State::FAILED: break;
+            default:
                 LogError("Unexpected event");
                 m_state = State::FAILED;
                 return true;
@@ -470,6 +492,9 @@ void Tunnel::DisconnectOnError()
     if (m_cancelled) {
         DestroyContext(lock);
         THROW_CANCELLED();
+    } else if (m_state == State::WILL_DISCONNECT) {
+        DestroyContext(lock);
+        throw ExceptionTunnelClosed{};
     } else if (m_state != State::CONNECTED) {
         DestroyContext(lock);
         THROW_INVALID_STATE("Invalid tunnel state");
index a585bfe658169d006640b5e3a286262e25b08c56..571e82e89b4ed1e339d8184d1b49452ac2b9bcf4 100644 (file)
@@ -16,6 +16,7 @@
 
 #pragma once
 
+#include "exception.h"
 #include "websockets.h"
 
 #include <cstdint>
@@ -32,6 +33,10 @@ struct ExtraHttpHeader {
     std::string value;
 };
 
+struct ExceptionTunnelClosed : public Exception::InvalidState {
+    ExceptionTunnelClosed() : Exception{"Tunnel connection closed."} {}
+};
+
 class ITunnel {
 protected:
     ITunnel() = default;
@@ -43,6 +48,8 @@ public:
                          std::optional<ExtraHttpHeader> extraHttpHeader = std::nullopt) = 0;
     virtual void WriteBinary(const std::vector<uint8_t> &msg) = 0;
     virtual std::vector<uint8_t> ReadBinary() = 0;
+    // Make the tunnel close the connection after @p usecs microseconds, no matter what.
+    virtual void CloseConnectionAfter(uint32_t usecs) = 0;
     virtual void Disconnect() = 0;
     virtual void Cancel() = 0;
 };
@@ -59,6 +66,7 @@ public:
                  std::optional<ExtraHttpHeader> extraHttpHeader = std::nullopt) override;
     void WriteBinary(const std::vector<uint8_t> &msg) override;
     std::vector<uint8_t> ReadBinary() override;
+    void CloseConnectionAfter(uint32_t usecs) override;
     void Disconnect() override;
 
     /*
@@ -71,6 +79,7 @@ public:
         DISCONNECTED,
         FAILED,
         CONNECTED,
+        WILL_DISCONNECT,
         WRITING,
     };
 
index ec5bd5cf75fcb089e5acead0fe07ba2c8f355729..4b60864fe747616f550da6619a59034f24900138 100644 (file)
@@ -23,7 +23,10 @@ void wah_make_credential(const wauthn_client_data_s *client_data,
                          const wauthn_pubkey_cred_creation_options_s *options,
                          wauthn_mc_callbacks_s *callbacks)
 {
-    RequestHandler::Instance().Process(client_data, RequestMC{options, callbacks});
+    RequestHandler::Instance().Process(client_data,
+                                       Request<RequestKind::MC>{options, callbacks},
+                                       QrTransaction{},
+                                       StateAssistedTransaction{});
 }
 
 WEBAUTHN_HAL_API
@@ -31,7 +34,10 @@ void wah_get_assertion(const wauthn_client_data_s *client_data,
                        const wauthn_pubkey_cred_request_options_s *options,
                        wauthn_ga_callbacks_s *callbacks)
 {
-    RequestHandler::Instance().Process(client_data, RequestGA{options, callbacks});
+    RequestHandler::Instance().Process(client_data,
+                                       Request<RequestKind::GA>{options, callbacks},
+                                       QrTransaction{},
+                                       StateAssistedTransaction{});
 }
 
 WEBAUTHN_HAL_API
index 2d5445aafe8dc1b8545e393aec58a8f9f1a6d6d7..b7b277a26c48c20514ae6bbc19d04a430976e403 100644 (file)
@@ -179,6 +179,11 @@ int Websockets::WriteBinary(Lws *wsi, unsigned char *buf, size_t len) noexcept
     return lws_write(Lws2Native(wsi), buf, len, LWS_WRITE_BINARY);
 }
 
+void Websockets::SetTimer(Lws *wsi, lws_usec_t usecs) noexcept
+{
+    lws_set_timer_usecs(Lws2Native(wsi), usecs);
+}
+
 void Websockets::ContextDestroy(LwsContext *context) noexcept
 {
     lws_context_destroy(Context2Native(context));
index 6a5ed326a073b579ae5222f79620872b70160d1a..2e45b535c0f9b679b9c7310d3d7909b7073b74fa 100644 (file)
@@ -157,6 +157,15 @@ public:
      */
     virtual int WriteBinary(Lws *wsi, unsigned char *buf, size_t len) noexcept = 0;
 
+    /*
+     * @brief  Schedule LWS_CALLBACK_TIMER event to happen after specified number of microseconds
+     *
+     * @param[in] wsi  The connection to be used to send event to
+     * @param[in] usecs  Number of microseconds after which the event will occur, or
+     *                   LWS_SET_TIMER_USEC_CANCEL to remove existing scheduled event
+     */
+    virtual void SetTimer(Lws *wsi, lws_usec_t usecs) noexcept = 0;
+
     /*
      * @brief  Destroys given context and releases resources and closes connection if necessary
      *
@@ -192,6 +201,7 @@ public:
     bool IsFirstFragment(Lws *wsi) const noexcept override;
     bool IsFinalFragment(Lws *wsi) const noexcept override;
     int WriteBinary(Lws *wsi, unsigned char *buf, size_t len) noexcept override;
+    void SetTimer(Lws *wsi, lws_usec_t usecs) noexcept override;
     void ContextDestroy(LwsContext *context) noexcept override;
 
     void SetListener(IWebsocketsListener *listener) noexcept override { m_listener = listener; }
index f6d6e4772cc732847899ce92b9e57819e832ec21..683a5aa727ca913ce12d00d800ef7b1bfd795a83 100644 (file)
@@ -51,7 +51,6 @@ LINK_DIRECTORIES(${TESTS_DEPS_LIBRARY_DIRS})
 SET(UNIT_TESTS_SOURCES
     ${WEBAUTHN_BLE_SOURCES}
     ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
-    ${CMAKE_CURRENT_SOURCE_DIR}/api_tests.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/bluetooth_tests.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/cbor_tests.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/turn_bluetooth.cpp
@@ -66,6 +65,7 @@ SET(UNIT_TESTS_SOURCES
     ${CMAKE_CURRENT_SOURCE_DIR}/message_tests.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/ctap_message_processor_tests.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/qr_code_shower_tests.cpp
+    ${CMAKE_CURRENT_SOURCE_DIR}/request_handler_tests.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/crypto/hkdf_unittest.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/crypto/hmac_unittest.cpp
     ${CMAKE_CURRENT_SOURCE_DIR}/crypto/sha2_unittest.cpp
diff --git a/tests/api_tests.cpp b/tests/api_tests.cpp
deleted file mode 100644 (file)
index 451ba2a..0000000
+++ /dev/null
@@ -1,313 +0,0 @@
-/*
- *  Copyright (c) 2023 Samsung Electronics Co., Ltd All Rights Reserved
- *
- *  Licensed under the Apache License, Version 2.0 (the "License");
- *  you may not use this file except in compliance with the License.
- *  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing, software
- *  distributed under the License is distributed on an "AS IS" BASIS,
- *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- *  See the License for the specific language governing permissions and
- *  limitations under the License
- */
-
-#include "bluetooth_advert.h"
-#include "crypto/ec_key.h"
-#include "crypto/random.h"
-#include "exception.h"
-#include "qr_code_shower.h"
-#include "request_handler.h"
-
-#include <cstring>
-#include <functional>
-#include <gtest/gtest.h>
-#include <stdexcept>
-#include <utility>
-#include <webauthn-hal.h>
-
-namespace {
-
-template <typename... Args>
-class CallbackInfo {
-public:
-    void Reset()
-    {
-        m_calls = 0;
-        m_argsCheckOk = false;
-    }
-
-    void Call(const Args... args)
-    {
-        m_calls++;
-        if (m_argsChecker)
-            m_argsCheckOk = m_argsChecker(args...);
-    }
-
-    void CalledOnce(int line)
-    {
-        EXPECT_EQ(m_calls, 1) << "line: " << line;
-        if (m_argsChecker) {
-            EXPECT_TRUE(m_argsCheckOk) << "line: " << line;
-        }
-        Reset();
-    }
-
-    void NotCalled(int line)
-    {
-        EXPECT_EQ(m_calls, 0) << "line: " << line;
-        Reset();
-    }
-
-    void SetArgsChecker(std::function<bool(Args...)> fn) { m_argsChecker = std::move(fn); }
-
-protected:
-    unsigned m_calls;
-    std::function<bool(Args...)> m_argsChecker;
-    bool m_argsCheckOk;
-};
-
-CallbackInfo<const char *, void *> qrCallback;
-CallbackInfo<const wauthn_pubkey_credential_attestation_s *, wauthn_error_e, void *> mcCallback;
-CallbackInfo<const wauthn_pubkey_credential_assertion_s *, wauthn_error_e, void *> gaCallback;
-
-void QrCallback(const char *qr_contents, void *user_data)
-{
-    qrCallback.Call(qr_contents, user_data);
-}
-
-void QrThrowingCallback(const char *qr_contents, void *user_data)
-{
-    qrCallback.Call(qr_contents, user_data);
-    throw Exception::InvalidParam();
-}
-
-void McCallback(const wauthn_pubkey_credential_attestation_s *pubkey_cred,
-                wauthn_error_e result,
-                void *user_data)
-{
-    mcCallback.Call(pubkey_cred, result, user_data);
-}
-
-void GaCallback(const wauthn_pubkey_credential_assertion_s *pubkey_cred,
-                wauthn_error_e result,
-                void *user_data)
-{
-    gaCallback.Call(pubkey_cred, result, user_data);
-}
-
-class MBtAdvertScanner : public IBtAdvertScanner {
-public:
-    void AwaitAdvert(const CryptoBuffer & /*eidKey*/, CryptoBuffer & /*decryptedAdvert*/) override
-    {
-        throw std::runtime_error{"should not be called"};
-    }
-
-    void Cancel() override { throw std::runtime_error{"should not be called"}; }
-};
-
-class MHandshake : public IHandshake {
-public:
-    void QrInitiatedConnect(const std::string &, const BufferView &, const BufferView &) override
-    {
-        throw std::runtime_error{"should not be called"};
-    }
-
-    HandshakeResult DoQrInitiatedHandshake(const CryptoBuffer &,
-                                           const Crypto::X9_62_P_256_Key &) override
-    {
-        throw std::runtime_error{"should not be called"};
-    }
-
-    virtual void
-    StateAssistedConnect(const std::string &, const BufferView &, const BufferView &) override
-    {
-        throw std::runtime_error{"should not be called"};
-    }
-
-    HandshakeResult DoStateAssistedHandshake(const CryptoBuffer &, const CryptoBuffer &) override
-    {
-        throw std::runtime_error{"should not be called"};
-    }
-
-    void Cancel() override { throw std::runtime_error{"should not be called"}; }
-};
-
-class MCtapMessageProcessor : public ICtapMessageProcessor {
-    void SetEncryptedTunnel(std::unique_ptr<IEncryptedTunnel>) override
-    {
-        throw std::runtime_error{"should not be called"};
-    }
-
-    MCResult MakeCredential(const wauthn_client_data_s &,
-                            const wauthn_pubkey_cred_creation_options_s &) override
-    {
-        throw std::runtime_error{"should not be called"};
-    }
-
-    GAResult GetAssertion(const wauthn_client_data_s &,
-                          const wauthn_pubkey_cred_request_options_s &) override
-    {
-        throw std::runtime_error{"should not be called"};
-    }
-
-    void Cancel() override { throw std::runtime_error{"should not be called"}; }
-};
-
-void mocked_wah_make_credential(const wauthn_client_data_s *client_data,
-                                const wauthn_pubkey_cred_creation_options_s *options,
-                                wauthn_mc_callbacks_s *callbacks)
-{
-    RequestHandler::Instance().Process(client_data,
-                                       RequestMC{options, callbacks},
-                                       std::make_unique<QrCodeShower>(),
-                                       std::make_unique<MBtAdvertScanner>(),
-                                       std::make_unique<MHandshake>(),
-                                       std::make_unique<MCtapMessageProcessor>());
-}
-
-void mocked_wah_get_assertion(const wauthn_client_data_s *client_data,
-                              const wauthn_pubkey_cred_request_options_s *options,
-                              wauthn_ga_callbacks_s *callbacks)
-{
-    RequestHandler::Instance().Process(client_data,
-                                       RequestGA{options, callbacks},
-                                       std::make_unique<QrCodeShower>(),
-                                       std::make_unique<MBtAdvertScanner>(),
-                                       std::make_unique<MHandshake>(),
-                                       std::make_unique<MCtapMessageProcessor>());
-}
-
-void InvokeMc(const wauthn_client_data_s *clientData,
-              const wauthn_pubkey_cred_creation_options_s *options,
-              wauthn_cb_display_qrcode qrcodeCallback,
-              wauthn_cb_mc_on_response responseCallback,
-              void *userData)
-{
-    wauthn_mc_callbacks_s mcCb;
-    mcCb.qrcode_callback = qrcodeCallback;
-    mcCb.response_callback = responseCallback;
-    mcCb.user_data = userData;
-
-    mocked_wah_make_credential(clientData, options, &mcCb);
-}
-
-void InvokeGa(const wauthn_client_data_s *clientData,
-              const wauthn_pubkey_cred_request_options_s *options,
-              wauthn_cb_display_qrcode qrcodeCallback,
-              wauthn_cb_ga_on_response responseCallback,
-              void *userData)
-{
-    wauthn_ga_callbacks_s gaCb;
-    gaCb.qrcode_callback = qrcodeCallback;
-    gaCb.response_callback = responseCallback;
-    gaCb.user_data = userData;
-
-    mocked_wah_get_assertion(clientData, options, &gaCb);
-}
-
-template <typename T>
-std::function<bool(const T *, wauthn_error_e, void *)> invalidParamChecker(void *userData)
-{
-    return [=](const T *pubkey_cred, wauthn_error_e result, void *user_data) {
-        return pubkey_cred == nullptr && result == WAUTHN_ERROR_INVALID_PARAMETER &&
-            user_data == userData;
-    };
-}
-
-} // namespace
-
-TEST(ApiTest, testInvalidArguments)
-{
-    wauthn_client_data_s client;
-    wauthn_pubkey_cred_creation_options_s mcOptions;
-    wauthn_pubkey_cred_request_options_s gaOptions;
-    size_t userData = 123'456;
-
-    memset(&client, 0, sizeof client);
-    memset(&mcOptions, 0, sizeof mcOptions);
-    memset(&gaOptions, 0, sizeof gaOptions);
-
-    auto mcInvalid = invalidParamChecker<wauthn_pubkey_credential_attestation_s>(&userData);
-    auto gaInvalid = invalidParamChecker<wauthn_pubkey_credential_assertion_s>(&userData);
-
-    mocked_wah_make_credential(&client, &mcOptions, nullptr);
-    InvokeMc(&client, &mcOptions, nullptr, nullptr, nullptr);
-    qrCallback.NotCalled(__LINE__);
-    mcCallback.NotCalled(__LINE__);
-    gaCallback.NotCalled(__LINE__);
-
-    mcCallback.SetArgsChecker(mcInvalid);
-
-    InvokeMc(nullptr, &mcOptions, QrCallback, McCallback, &userData);
-    qrCallback.NotCalled(__LINE__);
-    mcCallback.CalledOnce(__LINE__);
-    gaCallback.NotCalled(__LINE__);
-
-    InvokeMc(&client, nullptr, QrCallback, McCallback, &userData);
-    qrCallback.NotCalled(__LINE__);
-    mcCallback.CalledOnce(__LINE__);
-    gaCallback.NotCalled(__LINE__);
-
-    mocked_wah_get_assertion(&client, &gaOptions, nullptr);
-    InvokeGa(&client, &gaOptions, nullptr, nullptr, nullptr);
-    qrCallback.NotCalled(__LINE__);
-    mcCallback.NotCalled(__LINE__);
-    gaCallback.NotCalled(__LINE__);
-
-    gaCallback.SetArgsChecker(gaInvalid);
-
-    InvokeGa(nullptr, &gaOptions, QrCallback, GaCallback, &userData);
-    qrCallback.NotCalled(__LINE__);
-    mcCallback.NotCalled(__LINE__);
-    gaCallback.CalledOnce(__LINE__);
-
-    InvokeGa(&client, nullptr, QrCallback, GaCallback, &userData);
-    qrCallback.NotCalled(__LINE__);
-    mcCallback.NotCalled(__LINE__);
-    gaCallback.CalledOnce(__LINE__);
-
-    EXPECT_EQ(wah_cancel(), WAUTHN_ERROR_NOT_ALLOWED);
-}
-
-TEST(ApiTest, testQrCallback)
-{
-    wauthn_client_data_s client;
-    wauthn_pubkey_cred_creation_options_s mcOptions;
-    wauthn_pubkey_cred_request_options_s gaOptions;
-    size_t userData = 123'456;
-
-    memset(&client, 0, sizeof client);
-    memset(&mcOptions, 0, sizeof mcOptions);
-    memset(&gaOptions, 0, sizeof gaOptions);
-
-    auto qrCheck = [&](const char *qr_contents, void *user_data) {
-        static const std::string PREFIX = "FIDO:/";
-
-        if (strncmp(qr_contents, PREFIX.c_str(), PREFIX.size()) != 0)
-            return false;
-
-        return &userData == user_data;
-    };
-
-    auto mcInvalid = invalidParamChecker<wauthn_pubkey_credential_attestation_s>(&userData);
-    auto gaInvalid = invalidParamChecker<wauthn_pubkey_credential_assertion_s>(&userData);
-
-    qrCallback.SetArgsChecker(qrCheck);
-    mcCallback.SetArgsChecker(mcInvalid);
-    gaCallback.SetArgsChecker(gaInvalid);
-
-    // Use throwing qr callback to stop further processing
-
-    InvokeMc(&client, &mcOptions, QrThrowingCallback, McCallback, &userData);
-    qrCallback.CalledOnce(__LINE__);
-    mcCallback.CalledOnce(__LINE__);
-    gaCallback.NotCalled(__LINE__);
-
-    InvokeGa(&client, &gaOptions, QrThrowingCallback, GaCallback, &userData);
-    qrCallback.CalledOnce(__LINE__);
-    mcCallback.NotCalled(__LINE__);
-    gaCallback.CalledOnce(__LINE__);
-}
diff --git a/tests/buffer.h b/tests/buffer.h
new file mode 100644 (file)
index 0000000..1811a4d
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+#pragma once
+
+#include "common.h"
+
+inline Buffer ToBuffer(const BufferView &bv) { return Buffer(bv.begin(), bv.end()); }
+
+// Use with string literals only, because it assumes the input array to be null-terminated.
+template <size_t N>
+Buffer BUFFER(const char (&data)[N])
+{
+    return Buffer{reinterpret_cast<const uint8_t *>(data),
+                  reinterpret_cast<const uint8_t *>(data) + N - 1};
+}
diff --git a/tests/buffer_view.h b/tests/buffer_view.h
new file mode 100644 (file)
index 0000000..89009f5
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+#pragma once
+
+#include "common.h"
+
+template <class T>
+BufferView ToBufferView(T &buffer) noexcept
+{
+    static_assert(sizeof(decltype(*buffer.data())) == 1, "for reinterpret_cast below");
+    return BufferView{reinterpret_cast<const uint8_t *>(buffer.data()), buffer.size()};
+}
+
+// Use with string literals only, because it assumes the input array to be null-terminated.
+template <size_t N>
+BufferView BUFFER_VIEW(const char (&data)[N])
+{
+    return BufferView(reinterpret_cast<const uint8_t *>(data), N - 1);
+}
index 6c85591692b1699cc734474f57c7a21517a4e512..07ceaff31cb926e154365c59e9fd94724a0f8406 100644 (file)
  *  limitations under the License
  */
 
+#include "buffer.h"
+#include "buffer_view.h"
 #include "ctap_message_processor.h"
 #include "exception.h"
+#include "message_examples.h"
 #include "test_cancel_from_the_other_thread.h"
 #include "to_wauthn_const_buff.h"
 
@@ -27,7 +30,7 @@ namespace {
 
 class MEncryptedTunnel : public IEncryptedTunnel {
 public:
-    explicit MEncryptedTunnel(std::vector<CryptoBuffer> messagesToRead)
+    explicit MEncryptedTunnel(std::vector<CryptoBuffer> &&messagesToRead)
     : m_messagesToRead{std::move(messagesToRead)}
     {
     }
@@ -47,11 +50,13 @@ public:
             EXPECT_EQ(m_nextMsgToRead, 1); // Writing after reading GetInfo
         } else {
             EXPECT_EQ(m_nextMsgToRead,
-                      m_messagesToRead.size()); // Writting after reading everything
+                      m_messagesToRead.size()); // Writing after reading everything
         }
         m_writtenMessages.emplace_back(std::move(msg));
     }
 
+    void CloseConnectionAfter(uint32_t) override {}
+
     void Cancel() override
     {
         ADD_FAILURE() << "This should not be called";
@@ -135,18 +140,6 @@ auto WithDefaultGAArgs(Func &&func, const BufferView &credentialIdBytes)
     return std::forward<Func>(func)(clientData, options);
 }
 
-const auto IPHONE_POST_HANDSHAKE_RESPONSE = BUFFER_VIEW(
-    "\xa1\x01\x58\x4f\xa5\x01\x82\x68\x46\x49\x44\x4f\x5f\x32\x5f\x30\x68\x46\x49\x44\x4f"
-    "\x5f\x32\x5f\x31\x02\x81\x69\x6c\x61\x72\x67\x65\x42\x6c\x6f\x62\x03\x50\xf2\x4a\x8e"
-    "\x70\xd0\xd3\xf8\x2c\x29\x37\x32\x52\x3c\xc4\xde\x5a\x04\xa2\x62\x72\x6b\xf5\x62\x75"
-    "\x76\xf5\x09\x82\x68\x69\x6e\x74\x65\x72\x6e\x61\x6c\x66\x68\x79\x62\x72\x69\x64");
-
-const auto ANDROID_POST_HANDSHAKE_RESPONSE = BUFFER_VIEW(
-    "\xa1\x01\x58\x53\xa5\x01\x82\x68\x46\x49\x44\x4f\x5f\x32\x5f\x30\x68\x46\x49\x44\x4f\x5f"
-    "\x32\x5f\x31\x02\x81\x63\x70\x72\x66\x03\x50\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-    "\x00\x00\x00\x00\x00\x04\xa4\x62\x72\x6b\xf5\x62\x75\x70\xf5\x62\x75\x76\xf5\x64\x70\x6c"
-    "\x61\x74\xf4\x09\x82\x68\x69\x6e\x74\x65\x72\x6e\x61\x6c\x66\x68\x79\x62\x72\x69\x64");
-
 void CheckIphonePostHandshakeResponse(const PostHandshakeResponse &phr)
 {
     EXPECT_EQ(phr.m_versions, (std::vector<std::string>{"FIDO_2_0", "FIDO_2_1"}));
@@ -212,52 +205,25 @@ void CheckAndroidPostHandshakeResponse(const PostHandshakeResponse &phr)
 void CheckAndroidUpdateMessage(const UpdateMessage &upMsg, const BufferView &rawUpdateMsg)
 {
     ASSERT_NE(upMsg.GetLinkData(), std::nullopt);
-    EXPECT_EQ(ToBufferView(upMsg.GetLinkData()->m_contactId), rawUpdateMsg.substr(174, 152));
-    EXPECT_EQ(ToBufferView(upMsg.GetLinkData()->m_linkId), rawUpdateMsg.substr(328, 8));
-    EXPECT_EQ(ToBufferView(upMsg.GetLinkData()->m_linkSecret), rawUpdateMsg.substr(339, 32));
+    EXPECT_EQ(ToBufferView(upMsg.GetLinkData()->m_contactId), rawUpdateMsg.substr(185, 152));
+    EXPECT_EQ(ToBufferView(upMsg.GetLinkData()->m_linkId), rawUpdateMsg.substr(339, 8));
+    EXPECT_EQ(ToBufferView(upMsg.GetLinkData()->m_linkSecret), rawUpdateMsg.substr(350, 32));
     EXPECT_EQ(ToBufferView(upMsg.GetLinkData()->m_authenticatorPublicKey),
-              rawUpdateMsg.substr(374, 65));
-    EXPECT_EQ(ToBufferView(upMsg.GetLinkData()->m_authenticatorName), rawUpdateMsg.substr(441, 16));
+              rawUpdateMsg.substr(385, 65));
+    EXPECT_EQ(ToBufferView(upMsg.GetLinkData()->m_authenticatorName), rawUpdateMsg.substr(452, 5));
     EXPECT_EQ(ToBufferView(upMsg.GetLinkData()->m_handshakeSignature),
               rawUpdateMsg.substr(460, 32));
 }
 
-const auto IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_REQUEST = BUFFER_VIEW(
-    "\x01\x01\xa4\x01\x58\x20\x44\x13\x6f\xa3\x55\xb3\x67\x8a\x11\x46\xad\x16\xf7\xe8\x64\x9e"
-    "\x94\xfb\x4f\xc2\x1f\xe7\x7e\x83\x10\xc0\x60\xf6\x1c\xaa\xff\x8a\x02\xa2\x62\x69\x64\x68"
-    "\x61\x63\x6d\x65\x2e\x63\x6f\x6d\x64\x6e\x61\x6d\x65\x64\x41\x63\x6d\x65\x03\xa3\x62\x69"
-    "\x64\x48\x00\x01\x02\x03\x04\x05\x06\x07\x64\x6e\x61\x6d\x65\x69\x75\x73\x65\x72\x20\x6e"
-    "\x61\x6d\x65\x6b\x64\x69\x73\x70\x6c\x61\x79\x4e\x61\x6d\x65\x71\x75\x73\x65\x72\x20\x64"
-    "\x69\x73\x70\x6c\x61\x79\x20\x6e\x61\x6d\x65\x04\x82\xa2\x63\x61\x6c\x67\x26\x64\x74\x79"
-    "\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\xa2\x63\x61\x6c\x67\x39\x01\x00\x64"
-    "\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79");
-
-const auto IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE = BUFFER_VIEW(
-    "\x01\x00\xa4\x01\x64\x6e\x6f\x6e\x65\x02\x58\x98\x11\x94\x22\x8d\xa8\xfd\xbd\xee\xfd"
-    "\x26\x1b\xd7\xb6\x59\x5c\xfd\x70\xa5\x0d\x70\xc6\x40\x7b\xcf\x01\x3d\xe9\x6d\x4e\xfb"
-    "\x17\xde\x5d\x00\x00\x00\x00\xfb\xfc\x30\x07\x15\x4e\x4e\xcc\x8c\x0b\x6e\x02\x05\x57"
-    "\xd7\xbd\x00\x14\x7c\xe0\x64\xdc\xae\x09\x40\x6a\x9a\x2f\xae\x13\x5e\x01\x63\x4a\xbd"
-    "\x50\xb5\xd2\xa5\x01\x02\x03\x26\x20\x01\x21\x58\x20\x70\x9a\xdb\x3b\x95\x96\x6b\xc0"
-    "\x9c\x68\x31\xf2\xf7\x38\xfe\x19\x1d\x7c\x55\xb7\xbf\xde\xb7\x11\x5a\x5e\xab\xef\x10"
-    "\xaf\xb4\x6d\x22\x58\x20\xab\x15\xf0\x1d\x37\x94\xcb\x9b\xaf\xd3\x92\xc3\x0a\x07\x03"
-    "\x82\xa8\xc7\xc5\x88\x8e\xed\xdf\xaf\xb0\x80\x39\x4d\x4e\x67\x8c\xd1\x03\xa0\x06\xa0");
-
-const auto IPHONE_EXAMPLE_MAKE_CREDENTIAL_RESPONSE_PUBLIC_KEY_DER = BUFFER_VIEW(
-    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48\xce\x3d\x03\x01"
-    "\x07\x03\x42\x00\x04\x70\x9a\xdb\x3b\x95\x96\x6b\xc0\x9c\x68\x31\xf2\xf7\x38\xfe\x19\x1d"
-    "\x7c\x55\xb7\xbf\xde\xb7\x11\x5a\x5e\xab\xef\x10\xaf\xb4\x6d\xab\x15\xf0\x1d\x37\x94\xcb"
-    "\x9b\xaf\xd3\x92\xc3\x0a\x07\x03\x82\xa8\xc7\xc5\x88\x8e\xed\xdf\xaf\xb0\x80\x39\x4d\x4e"
-    "\x67\x8c\xd1");
-
 const auto CTAP_SHUTDOWN_MESSAGE = BUFFER_VIEW("\x00");
 
 } // namespace
 
 TEST(CtapMessageProcessor, IphoneExampleMakeCredential)
 {
-    auto rawMCResp = IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE;
+    auto rawMCResp = BUFFER_VIEW(IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE);
     auto tunnelUPtr = std::make_unique<MEncryptedTunnel>(std::vector<CryptoBuffer>{
-        ToBuffer(IPHONE_POST_HANDSHAKE_RESPONSE),
+        BUFFER(IPHONE_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE),
         ToBuffer(rawMCResp),
     });
     auto tunnel = tunnelUPtr.get();
@@ -270,7 +236,7 @@ TEST(CtapMessageProcessor, IphoneExampleMakeCredential)
     // Sent messages
     ASSERT_EQ(tunnel->WrittenMessages().size(), 2);
     EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[0]),
-              IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_REQUEST);
+              BUFFER_VIEW(IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_REQUEST));
     EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[1]), CTAP_SHUTDOWN_MESSAGE);
     // res.getInfo
     CheckIphonePostHandshakeResponse(res.getInfo);
@@ -283,7 +249,7 @@ TEST(CtapMessageProcessor, IphoneExampleMakeCredential)
     ASSERT_NE(res.response.m_authData.m_attestationData, std::nullopt);
     EXPECT_EQ(res.response.m_authData.m_attestationData->m_credentialId, rawMCResp.substr(67, 20));
     EXPECT_EQ(ToBufferView(res.response.m_authData.m_attestationData->m_publicKeyDer),
-              IPHONE_EXAMPLE_MAKE_CREDENTIAL_RESPONSE_PUBLIC_KEY_DER);
+              BUFFER_VIEW(IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE_PUBLIC_KEY_DER));
     EXPECT_EQ(res.response.m_authData.m_attestationData->m_alg,
               WAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256);
     EXPECT_EQ(ToBufferView(res.response.m_attestationObject), rawMCResp.substr(2));
@@ -292,43 +258,11 @@ TEST(CtapMessageProcessor, IphoneExampleMakeCredential)
     EXPECT_EQ(res.response.m_largeBlobKey, Buffer{});
 }
 
-namespace {
-
-const auto ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_REQUEST = BUFFER_VIEW(
-    "\x01\x01\xa4\x01\x58\x20\x44\x13\x6f\xa3\x55\xb3\x67\x8a\x11\x46\xad\x16\xf7\xe8\x64\x9e"
-    "\x94\xfb\x4f\xc2\x1f\xe7\x7e\x83\x10\xc0\x60\xf6\x1c\xaa\xff\x8a\x02\xa2\x62\x69\x64\x68"
-    "\x61\x63\x6d\x65\x2e\x63\x6f\x6d\x64\x6e\x61\x6d\x65\x64\x41\x63\x6d\x65\x03\xa3\x62\x69"
-    "\x64\x48\x00\x01\x02\x03\x04\x05\x06\x07\x64\x6e\x61\x6d\x65\x69\x75\x73\x65\x72\x20\x6e"
-    "\x61\x6d\x65\x6b\x64\x69\x73\x70\x6c\x61\x79\x4e\x61\x6d\x65\x71\x75\x73\x65\x72\x20\x64"
-    "\x69\x73\x70\x6c\x61\x79\x20\x6e\x61\x6d\x65\x04\x82\xa2\x63\x61\x6c\x67\x26\x64\x74\x79"
-    "\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\xa2\x63\x61\x6c\x67\x39\x01\x00\x64"
-    "\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79");
-
-const auto ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE = BUFFER_VIEW(
-    "\x01\x00\xa3\x01\x64\x6e\x6f\x6e\x65\x02\x58\xa4\x11\x94\x22\x8d\xa8\xfd\xbd\xee\xfd\x26"
-    "\x1b\xd7\xb6\x59\x5c\xfd\x70\xa5\x0d\x70\xc6\x40\x7b\xcf\x01\x3d\xe9\x6d\x4e\xfb\x17\xde"
-    "\x45\x00\x00\x00\x00\x53\x41\x4d\x53\x55\x4e\x47\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-    "\x20\xde\x2f\x46\x7f\xb4\xf6\xc7\xbe\x49\x42\xdb\x80\xa4\x3d\xcd\xf2\x1b\xc9\xf7\x31\x54"
-    "\x9c\x7b\x58\xc8\x2b\x22\x46\x1d\x17\x43\x1d\xa5\x01\x02\x03\x26\x20\x01\x21\x58\x20\x9f"
-    "\x80\x80\xc9\xac\xf9\x92\x66\x12\x3a\xcf\x0e\xf8\x7e\x5c\x3f\xbf\xb4\x34\xc8\x96\xa0\xe2"
-    "\x54\xbe\xeb\x8c\x62\xb2\x5d\x54\xc8\x22\x58\x20\xc2\x9f\x0d\xc8\x8d\x81\x8f\x57\x51\x46"
-    "\x93\x31\xc0\x59\xce\x13\x5f\xbb\x3d\x65\x39\xc2\x89\x68\x77\x52\xb8\x80\x0c\x86\xf9\xd6"
-    "\x03\xa0");
-
-const auto ANDROID_EXAMPLE_MAKE_CREDENTIAL_RESPONSE_PUBLIC_KEY_DER = BUFFER_VIEW(
-    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48\xce\x3d\x03\x01"
-    "\x07\x03\x42\x00\x04\x9f\x80\x80\xc9\xac\xf9\x92\x66\x12\x3a\xcf\x0e\xf8\x7e\x5c\x3f\xbf"
-    "\xb4\x34\xc8\x96\xa0\xe2\x54\xbe\xeb\x8c\x62\xb2\x5d\x54\xc8\xc2\x9f\x0d\xc8\x8d\x81\x8f"
-    "\x57\x51\x46\x93\x31\xc0\x59\xce\x13\x5f\xbb\x3d\x65\x39\xc2\x89\x68\x77\x52\xb8\x80\x0c"
-    "\x86\xf9\xd6");
-
-} // namespace
-
 TEST(CtapMessageProcessor, AndroidExampleMakeCredentialWithoutUpdateMessage)
 {
-    auto rawMCResp = ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE;
+    auto rawMCResp = BUFFER_VIEW(ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE);
     auto tunnelUPtr = std::make_unique<MEncryptedTunnel>(std::vector<CryptoBuffer>{
-        ToBuffer(ANDROID_POST_HANDSHAKE_RESPONSE),
+        BUFFER(ANDROID_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE),
         ToBuffer(rawMCResp),
     });
     auto tunnel = tunnelUPtr.get();
@@ -341,7 +275,7 @@ TEST(CtapMessageProcessor, AndroidExampleMakeCredentialWithoutUpdateMessage)
     // Sent messages
     ASSERT_EQ(tunnel->WrittenMessages().size(), 2);
     EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[0]),
-              ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_REQUEST);
+              BUFFER_VIEW(ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_REQUEST));
     EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[1]), CTAP_SHUTDOWN_MESSAGE);
     // res.getInfo
     CheckAndroidPostHandshakeResponse(res.getInfo);
@@ -354,7 +288,7 @@ TEST(CtapMessageProcessor, AndroidExampleMakeCredentialWithoutUpdateMessage)
     ASSERT_NE(res.response.m_authData.m_attestationData, std::nullopt);
     EXPECT_EQ(res.response.m_authData.m_attestationData->m_credentialId, rawMCResp.substr(67, 32));
     EXPECT_EQ(ToBufferView(res.response.m_authData.m_attestationData->m_publicKeyDer),
-              ANDROID_EXAMPLE_MAKE_CREDENTIAL_RESPONSE_PUBLIC_KEY_DER);
+              BUFFER_VIEW(ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE_PUBLIC_KEY_DER));
     EXPECT_EQ(res.response.m_authData.m_attestationData->m_alg,
               WAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256);
     EXPECT_EQ(ToBufferView(res.response.m_attestationObject), rawMCResp.substr(2));
@@ -363,41 +297,12 @@ TEST(CtapMessageProcessor, AndroidExampleMakeCredentialWithoutUpdateMessage)
     EXPECT_EQ(res.response.m_largeBlobKey, Buffer{});
 }
 
-namespace {
-
-const auto ANDROID_EXAMPLE_RAW_UPDATE_MSG = BUFFER_VIEW(
-    "\x02\xa2\x00\x58\xa4\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xa7\x01\x58\x98\x63\x76"
-    "\x33\x31\x4f\x51\x6b\x6a\x50\x4c\x73\x3a\x41\x50\x41\x39\x31\x62\x47\x6a\x54\x72\x41\x38"
-    "\x33\x33\x61\x57\x62\x42\x43\x49\x79\x58\x73\x74\x78\x43\x30\x36\x52\x71\x43\x78\x35\x43"
-    "\x34\x61\x76\x33\x65\x37\x57\x4d\x6a\x39\x77\x64\x59\x44\x67\x64\x6d\x48\x69\x4e\x48\x4a"
-    "\x30\x68\x4f\x36\x4a\x46\x78\x5f\x39\x69\x5a\x6d\x68\x53\x55\x30\x62\x6f\x6c\x61\x6d\x71"
-    "\x63\x50\x33\x59\x6b\x71\x66\x6e\x38\x4d\x55\x65\x74\x4e\x35\x4c\x58\x73\x45\x6d\x6f\x55"
-    "\x55\x72\x56\x68\x4a\x56\x51\x33\x48\x5a\x38\x68\x6c\x61\x6c\x53\x44\x4c\x61\x4e\x6d\x6c"
-    "\x77\x38\x41\x51\x70\x79\x65\x67\x6b\x5a\x34\x65\x61\x73\x78\x73\x62\x6f\x02\x48\x9c\x23"
-    "\xe9\xd7\x3e\x9a\x8b\x74\x03\x58\x20\xc5\x55\xf3\x1b\xe9\x76\xc7\x2b\x3f\x7f\x42\xd4\x19"
-    "\x99\x06\x73\x2d\xde\xa3\x19\xd0\x07\x83\xae\xa8\x09\xab\x38\x9c\xd7\x46\x4f\x04\x58\x41"
-    "\x04\x61\x65\xd4\xff\x17\x71\x10\x23\xc6\xc9\x75\x58\x87\xf6\x32\x1f\x51\x4d\x22\xe1\xbe"
-    "\x70\xdd\xe6\x7e\x7b\x39\x63\xf1\x38\x38\x7b\x39\x4f\xad\x8b\xc6\xb7\x05\x6d\xa7\x3f\xe3"
-    "\x94\x7c\x87\x52\x8d\x53\x1f\xdb\x70\xc0\x2b\x5d\xc9\xc1\xe2\xd8\x78\x1b\x1f\xbd\x3a\x05"
-    "\x70\x47\x61\x6c\x61\x78\x79\x20\x53\x32\x34\x20\x55\x6c\x74\x72\x61\x06\x58\x20\x53\xb3"
-    "\x57\x6b\xc6\x84\xeb\x1e\xdb\x50\xde\xad\x42\xc9\xef\x6c\xd3\x3b\x82\xb2\xa5\xfa\x76\x28"
-    "\x0f\xc1\x57\x5f\x57\xdc\x0c\x63\x19\x03\xe7\xf5");
-
-} // namespace
-
-TEST(CtapMessageProcessor, AndroidExampleMakeCredentialWitUpdateMessage)
+TEST(CtapMessageProcessor, AndroidExampleMakeCredentialWithUpdateMessage)
 {
-    auto rawUpdateMsg = ANDROID_EXAMPLE_RAW_UPDATE_MSG;
-    auto rawMCResp = ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE;
+    auto rawUpdateMsg = BUFFER_VIEW(ANDROID_EXAMPLE_RAW_UPDATE_MSG);
+    auto rawMCResp = BUFFER_VIEW(ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE);
     auto tunnelUPtr = std::make_unique<MEncryptedTunnel>(std::vector<CryptoBuffer>{
-        ToBuffer(ANDROID_POST_HANDSHAKE_RESPONSE),
+        BUFFER(ANDROID_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE),
         ToBuffer(rawUpdateMsg),
         ToBuffer(rawMCResp),
     });
@@ -411,7 +316,7 @@ TEST(CtapMessageProcessor, AndroidExampleMakeCredentialWitUpdateMessage)
     // Sent messages
     ASSERT_EQ(tunnel->WrittenMessages().size(), 2);
     EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[0]),
-              ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_REQUEST);
+              BUFFER_VIEW(ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_REQUEST));
     EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[1]), CTAP_SHUTDOWN_MESSAGE);
     // res.getInfo
     CheckAndroidPostHandshakeResponse(res.getInfo);
@@ -424,7 +329,7 @@ TEST(CtapMessageProcessor, AndroidExampleMakeCredentialWitUpdateMessage)
     ASSERT_NE(res.response.m_authData.m_attestationData, std::nullopt);
     EXPECT_EQ(res.response.m_authData.m_attestationData->m_credentialId, rawMCResp.substr(67, 32));
     EXPECT_EQ(ToBufferView(res.response.m_authData.m_attestationData->m_publicKeyDer),
-              ANDROID_EXAMPLE_MAKE_CREDENTIAL_RESPONSE_PUBLIC_KEY_DER);
+              BUFFER_VIEW(ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE_PUBLIC_KEY_DER));
     EXPECT_EQ(res.response.m_authData.m_attestationData->m_alg,
               WAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256);
     EXPECT_EQ(ToBufferView(res.response.m_attestationObject), rawMCResp.substr(2));
@@ -433,37 +338,11 @@ TEST(CtapMessageProcessor, AndroidExampleMakeCredentialWitUpdateMessage)
     EXPECT_EQ(res.response.m_largeBlobKey, Buffer{});
 }
 
-namespace {
-
-const auto IPHONE_EXAMPLE_GET_ASSERTION_RAW_REQUEST = BUFFER_VIEW(
-    "\x01\x02\xa4\x01\x68\x61\x63\x6d\x65\x2e\x63\x6f\x6d\x02\x58\x20\x44\x13\x6f\xa3\x55\xb3"
-    "\x67\x8a\x11\x46\xad\x16\xf7\xe8\x64\x9e\x94\xfb\x4f\xc2\x1f\xe7\x7e\x83\x10\xc0\x60\xf6"
-    "\x1c\xaa\xff\x8a\x03\x81\xa3\x62\x69\x64\x54\xef\x42\x5d\x64\xe3\xed\xe0\xaf\x1e\x44\xe8"
-    "\xd4\xf3\xd7\x8c\xa0\x93\x3c\x39\xfc\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d"
-    "\x6b\x65\x79\x6a\x74\x72\x61\x6e\x73\x70\x6f\x72\x74\x73\x82\x66\x68\x79\x62\x72\x69\x64"
-    "\x68\x69\x6e\x74\x65\x72\x6e\x61\x6c\x05\xa1\x62\x75\x76\xf5");
-
-const auto IPHONE_EXAMPLE_GET_ASSERTION_RAW_RESPONSE = BUFFER_VIEW(
-    "\x01\x00\xa5\x01\xa2\x62\x69\x64\x54\xef\x42\x5d\x64\xe3\xed\xe0\xaf\x1e\x44\xe8\xd4\xf3"
-    "\xd7\x8c\xa0\x93\x3c\x39\xfc\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65"
-    "\x79\x02\x58\x25\x11\x94\x22\x8d\xa8\xfd\xbd\xee\xfd\x26\x1b\xd7\xb6\x59\x5c\xfd\x70\xa5"
-    "\x0d\x70\xc6\x40\x7b\xcf\x01\x3d\xe9\x6d\x4e\xfb\x17\xde\x1d\x00\x00\x00\x00\x03\x58\x48"
-    "\x30\x46\x02\x21\x00\xf7\x3d\xb9\x8b\xc4\xc4\xc5\x08\xf8\x24\x3e\xdc\x79\x04\x26\x24\x69"
-    "\xb7\xa9\x4a\xa2\x5a\xac\x49\x6c\x72\x3a\xf2\x31\x61\x7b\x4a\x02\x21\x00\xc9\x94\x3c\x24"
-    "\x15\x9e\x94\xee\x55\xb3\xba\xe9\x5c\xfd\xb9\xbd\x08\xc0\xef\x10\x2d\xe9\x75\xef\xcf\x27"
-    "\x1c\x16\x5b\x69\xb9\xe1\x04\xa3\x62\x69\x64\x48\x00\x01\x02\x03\x04\x05\x06\x07\x64\x6e"
-    "\x61\x6d\x65\x60\x6b\x64\x69\x73\x70\x6c\x61\x79\x4e\x61\x6d\x65\x60\x08\xa0");
-
-const auto IPHONE_EXAMPLE_GET_ASSERTION_CREDENTIAL_ID =
-    BUFFER_VIEW("\xef\x42\x5d\x64\xe3\xed\xe0\xaf\x1e\x44\xe8\xd4\xf3\xd7\x8c\xa0\x93\x3c\x39\xfc");
-
-} // namespace
-
 TEST(CtapMessageProcessor, IphoneExampleGetAssertion)
 {
-    auto rawGAResp = IPHONE_EXAMPLE_GET_ASSERTION_RAW_RESPONSE;
+    auto rawGAResp = BUFFER_VIEW(IPHONE_EXAMPLE_GET_ASSERTION_RAW_RESPONSE);
     auto tunnelUPtr = std::make_unique<MEncryptedTunnel>(std::vector<CryptoBuffer>{
-        ToBuffer(IPHONE_POST_HANDSHAKE_RESPONSE),
+        BUFFER(IPHONE_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE),
         ToBuffer(rawGAResp),
     });
     auto tunnel = tunnelUPtr.get();
@@ -474,10 +353,11 @@ TEST(CtapMessageProcessor, IphoneExampleGetAssertion)
         [&](const auto &clientData, const auto &options) {
             return cmp.GetAssertion(clientData, options);
         },
-        IPHONE_EXAMPLE_GET_ASSERTION_CREDENTIAL_ID);
+        BUFFER_VIEW(IPHONE_EXAMPLE_GET_ASSERTION_RAW_RESPONSE_CREDENTIAL_ID));
     // Sent messages
     ASSERT_EQ(tunnel->WrittenMessages().size(), 2);
-    EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[0]), IPHONE_EXAMPLE_GET_ASSERTION_RAW_REQUEST);
+    EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[0]),
+              BUFFER_VIEW(IPHONE_EXAMPLE_GET_ASSERTION_RAW_REQUEST));
     EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[1]), CTAP_SHUTDOWN_MESSAGE);
     // res.getInfo
     CheckIphonePostHandshakeResponse(res.getInfo);
@@ -495,38 +375,11 @@ TEST(CtapMessageProcessor, IphoneExampleGetAssertion)
     EXPECT_EQ(res.response.m_userDisplayName, "");
 }
 
-namespace {
-
-const auto ANDROID_EXAMPLE_GET_ASSERTION_RAW_REQUEST = BUFFER_VIEW(
-    "\x01\x02\xa4\x01\x68\x61\x63\x6d\x65\x2e\x63\x6f\x6d\x02\x58\x20\x44\x13\x6f\xa3\x55\xb3"
-    "\x67\x8a\x11\x46\xad\x16\xf7\xe8\x64\x9e\x94\xfb\x4f\xc2\x1f\xe7\x7e\x83\x10\xc0\x60\xf6"
-    "\x1c\xaa\xff\x8a\x03\x81\xa3\x62\x69\x64\x58\x20\x9e\x39\x33\x8a\xa5\xf2\x71\xee\x29\x8b"
-    "\x9e\xab\x7a\xf2\x04\x73\xce\x0a\x6a\x5d\x1d\x8e\xf7\x14\xa7\x23\x0b\xc3\x44\x87\xea\xcd"
-    "\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\x6a\x74\x72\x61\x6e\x73"
-    "\x70\x6f\x72\x74\x73\x82\x66\x68\x79\x62\x72\x69\x64\x68\x69\x6e\x74\x65\x72\x6e\x61\x6c"
-    "\x05\xa1\x62\x75\x76\xf5");
-
-const auto ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE = BUFFER_VIEW(
-    "\x01\x00\xa3\x01\xa2\x62\x69\x64\x58\x20\x9e\x39\x33\x8a\xa5\xf2\x71\xee\x29\x8b\x9e\xab"
-    "\x7a\xf2\x04\x73\xce\x0a\x6a\x5d\x1d\x8e\xf7\x14\xa7\x23\x0b\xc3\x44\x87\xea\xcd\x64\x74"
-    "\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\x02\x58\x25\x11\x94\x22\x8d\xa8"
-    "\xfd\xbd\xee\xfd\x26\x1b\xd7\xb6\x59\x5c\xfd\x70\xa5\x0d\x70\xc6\x40\x7b\xcf\x01\x3d\xe9"
-    "\x6d\x4e\xfb\x17\xde\x05\x00\x00\x00\x00\x03\x58\x48\x30\x46\x02\x21\x00\xc4\x34\xe3\x98"
-    "\x33\xac\xd5\x9a\x88\x5d\x00\x90\x56\x0f\x10\xde\x7a\x0e\xd0\xd4\x02\x76\xcc\x28\x74\xf0"
-    "\xa9\x39\x7e\x19\x08\x19\x02\x21\x00\xff\xf1\x13\x5b\x28\x75\xce\xe5\x1c\x9b\x1c\xaa\xfe"
-    "\x6e\x41\x5d\x67\x7c\x0c\xee\x64\xe6\x18\xd5\x81\x95\xd1\xae\xc1\xa2\xdb\x5a");
-
-const auto ANDROID_EXAMPLE_GET_ASSERTION_CREDENTIAL_ID =
-    BUFFER_VIEW("\x9e\x39\x33\x8a\xa5\xf2\x71\xee\x29\x8b\x9e\xab\x7a\xf2\x04\x73\xce\x0a"
-                "\x6a\x5d\x1d\x8e\xf7\x14\xa7\x23\x0b\xc3\x44\x87\xea\xcd");
-
-} // namespace
-
 TEST(CtapMessageProcessor, AndroidExampleGetAssertionWithoutUpdate)
 {
-    auto rawGAResp = ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE;
+    auto rawGAResp = BUFFER_VIEW(ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE);
     auto tunnelUPtr = std::make_unique<MEncryptedTunnel>(std::vector<CryptoBuffer>{
-        ToBuffer(ANDROID_POST_HANDSHAKE_RESPONSE),
+        BUFFER(ANDROID_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE),
         ToBuffer(rawGAResp),
     });
     auto tunnel = tunnelUPtr.get();
@@ -537,11 +390,11 @@ TEST(CtapMessageProcessor, AndroidExampleGetAssertionWithoutUpdate)
         [&](const auto &clientData, const auto &options) {
             return cmp.GetAssertion(clientData, options);
         },
-        ANDROID_EXAMPLE_GET_ASSERTION_CREDENTIAL_ID);
+        BUFFER_VIEW(ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE_CREDENTIAL_ID));
     // Sent messages
     ASSERT_EQ(tunnel->WrittenMessages().size(), 2);
     EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[0]),
-              ANDROID_EXAMPLE_GET_ASSERTION_RAW_REQUEST);
+              BUFFER_VIEW(ANDROID_EXAMPLE_GET_ASSERTION_RAW_REQUEST));
     EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[1]), CTAP_SHUTDOWN_MESSAGE);
     // res.getInfo
     CheckAndroidPostHandshakeResponse(res.getInfo);
@@ -559,12 +412,12 @@ TEST(CtapMessageProcessor, AndroidExampleGetAssertionWithoutUpdate)
     EXPECT_EQ(res.response.m_userDisplayName, "");
 }
 
-TEST(CtapMessageProcessor, AndroidExampleGetAssertionWitUpdate)
+TEST(CtapMessageProcessor, AndroidExampleGetAssertionWithUpdate)
 {
-    auto rawUpdateMsg = ANDROID_EXAMPLE_RAW_UPDATE_MSG;
-    auto rawGAResp = ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE;
+    auto rawUpdateMsg = BUFFER_VIEW(ANDROID_EXAMPLE_RAW_UPDATE_MSG);
+    auto rawGAResp = BUFFER_VIEW(ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE);
     auto tunnelUPtr = std::make_unique<MEncryptedTunnel>(std::vector<CryptoBuffer>{
-        ToBuffer(ANDROID_POST_HANDSHAKE_RESPONSE),
+        BUFFER(ANDROID_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE),
         ToBuffer(rawUpdateMsg),
         ToBuffer(rawGAResp),
     });
@@ -576,11 +429,11 @@ TEST(CtapMessageProcessor, AndroidExampleGetAssertionWitUpdate)
         [&](const auto &clientData, const auto &options) {
             return cmp.GetAssertion(clientData, options);
         },
-        ANDROID_EXAMPLE_GET_ASSERTION_CREDENTIAL_ID);
+        BUFFER_VIEW(ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE_CREDENTIAL_ID));
     // Sent messages
     ASSERT_EQ(tunnel->WrittenMessages().size(), 2);
     EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[0]),
-              ANDROID_EXAMPLE_GET_ASSERTION_RAW_REQUEST);
+              BUFFER_VIEW(ANDROID_EXAMPLE_GET_ASSERTION_RAW_REQUEST));
     EXPECT_EQ(ToBufferView(tunnel->WrittenMessages()[1]), CTAP_SHUTDOWN_MESSAGE);
     // res.getInfo
     CheckAndroidPostHandshakeResponse(res.getInfo);
@@ -603,7 +456,7 @@ TEST(CtapMessageProcessor, InvalidParamsForMakeCredential)
     static constexpr auto doTest = [](const auto &clientData, const auto &options) {
         auto cmp = CtapMessageProcessor{};
         cmp.SetEncryptedTunnel(std::make_unique<MEncryptedTunnel>(
-            std::vector<CryptoBuffer>{ToBuffer(IPHONE_POST_HANDSHAKE_RESPONSE)}));
+            std::vector<CryptoBuffer>{BUFFER(IPHONE_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE)}));
         EXPECT_THROW(cmp.MakeCredential(clientData, options), Exception::InvalidParam);
     };
     WithDefaultMCArgs([](auto &clientData, auto &options) {
@@ -665,7 +518,7 @@ TEST(CtapMessageProcessor, InvalidParamsForGetAssertion)
     static constexpr auto doTest = [](const auto &clientData, const auto &options) {
         auto cmp = CtapMessageProcessor{};
         cmp.SetEncryptedTunnel(std::make_unique<MEncryptedTunnel>(
-            std::vector<CryptoBuffer>{ToBuffer(IPHONE_POST_HANDSHAKE_RESPONSE)}));
+            std::vector<CryptoBuffer>{BUFFER(IPHONE_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE)}));
         EXPECT_THROW(cmp.GetAssertion(clientData, options), Exception::InvalidParam);
     };
     WithDefaultGAArgs(
@@ -741,8 +594,8 @@ TEST(CtapMessageProcessor, TooShortReceivedMessageDuringMakeCredentialWithoutUpd
         });
     };
 
-    auto rawPostHandshakeResp = IPHONE_POST_HANDSHAKE_RESPONSE;
-    auto rawMCResp = IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE;
+    auto rawPostHandshakeResp = BUFFER_VIEW(IPHONE_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE);
+    auto rawMCResp = BUFFER_VIEW(IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE);
     for (size_t i = 0; i < rawPostHandshakeResp.size(); ++i)
         test(BufferView{rawPostHandshakeResp.data(), i}, rawMCResp);
     for (size_t i = 0; i < rawMCResp.size(); ++i)
@@ -765,9 +618,9 @@ TEST(CtapMessageProcessor, TooShortReceivedMessageDuringMakeCredentialWithUpdate
         });
     };
 
-    auto rawPostHandshakeResp = ANDROID_POST_HANDSHAKE_RESPONSE;
-    auto rawUpdateMsg = ANDROID_EXAMPLE_RAW_UPDATE_MSG;
-    auto rawMCResp = ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE;
+    auto rawPostHandshakeResp = BUFFER_VIEW(ANDROID_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE);
+    auto rawUpdateMsg = BUFFER_VIEW(ANDROID_EXAMPLE_RAW_UPDATE_MSG);
+    auto rawMCResp = BUFFER_VIEW(ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE);
     for (size_t i = 0; i < rawPostHandshakeResp.size(); ++i)
         test(BufferView{rawPostHandshakeResp.data(), i}, rawUpdateMsg, rawMCResp);
     for (size_t i = 0; i < rawUpdateMsg.size(); ++i)
@@ -788,11 +641,11 @@ TEST(CtapMessageProcessor, TooShortReceivedMessageDuringGetAssertionWithoutUpdat
             [&](const auto &clientData, const auto &options) {
                 EXPECT_THROW(cmp.GetAssertion(clientData, options), Exception::Unknown);
             },
-            ANDROID_EXAMPLE_GET_ASSERTION_CREDENTIAL_ID);
+            BUFFER_VIEW(ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE_CREDENTIAL_ID));
     };
 
-    auto rawPostHandshakeResp = IPHONE_POST_HANDSHAKE_RESPONSE;
-    auto rawGAResp = ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE;
+    auto rawPostHandshakeResp = BUFFER_VIEW(IPHONE_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE);
+    auto rawGAResp = BUFFER_VIEW(ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE);
     for (size_t i = 0; i < rawPostHandshakeResp.size(); ++i)
         test(BufferView{rawPostHandshakeResp.data(), i}, rawGAResp);
     for (size_t i = 0; i < rawGAResp.size(); ++i)
@@ -814,12 +667,12 @@ TEST(CtapMessageProcessor, TooShortReceivedMessageDuringGetAssertionWithUpdate)
             [&](const auto &clientData, const auto &options) {
                 EXPECT_THROW(cmp.GetAssertion(clientData, options), Exception::Unknown);
             },
-            ANDROID_EXAMPLE_GET_ASSERTION_CREDENTIAL_ID);
+            BUFFER_VIEW(ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE_CREDENTIAL_ID));
     };
 
-    auto rawPostHandshakeResp = ANDROID_POST_HANDSHAKE_RESPONSE;
-    auto rawUpdateMsg = ANDROID_EXAMPLE_RAW_UPDATE_MSG;
-    auto rawGAResp = ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE;
+    auto rawPostHandshakeResp = BUFFER_VIEW(ANDROID_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE);
+    auto rawUpdateMsg = BUFFER_VIEW(ANDROID_EXAMPLE_RAW_UPDATE_MSG);
+    auto rawGAResp = BUFFER_VIEW(ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE);
     for (size_t i = 0; i < rawPostHandshakeResp.size(); ++i)
         test(BufferView{rawPostHandshakeResp.data(), i}, rawUpdateMsg, rawGAResp);
     for (size_t i = 0; i < rawUpdateMsg.size(); ++i)
@@ -846,6 +699,8 @@ public:
         m_cancelFacilitator.WithCancelCheck([&] { MEncryptedTunnel::WriteBinary(std::move(msg)); });
     }
 
+    void CloseConnectionAfter(uint32_t) override {}
+
     void Cancel() override { m_cancelFacilitator.Cancel(); }
 
 private:
@@ -857,11 +712,11 @@ private:
 TEST(CtapMessageProcessor, MakeCredential_without_update_message_cancel_from_the_other_thread)
 {
     auto makeCMP = [&] {
-        CtapMessageProcessor cmp;
-        cmp.SetEncryptedTunnel(
+        auto cmp = std::make_unique<CtapMessageProcessor>();
+        cmp->SetEncryptedTunnel(
             std::make_unique<CancellationMEncryptedTunnel>(std::vector<CryptoBuffer>{
-                ToBuffer(IPHONE_POST_HANDSHAKE_RESPONSE),
-                ToBuffer(IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE),
+                BUFFER(IPHONE_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE),
+                BUFFER(IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE),
             }));
         return cmp;
     };
@@ -876,12 +731,12 @@ TEST(CtapMessageProcessor, MakeCredential_without_update_message_cancel_from_the
 TEST(CtapMessageProcessor, MakeCredential_with_update_message_cancel_from_the_other_thread)
 {
     auto makeCMP = [&] {
-        CtapMessageProcessor cmp;
-        cmp.SetEncryptedTunnel(
+        auto cmp = std::make_unique<CtapMessageProcessor>();
+        cmp->SetEncryptedTunnel(
             std::make_unique<CancellationMEncryptedTunnel>(std::vector<CryptoBuffer>{
-                ToBuffer(ANDROID_POST_HANDSHAKE_RESPONSE),
-                ToBuffer(ANDROID_EXAMPLE_RAW_UPDATE_MSG),
-                ToBuffer(ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE),
+                BUFFER(ANDROID_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE),
+                BUFFER(ANDROID_EXAMPLE_RAW_UPDATE_MSG),
+                BUFFER(ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE),
             }));
         return cmp;
     };
@@ -896,11 +751,11 @@ TEST(CtapMessageProcessor, MakeCredential_with_update_message_cancel_from_the_ot
 TEST(CtapMessageProcessor, GetAssertion_without_update_message_cancel_from_the_other_thread)
 {
     auto makeCMP = [&] {
-        CtapMessageProcessor cmp;
-        cmp.SetEncryptedTunnel(
+        auto cmp = std::make_unique<CtapMessageProcessor>();
+        cmp->SetEncryptedTunnel(
             std::make_unique<CancellationMEncryptedTunnel>(std::vector<CryptoBuffer>{
-                ToBuffer(IPHONE_POST_HANDSHAKE_RESPONSE),
-                ToBuffer(IPHONE_EXAMPLE_GET_ASSERTION_RAW_RESPONSE),
+                BUFFER(IPHONE_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE),
+                BUFFER(IPHONE_EXAMPLE_GET_ASSERTION_RAW_RESPONSE),
             }));
         return cmp;
     };
@@ -908,30 +763,160 @@ TEST(CtapMessageProcessor, GetAssertion_without_update_message_cancel_from_the_o
         200, 20, makeCMP, [](ICtapMessageProcessor &cmp) {
             WithDefaultGAArgs([&](const auto &clientData,
                                   const auto &options) { cmp.GetAssertion(clientData, options); },
-                              IPHONE_EXAMPLE_GET_ASSERTION_CREDENTIAL_ID);
+                              BUFFER_VIEW(IPHONE_EXAMPLE_GET_ASSERTION_RAW_RESPONSE_CREDENTIAL_ID));
         });
 }
 
 TEST(CtapMessageProcessor, GetAssertion_with_update_message_cancel_from_the_other_thread)
 {
     auto makeCMP = [&] {
-        CtapMessageProcessor cmp;
-        cmp.SetEncryptedTunnel(
+        auto cmp = std::make_unique<CtapMessageProcessor>();
+        cmp->SetEncryptedTunnel(
             std::make_unique<CancellationMEncryptedTunnel>(std::vector<CryptoBuffer>{
-                ToBuffer(ANDROID_POST_HANDSHAKE_RESPONSE),
-                ToBuffer(ANDROID_EXAMPLE_RAW_UPDATE_MSG),
-                ToBuffer(ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE),
+                BUFFER(ANDROID_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE),
+                BUFFER(ANDROID_EXAMPLE_RAW_UPDATE_MSG),
+                BUFFER(ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE),
             }));
         return cmp;
     };
     TestCancelFromTheOtherThread<ICtapMessageProcessor>(
         200, 20, makeCMP, [](ICtapMessageProcessor &cmp) {
-            WithDefaultGAArgs([&](const auto &clientData,
-                                  const auto &options) { cmp.GetAssertion(clientData, options); },
-                              ANDROID_EXAMPLE_GET_ASSERTION_CREDENTIAL_ID);
+            WithDefaultGAArgs(
+                [&](const auto &clientData, const auto &options) {
+                    cmp.GetAssertion(clientData, options);
+                },
+                BUFFER_VIEW(ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE_CREDENTIAL_ID));
         });
 }
 
+namespace {
+
+class ProcessingFollowingUpdateMsgsMEncryptedTunnel : public IEncryptedTunnel {
+public:
+    ProcessingFollowingUpdateMsgsMEncryptedTunnel(std::vector<CryptoBuffer> &&messagesToRead)
+    : m_messagesToRead{std::move(messagesToRead)}
+    {
+    }
+
+    CryptoBuffer ReadBinary() override
+    {
+        m_called += "ReadBinary;";
+        if (m_nextMsgToRead == m_messagesToRead.size())
+            throw ExceptionTunnelClosed{};
+        return std::move(m_messagesToRead[m_nextMsgToRead++]);
+    }
+
+    void CloseConnectionAfter(uint32_t usecs) override
+    {
+        m_called += "CloseConnectionAfter;";
+        EXPECT_EQ(usecs, 120'000'000);
+    }
+
+    void WriteBinary(CryptoBuffer) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    void Cancel() override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    std::string m_called;
+
+private:
+    std::vector<CryptoBuffer> m_messagesToRead;
+    size_t m_nextMsgToRead = 0;
+};
+
+} // namespace
+
+TEST(CtapMessageProcessor, ProcessFollowingUpdateMsgs_invalid_message_type)
+{
+    auto encryptedTunnelUPtr = std::make_unique<ProcessingFollowingUpdateMsgsMEncryptedTunnel>(
+        std::vector<CryptoBuffer>{BUFFER(ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE)});
+    auto encryptedTunnel = encryptedTunnelUPtr.get();
+
+    auto cmp = CtapMessageProcessor{};
+    cmp.SetEncryptedTunnel(std::move(encryptedTunnelUPtr));
+
+    EXPECT_THROW(cmp.ProcessFollowingUpdateMsgs(
+                     [](UpdateMessage &&) { ADD_FAILURE() << "This should not be called"; }),
+                 Exception::Unknown);
+    EXPECT_EQ(encryptedTunnel->m_called, "CloseConnectionAfter;ReadBinary;");
+}
+
+TEST(CtapMessageProcessor, ProcessFollowingUpdateMsgs_parse_error_of_the_truncated_update_message)
+{
+    const auto EXAMPLE_UPDATE_MSG = BUFFER_VIEW(ANDROID_EXAMPLE_RAW_UPDATE_MSG);
+    for (size_t len = 0; len + 1 < EXAMPLE_UPDATE_MSG.size(); ++len) {
+        auto encryptedTunnelUPtr = std::make_unique<ProcessingFollowingUpdateMsgsMEncryptedTunnel>(
+            std::vector<CryptoBuffer>{ToBuffer(EXAMPLE_UPDATE_MSG.substr(0, len))});
+        auto encryptedTunnel = encryptedTunnelUPtr.get();
+
+        auto cmp = CtapMessageProcessor{};
+        cmp.SetEncryptedTunnel(std::move(encryptedTunnelUPtr));
+
+        EXPECT_THROW(cmp.ProcessFollowingUpdateMsgs(
+                         [](UpdateMessage &&) { ADD_FAILURE() << "This should not be called"; }),
+                     Exception::Unknown);
+        EXPECT_EQ(encryptedTunnel->m_called, "CloseConnectionAfter;ReadBinary;");
+    }
+}
+
+TEST(CtapMessageProcessor, ProcessFollowingUpdateMsgs_passes_parsed_messages_to_callback)
+{
+    for (size_t msgNum = 0; msgNum < 17; ++msgNum) {
+        auto messages = std::vector<CryptoBuffer>(msgNum, BUFFER(ANDROID_EXAMPLE_RAW_UPDATE_MSG));
+        for (size_t i = 0; i < msgNum; ++i) {
+            ASSERT_GE(messages[i].size(), 5);
+            messages[i].end()[-5] = i; // last byte of the handshake signature
+        }
+
+        auto encryptedTunnelUPtr =
+            std::make_unique<ProcessingFollowingUpdateMsgsMEncryptedTunnel>(std::move(messages));
+        auto encryptedTunnel = encryptedTunnelUPtr.get();
+
+        auto cmp = CtapMessageProcessor{};
+        cmp.SetEncryptedTunnel(std::move(encryptedTunnelUPtr));
+
+        size_t callbackCalledNum = 0;
+        EXPECT_NO_THROW(cmp.ProcessFollowingUpdateMsgs([&](UpdateMessage &&updateMsg) {
+            ++callbackCalledNum;
+
+            UpdateMessage expectedUpdateMsg;
+            auto bv = BUFFER_VIEW(ANDROID_EXAMPLE_RAW_UPDATE_MSG);
+            bv.remove_prefix(1);
+            expectedUpdateMsg.Deserialize(bv);
+
+            ASSERT_NE(updateMsg.GetLinkData(), std::nullopt);
+            ASSERT_NE(expectedUpdateMsg.GetLinkData(), std::nullopt);
+
+            auto &linkData = *updateMsg.GetLinkData();
+            auto &expectedLinkData = *expectedUpdateMsg.GetLinkData();
+
+            EXPECT_EQ(linkData.m_contactId, expectedLinkData.m_contactId);
+            EXPECT_EQ(linkData.m_linkId, expectedLinkData.m_linkId);
+            EXPECT_EQ(linkData.m_linkSecret, expectedLinkData.m_linkSecret);
+            EXPECT_EQ(linkData.m_authenticatorPublicKey, expectedLinkData.m_authenticatorPublicKey);
+            EXPECT_EQ(linkData.m_authenticatorName, expectedLinkData.m_authenticatorName);
+
+            auto expectedSignature = expectedLinkData.m_handshakeSignature;
+            ASSERT_FALSE(expectedSignature.empty());
+            expectedSignature.back() = callbackCalledNum - 1;
+            EXPECT_EQ(linkData.m_handshakeSignature, expectedSignature);
+        }));
+
+        std::string expectedCalled = "CloseConnectionAfter;";
+        for (size_t i = 0; i < msgNum + 1; ++i) {
+            expectedCalled += "ReadBinary;";
+        }
+        EXPECT_EQ(encryptedTunnel->m_called, expectedCalled);
+    }
+}
+
 TEST(CtapMessageProcessor, RpIdValidation)
 {
     static constexpr uint8_t blob[] =
index f75665f588955895d841fedcd6ca4301b31666a9..2044eab9fff6e7aa2407bccb684a76bce0e11ce5 100644 (file)
@@ -21,6 +21,7 @@
 #include "tunnel.h"
 
 #include <gmock/gmock.h>
+#include <limits>
 #include <stdexcept>
 
 namespace {
@@ -41,6 +42,11 @@ public:
         return msg;
     }
 
+    void CloseConnectionAfter(uint32_t) override
+    {
+        throw std::runtime_error{"CloseConnectionAfter() should not be called"};
+    }
+
     void Disconnect() override {}
 
     void Cancel() override { throw std::runtime_error{"Cancel() should not be called"}; }
@@ -142,6 +148,63 @@ TEST(EncryptedTunnel, invalid_data)
 
 namespace {
 
+class CloseConnectionAfterMTunnel : public ITunnel {
+public:
+    void Connect(const std::string &, std::optional<ExtraHttpHeader>)
+    {
+        throw std::runtime_error{__FUNCTION__ + std::string{"() should not be called"}};
+    }
+
+    void WriteBinary(const std::vector<uint8_t> &)
+    {
+        throw std::runtime_error{__FUNCTION__ + std::string{"() should not be called"}};
+    }
+
+    std::vector<uint8_t> ReadBinary()
+    {
+        throw std::runtime_error{__FUNCTION__ + std::string{"() should not be called"}};
+    }
+
+    void CloseConnectionAfter(uint32_t usecs)
+    {
+        m_called += "CloseConnectionAfter;";
+        m_closeConnectionAfterUsecs = usecs;
+    }
+
+    void Disconnect()
+    {
+        throw std::runtime_error{__FUNCTION__ + std::string{"() should not be called"}};
+    }
+
+    void Cancel()
+    {
+        throw std::runtime_error{__FUNCTION__ + std::string{"() should not be called"}};
+    }
+
+    std::string m_called;
+    uint32_t m_closeConnectionAfterUsecs = 0;
+};
+
+} // namespace
+
+TEST(EncryptedTunnel, CloseConnectionAfter)
+{
+    auto tunnelUPtr = std::make_unique<CloseConnectionAfterMTunnel>();
+    auto tunnel = tunnelUPtr.get();
+    auto readerWriter = Crypto::Noise::SymmetricState::CipherState{
+        CryptoBuffer(Crypto::Noise::KEY_AND_HASH_LEN, 0x5f)};
+
+    auto encryptedTunnel = EncryptedTunnel{std::move(tunnelUPtr), readerWriter, readerWriter};
+    auto usecs =
+        get_random(std::numeric_limits<uint32_t>::min(), std::numeric_limits<uint32_t>::max());
+    encryptedTunnel.CloseConnectionAfter(usecs);
+
+    EXPECT_EQ(tunnel->m_called, "CloseConnectionAfter;");
+    EXPECT_EQ(tunnel->m_closeConnectionAfterUsecs, usecs);
+}
+
+namespace {
+
 class CancellationMEchoTunnel : public ITunnel {
 public:
     void Connect(const std::string &, std::optional<ExtraHttpHeader>) override
@@ -161,6 +224,11 @@ public:
         return msg;
     }
 
+    void CloseConnectionAfter(uint32_t) override
+    {
+        throw std::runtime_error{"CloseConnectionAfter() should not be called"};
+    }
+
     void Disconnect() override {}
 
     void Cancel() override { m_cancelFacilitator.Cancel(); }
index 3f80d4864a7567cefece6fbbce9949f78fe82fd2..d342eb8c2b6646596a141145bd1c7821e3a27306 100644 (file)
@@ -48,6 +48,12 @@ public:
         throw std::runtime_error{""};
     }
 
+    void CloseConnectionAfter(uint32_t) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
     void Disconnect() override
     {
         ADD_FAILURE() << "This should not be called";
index d6dd6ae990b4bfb9c0d6147066316f9a18e70138..3521344f05f8d7fa47eb661c08ff9df23f3b81bc 100644 (file)
 
 #include <cstring>
 #include <iostream>
+#include <mutex>
 #include <mv_barcode.h>
 #include <optional>
 #include <string>
+#include <thread>
 #include <webauthn-hal.h>
 
 namespace {
@@ -46,13 +48,18 @@ struct LinkedData {
 };
 
 struct TestContents {
-    bool status;
-    std::string path;
+    std::mutex mutex;
+    bool negative;
+    bool succeeded;
+    std::string qrCodeImagePath;
     Buffer credentialRawId;
     Buffer userId;
     unsigned transports = WAUTHN_TRANSPORT_NONE;
     std::optional<LinkedData> linkedData;
-    bool negative = false;
+    bool seenLastMCUpdateCallback;
+    bool seenLastGAUpdateCallback;
+
+    std::thread GAThread;
 
     void UpdateLinkedData(const wauthn_hybrid_linked_data_s *ld)
     {
@@ -66,43 +73,47 @@ struct TestContents {
             linkedData->signature = ToBuffer(*ld->signature);
             linkedData->tunnelServerDomain = ToBuffer(*ld->tunnel_server_domain);
             linkedData->identityKey = ToBuffer(*ld->identity_key);
+            std::cout << "  linked data: authenticator's public key: "
+                      << LowercaseHexStringOf(linkedData->authenticatorPubKey) << std::endl;
         } else {
             linkedData = std::nullopt;
         }
     }
 };
 
-void DisplayQR(struct TestContents &contents)
+void DisplayQR(struct TestContents &testContents)
 {
     int ret;
-    std::string command = "chsmack -a '_' ";
-    command += contents.path.c_str();
+    std::string command = "chsmack -a '_' '";
+    command += testContents.qrCodeImagePath.c_str();
+    command += '\'';
 
     ret = system(command.c_str());
     if (ret) {
         std::cout << "chsmack command failed\n"
                   << "System() returned: " << ret << std::endl;
-        contents.status = false;
+        testContents.succeeded = false;
         return;
     }
 
     command = "launch_app org.tizen.image-viewer Path ";
-    command += contents.path.c_str();
+    command += testContents.qrCodeImagePath.c_str();
 
+    std::cout << "Launching QR code viewer..." << std::endl;
     ret = system(command.c_str());
     if (ret) {
         std::cout << "launch_app command failed\n"
                   << "System() returned: " << ret << std::endl;
-        contents.status = false;
+        testContents.succeeded = false;
     }
 }
 
-void GenerateAndDisplayQR(const std::string &encoded, struct TestContents &contents)
+void GenerateAndDisplayQR(const std::string &encoded, struct TestContents &testContents)
 {
     int ret;
-    int width = 300;
-    int height = 300;
-    int qr_version = 5;
+    static constexpr int width = 300;
+    static constexpr int height = 300;
+    static constexpr int qr_version = 5;
 
     mv_engine_config_h engine_cfg = NULL;
     mv_barcode_type_e type = MV_BARCODE_QR;
@@ -118,121 +129,83 @@ void GenerateAndDisplayQR(const std::string &encoded, struct TestContents &conte
                                     qr_enc_mode,
                                     qr_ecc,
                                     qr_version,
-                                    contents.path.c_str(),
+                                    testContents.qrCodeImagePath.c_str(),
                                     image_format);
 
     if (ret != MEDIA_VISION_ERROR_NONE) {
         std::cout << "mv_barcode_generate_image failed\n"
                   << "Error code: " << ret << std::endl;
-        contents.status = false;
+        testContents.succeeded = false;
     } else {
-        DisplayQR(contents);
+        DisplayQR(testContents);
     }
 }
 
-void DisplayQRCallback(const char *qr_contents, void *data)
+void DisplayQRCallback(const char *qrContents, void *data)
 {
-    std::string encoded(qr_contents);
-
+    std::cout << "qrcode_callback() was called." << std::endl;
     auto testContents = static_cast<TestContents *>(data);
-    if (testContents->path.empty() || !qr_contents) {
-        std::cout << "qrcode_callback failed" << std::endl;
-        std::cout << "QRcode path: " << testContents->path << std::endl;
-        std::cout << "qr_contents: " << (qr_contents ? qr_contents : "null") << std::endl;
-        testContents->status = false;
+    if (!qrContents) {
+        std::cout << "qrcode_callback(): NULL qrContents" << std::endl;
+        testContents->succeeded = false;
         return;
     }
+    if (testContents->qrCodeImagePath.empty()) {
+        std::cout << "qrcode_callback(): empty QR code path" << std::endl;
+        testContents->succeeded = false;
+        return;
+    }
+
     if (testContents->negative)
         GenerateAndDisplayQR("https://tinyurl.com/5x2bcwjy", *testContents);
-    else
-        GenerateAndDisplayQR(encoded, *testContents);
+    else {
+        GenerateAndDisplayQR(qrContents, *testContents);
+    }
 }
 
 void MCCallback(const wauthn_pubkey_credential_attestation_s *pubkey_cred,
                 wauthn_error_e result,
-                void *data)
-{
-    auto *testContents = static_cast<TestContents *>(data);
-    if (pubkey_cred == nullptr || result != WAUTHN_ERROR_NONE || testContents->path.empty()) {
-        std::cout << __FUNCTION__ << ": response_callback failed\n"
-                  << "Error code: " << result << std::endl;
-        testContents->status = false;
-        return;
-    }
-
-    testContents->credentialRawId = ToBuffer(*pubkey_cred->rawId);
-    std::cerr << "MC: credentialRawId: " << LowercaseHexStringOf(testContents->credentialRawId)
-              << std::endl;
-
-    testContents->transports = pubkey_cred->response->transports;
-
-    testContents->UpdateLinkedData(pubkey_cred->linked_device);
-}
+                void *data);
 
 void GACallback(const wauthn_pubkey_credential_assertion_s *pubkey_cred,
                 wauthn_error_e result,
-                void *data)
-{
-    auto *testContents = static_cast<TestContents *>(data);
-    if (pubkey_cred == nullptr || result != WAUTHN_ERROR_NONE || testContents->path.empty()) {
-        std::cout << __FUNCTION__ << ": response_callback failed\n"
-                  << "Error code: " << result << std::endl;
-        testContents->status = false;
-        return;
-    }
+                void *data);
 
-    auto credentialRawId = ToBuffer(*pubkey_cred->rawId);
-    if (credentialRawId != testContents->credentialRawId) {
-        std::cerr << "Error: invalid credentialRawId in GA: "
-                  << LowercaseHexStringOf(credentialRawId) << std::endl;
-        testContents->status = false;
-        return;
-    }
+void MCUpdateLinkedDataCallback(const wauthn_hybrid_linked_data_s *linkedData,
+                                wauthn_error_e result,
+                                void *data);
 
-    if (pubkey_cred->response->user_handle) {
-        auto userHandle = ToBuffer(*pubkey_cred->response->user_handle);
-        if (userHandle != testContents->userId) {
-            std::cerr << "Error: invalid userHandle in GA: " << LowercaseHexStringOf(userHandle)
-                      << std::endl;
-            testContents->status = false;
-            return;
-        }
-    }
+void GAUpdateLinkedDataCallback(const wauthn_hybrid_linked_data_s *linkedData,
+                                wauthn_error_e result,
+                                void *data);
 
-    testContents->UpdateLinkedData(pubkey_cred->linked_device);
-}
+constexpr inline char CLIENT_DATA_JSON[] = "{}";
+constexpr inline char RP_ID[] = "acme.com";
+constexpr inline char RP_NAME[] = "Acme";
+constexpr inline char USER_ID[] = "\x00\x01\x02\x03\x04\x05\x06\x07";
+constexpr inline char USER_NAME[] = "user name";
+constexpr inline char USER_DISPLAY_NAME[] = "user display name";
 
-bool Test(struct TestContents &testContents)
+void TestMC(struct TestContents &testContents)
 {
-    /* MakeCredential */
-    unsigned char clientDataJson[] = "{}";
-    wauthn_const_buffer_s clientDataJsonBuff;
-    clientDataJsonBuff.data = clientDataJson;
-    clientDataJsonBuff.size = sizeof(clientDataJson) - 1;
+    auto clientDataJsonBuff = ToWauthnConstBuff(CLIENT_DATA_JSON);
 
     wauthn_client_data_s clientData;
     clientData.client_data_json = &clientDataJsonBuff;
     clientData.hash_alg = WAUTHN_HASH_ALGORITHM_SHA_256;
 
-    char rpName[] = "Acme";
-    char rpId[] = "acme.com";
     wauthn_rp_entity_s rp;
-    rp.name = rpName;
-    rp.id = rpId;
-
-    unsigned char userId[] = "\x00\x01\x02\x03\x04\x05\x06\x07";
-    wauthn_const_buffer_s userIdBuff;
-    userIdBuff.data = userId;
-    userIdBuff.size = sizeof(userId) - 1;
+    rp.name = RP_NAME;
+    rp.id = RP_ID;
 
+    auto userIdBuff =
+        wauthn_const_buffer_s{reinterpret_cast<const uint8_t *>(USER_ID), sizeof(USER_ID) - 1};
     testContents.userId = ToBuffer(userIdBuff);
 
-    char userName[] = "user name";
-    char userDisplayName[] = "user display name";
     wauthn_user_entity_s user;
     user.id = &userIdBuff;
-    user.name = userName;
-    user.display_name = userDisplayName;
+    user.name = USER_NAME;
+    user.display_name = USER_DISPLAY_NAME;
 
     wauthn_pubkey_cred_param_s params[] = {
         {PCT_PUBLIC_KEY, WAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256       },
@@ -251,23 +224,24 @@ bool Test(struct TestContents &testContents)
     wauthn_mc_callbacks_s mcCallbacks;
     mcCallbacks.qrcode_callback = DisplayQRCallback;
     mcCallbacks.response_callback = MCCallback;
+    mcCallbacks.linked_data_callback = MCUpdateLinkedDataCallback;
     mcCallbacks.user_data = &testContents;
 
-    int err = TurnBluetoothOn();
-    if (err != BT_ERROR_NONE) {
-        std::cout << "Turning bluetooth on failed with " << BtErrorToString(err) << std::endl;
-        return false;
-    }
-
     wah_make_credential(&clientData, &mcOptions, &mcCallbacks);
+}
 
-    if (!testContents.status) {
-        return false;
-    }
+void TestGA(struct TestContents &testContents)
+{
+    auto lock = std::unique_lock{testContents.mutex};
+    auto clientDataJsonBuff = ToWauthnConstBuff(CLIENT_DATA_JSON);
 
-    /* GetAssertion */
+    wauthn_client_data_s clientData;
+    clientData.client_data_json = &clientDataJsonBuff;
+    clientData.hash_alg = WAUTHN_HASH_ALGORITHM_SHA_256;
 
-    wauthn_const_buffer_s credentialId = ToWauthnConstBuff(testContents.credentialRawId);
+    auto credentialRawIdCopy =
+        testContents.credentialRawId; // Copy so that we don't care about it changing.
+    wauthn_const_buffer_s credentialId = ToWauthnConstBuff(credentialRawIdCopy);
 
     wauthn_pubkey_cred_descriptor_s pubkeyCredDescriptor;
     pubkeyCredDescriptor.type = PCT_PUBLIC_KEY;
@@ -278,6 +252,8 @@ bool Test(struct TestContents &testContents)
     pubkeyCredDescriptors.size = 1;
     pubkeyCredDescriptors.descriptors = &pubkeyCredDescriptor;
 
+    auto linkedData = testContents.linkedData; // Copy so that we don't care about it changing.
+
     wauthn_const_buffer_s contactId;
     wauthn_const_buffer_s linkId;
     wauthn_const_buffer_s linkSecret;
@@ -288,15 +264,15 @@ bool Test(struct TestContents &testContents)
     wauthn_const_buffer_s identityKey;
 
     wauthn_hybrid_linked_data_s linkedDevice;
-    if (testContents.linkedData) {
-        contactId = ToWauthnConstBuff(testContents.linkedData->contactId);
-        linkId = ToWauthnConstBuff(testContents.linkedData->linkId);
-        linkSecret = ToWauthnConstBuff(testContents.linkedData->linkSecret);
-        authenticatorPubKey = ToWauthnConstBuff(testContents.linkedData->authenticatorPubKey);
-        authenticatorName = ToWauthnConstBuff(testContents.linkedData->authenticatorName);
-        signature = ToWauthnConstBuff(testContents.linkedData->signature);
-        tunnelServerDomain = ToWauthnConstBuff(testContents.linkedData->tunnelServerDomain);
-        identityKey = ToWauthnConstBuff(testContents.linkedData->identityKey);
+    if (linkedData) {
+        contactId = ToWauthnConstBuff(linkedData->contactId);
+        linkId = ToWauthnConstBuff(linkedData->linkId);
+        linkSecret = ToWauthnConstBuff(linkedData->linkSecret);
+        authenticatorPubKey = ToWauthnConstBuff(linkedData->authenticatorPubKey);
+        authenticatorName = ToWauthnConstBuff(linkedData->authenticatorName);
+        signature = ToWauthnConstBuff(linkedData->signature);
+        tunnelServerDomain = ToWauthnConstBuff(linkedData->tunnelServerDomain);
+        identityKey = ToWauthnConstBuff(linkedData->identityKey);
 
         linkedDevice.contact_id = &contactId;
         linkedDevice.link_id = &linkId;
@@ -309,36 +285,207 @@ bool Test(struct TestContents &testContents)
     }
 
     wauthn_pubkey_cred_request_options_s gaOptions;
-    gaOptions.timeout = 60000; // 60s
-    gaOptions.rpId = rpId;
+    std::memset(&gaOptions, 0, sizeof(gaOptions)); // For future compatibility.
+    gaOptions.timeout = 60000;                     // 60s
+    gaOptions.rpId = RP_ID;
     gaOptions.user_verification = UVR_REQUIRED;
     gaOptions.allow_credentials = &pubkeyCredDescriptors;
     gaOptions.hints = nullptr;
     gaOptions.attestation = AP_NONE;
     gaOptions.attestation_formats = nullptr;
     gaOptions.extensions = nullptr;
-    gaOptions.linked_device = testContents.linkedData ? &linkedDevice : nullptr;
+    gaOptions.linked_device = linkedData ? &linkedDevice : nullptr;
 
     wauthn_ga_callbacks_s gaCallbacks;
     gaCallbacks.qrcode_callback = DisplayQRCallback;
     gaCallbacks.response_callback = GACallback;
+    gaCallbacks.linked_data_callback = GAUpdateLinkedDataCallback;
     gaCallbacks.user_data = &testContents;
 
+    lock.unlock();
     wah_get_assertion(&clientData, &gaOptions, &gaCallbacks);
+}
+
+bool Test(struct TestContents &testContents)
+{
+    TestMC(testContents);
+    if (testContents.GAThread.joinable()) // Safe to access as changed only in the current thread.
+        testContents.GAThread.join();
+    return testContents.succeeded;
+}
+
+void MCCallback(const wauthn_pubkey_credential_attestation_s *pubkey_cred,
+                wauthn_error_e result,
+                void *data)
+{
+    std::cout << "MakeCredential: response_callback() was called." << std::endl;
+    auto *testContents = static_cast<TestContents *>(data);
+    if (result != WAUTHN_ERROR_NONE) {
+        std::cout << __FUNCTION__ << ": response_callback failed with code " << result << std::endl;
+        testContents->succeeded = false;
+        return;
+    }
+    if (!pubkey_cred) {
+        std::cout << __FUNCTION__ << ": pubkey_cred is NULL" << std::endl;
+        testContents->succeeded = false;
+        return;
+    }
+    if (!pubkey_cred->rawId) {
+        std::cout << __FUNCTION__ << ": pubkey_cred->rawId is NULL" << std::endl;
+        testContents->succeeded = false;
+        return;
+    }
+    if (!pubkey_cred->response) {
+        std::cout << __FUNCTION__ << ": pubkey_cred->response is NULL" << std::endl;
+        testContents->succeeded = false;
+        return;
+    }
 
-    return testContents.status;
+    testContents->credentialRawId = ToBuffer(*pubkey_cred->rawId);
+    std::cout << "MakeCredential: credentialRawId: "
+              << LowercaseHexStringOf(testContents->credentialRawId) << std::endl;
+    testContents->transports = pubkey_cred->response->transports;
+    testContents->UpdateLinkedData(pubkey_cred->linked_device);
+
+    testContents->GAThread = std::thread([testContents] { TestGA(*testContents); });
+
+    std::cout << "MakeCredential: awaiting potential CTAP UPDATE messages..." << std::endl;
+}
+
+void GACallback(const wauthn_pubkey_credential_assertion_s *pubkey_cred,
+                wauthn_error_e result,
+                void *data)
+{
+    std::cout << "GetAssertion: response_callback() was called." << std::endl;
+    auto *testContents = static_cast<TestContents *>(data);
+    auto lock = std::lock_guard{testContents->mutex};
+    if (result != WAUTHN_ERROR_NONE) {
+        std::cout << __FUNCTION__ << ": response_callback failed with code " << result << std::endl;
+        testContents->succeeded = false;
+        return;
+    }
+    if (!pubkey_cred) {
+        std::cout << __FUNCTION__ << ": pubkey_cred is NULL" << std::endl;
+        testContents->succeeded = false;
+        return;
+    }
+    if (!pubkey_cred->rawId) {
+        std::cout << __FUNCTION__ << ": pubkey_cred->rawId is NULL" << std::endl;
+        testContents->succeeded = false;
+        return;
+    }
+    if (!pubkey_cred->response) {
+        std::cout << __FUNCTION__ << ": pubkey_cred->response is NULL" << std::endl;
+        testContents->succeeded = false;
+        return;
+    }
+
+    auto credentialRawId = ToBuffer(*pubkey_cred->rawId);
+    if (credentialRawId != testContents->credentialRawId) {
+        std::cout << "Error: invalid credentialRawId in GA: "
+                  << LowercaseHexStringOf(credentialRawId) << std::endl;
+        testContents->succeeded = false;
+        return;
+    }
+
+    if (pubkey_cred->response->user_handle) {
+        auto userHandle = ToBuffer(*pubkey_cred->response->user_handle);
+        if (userHandle != testContents->userId) {
+            std::cout << "Error: invalid userHandle in GA: " << LowercaseHexStringOf(userHandle)
+                      << std::endl;
+            testContents->succeeded = false;
+            return;
+        }
+    }
+
+    testContents->UpdateLinkedData(pubkey_cred->linked_device);
+
+    std::cout << "GetAssertion: awaiting potential CTAP UPDATE messages..." << std::endl;
+}
+
+void UpdateLinkedDataCallbackImpl(const wauthn_hybrid_linked_data_s *linkedData,
+                                  wauthn_error_e result,
+                                  TestContents &testContents,
+                                  std::lock_guard<std::mutex> & /*lock*/,
+                                  bool &seenLastUpdateCallback)
+{
+    if (seenLastUpdateCallback) {
+        std::cout
+            << __FUNCTION__
+            << ": update_linked_data callback called after it was called with WAUTHN_ERROR_NONE"
+            << std::endl;
+        testContents.succeeded = false;
+        return;
+    }
+    if (result == WAUTHN_ERROR_NONE)
+        seenLastUpdateCallback = true;
+    else if (result != WAUTHN_ERROR_NONE_AND_WAIT) {
+        std::cout << __FUNCTION__ << ": update_linked_data callback failed with code: " << result
+                  << std::endl;
+        testContents.succeeded = false;
+        return;
+    }
+    testContents.UpdateLinkedData(linkedData);
+}
+
+void MCUpdateLinkedDataCallback(const wauthn_hybrid_linked_data_s *linkedData,
+                                wauthn_error_e result,
+                                void *data)
+{
+    std::cout << "MakeCredential: update_linked_data() was called." << std::endl;
+    auto *testContents = static_cast<TestContents *>(data);
+    auto lock = std::lock_guard{testContents->mutex};
+    UpdateLinkedDataCallbackImpl(
+        linkedData, result, *testContents, lock, testContents->seenLastMCUpdateCallback);
+    if (result == WAUTHN_ERROR_NONE)
+        std::cout << "MakeCredential: finished awaiting potential CTAP UPDATE messages."
+                  << std::endl;
+}
+
+void GAUpdateLinkedDataCallback(const wauthn_hybrid_linked_data_s *linkedData,
+                                wauthn_error_e result,
+                                void *data)
+{
+    std::cout << "GetAssertion: update_linked_data() was called." << std::endl;
+    auto *testContents = static_cast<TestContents *>(data);
+    auto lock = std::lock_guard{testContents->mutex};
+    UpdateLinkedDataCallbackImpl(
+        linkedData, result, *testContents, lock, testContents->seenLastGAUpdateCallback);
+    if (result == WAUTHN_ERROR_NONE)
+        std::cout << "GetAssertion: finished awaiting potential CTAP UPDATE messages." << std::endl;
 }
 
 } // anonymous namespace
 
 int main(int argc, char *argv[])
 {
-    TestContents testContents = {
-        true, "/tmp/qrcode.png", {}, {}, WAUTHN_TRANSPORT_NONE, std::nullopt, false};
-
-    if (argc > 1 && std::string_view("-n") == argv[1])
+    TestContents testContents = {{},
+                                 false,
+                                 true,
+                                 "/tmp/qrcode.png",
+                                 {},
+                                 {},
+                                 WAUTHN_TRANSPORT_NONE,
+                                 std::nullopt,
+                                 false,
+                                 false,
+                                 {}};
+
+    if (argc > 2) {
+        std::cout << "Usage: " << (argv[0] ? argv[0] : "webauthn-ble-manual-tests") << " [-n]\n"
+                  << "\nOptions:\n"
+                  << "  -n  Negative test -- with invalid QR code\n";
+        return 3;
+    }
+    if (argc == 2 && std::string_view("-n") == argv[1])
         testContents.negative = true;
 
+    int err = TurnBluetoothOn();
+    if (err != BT_ERROR_NONE) {
+        std::cout << "Turning bluetooth on failed with " << BtErrorToString(err) << std::endl;
+        return 2;
+    }
+
     int ret = 0;
     if (Test(testContents))
         std::cout << "Test passed.\n";
diff --git a/tests/message_examples.h b/tests/message_examples.h
new file mode 100644 (file)
index 0000000..a791c20
--- /dev/null
@@ -0,0 +1,174 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd All Rights Reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+#pragma once
+
+// Example iPhone post handshake message received after handshake
+constexpr inline char IPHONE_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE[] =
+    "\xa1\x01\x58\x4f\xa5\x01\x82\x68\x46\x49\x44\x4f\x5f\x32\x5f\x30\x68\x46\x49\x44\x4f\x5f\x32"
+    "\x5f\x31\x02\x81\x69\x6c\x61\x72\x67\x65\x42\x6c\x6f\x62\x03\x50\xf2\x4a\x8e\x70\xd0\xd3\xf8"
+    "\x2c\x29\x37\x32\x52\x3c\xc4\xde\x5a\x04\xa2\x62\x72\x6b\xf5\x62\x75\x76\xf5\x09\x82\x68\x69"
+    "\x6e\x74\x65\x72\x6e\x61\x6c\x66\x68\x79\x62\x72\x69\x64";
+
+// Example Android post handshake message received after handshake
+constexpr inline char ANDROID_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE[] =
+    "\xa1\x01\x58\x53\xa5\x01\x82\x68\x46\x49\x44\x4f\x5f\x32\x5f\x30\x68\x46\x49\x44\x4f\x5f\x32"
+    "\x5f\x31\x02\x81\x63\x70\x72\x66\x03\x50\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    "\x00\x00\x00\x04\xa4\x62\x72\x6b\xf5\x62\x75\x70\xf5\x62\x75\x76\xf5\x64\x70\x6c\x61\x74\xf4"
+    "\x09\x82\x68\x69\x6e\x74\x65\x72\x6e\x61\x6c\x66\x68\x79\x62\x72\x69\x64";
+
+// Example iPhone MakeCredential request collected during manual testing
+constexpr inline char IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_REQUEST[] =
+    "\x01\x01\xa4\x01\x58\x20\x44\x13\x6f\xa3\x55\xb3\x67\x8a\x11\x46\xad\x16\xf7\xe8\x64\x9e\x94"
+    "\xfb\x4f\xc2\x1f\xe7\x7e\x83\x10\xc0\x60\xf6\x1c\xaa\xff\x8a\x02\xa2\x62\x69\x64\x68\x61\x63"
+    "\x6d\x65\x2e\x63\x6f\x6d\x64\x6e\x61\x6d\x65\x64\x41\x63\x6d\x65\x03\xa3\x62\x69\x64\x48\x00"
+    "\x01\x02\x03\x04\x05\x06\x07\x64\x6e\x61\x6d\x65\x69\x75\x73\x65\x72\x20\x6e\x61\x6d\x65\x6b"
+    "\x64\x69\x73\x70\x6c\x61\x79\x4e\x61\x6d\x65\x71\x75\x73\x65\x72\x20\x64\x69\x73\x70\x6c\x61"
+    "\x79\x20\x6e\x61\x6d\x65\x04\x82\xa2\x63\x61\x6c\x67\x26\x64\x74\x79\x70\x65\x6a\x70\x75\x62"
+    "\x6c\x69\x63\x2d\x6b\x65\x79\xa2\x63\x61\x6c\x67\x39\x01\x00\x64\x74\x79\x70\x65\x6a\x70\x75"
+    "\x62\x6c\x69\x63\x2d\x6b\x65\x79";
+
+// Example iPhone MakeCredential response collected during manual testing
+constexpr inline char IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE[] =
+    "\x01\x00\xa4\x01\x64\x6e\x6f\x6e\x65\x02\x58\x98\x11\x94\x22\x8d\xa8\xfd\xbd\xee\xfd\x26\x1b"
+    "\xd7\xb6\x59\x5c\xfd\x70\xa5\x0d\x70\xc6\x40\x7b\xcf\x01\x3d\xe9\x6d\x4e\xfb\x17\xde\x5d\x00"
+    "\x00\x00\x00\xfb\xfc\x30\x07\x15\x4e\x4e\xcc\x8c\x0b\x6e\x02\x05\x57\xd7\xbd\x00\x14\x7c\xe0"
+    "\x64\xdc\xae\x09\x40\x6a\x9a\x2f\xae\x13\x5e\x01\x63\x4a\xbd\x50\xb5\xd2\xa5\x01\x02\x03\x26"
+    "\x20\x01\x21\x58\x20\x70\x9a\xdb\x3b\x95\x96\x6b\xc0\x9c\x68\x31\xf2\xf7\x38\xfe\x19\x1d\x7c"
+    "\x55\xb7\xbf\xde\xb7\x11\x5a\x5e\xab\xef\x10\xaf\xb4\x6d\x22\x58\x20\xab\x15\xf0\x1d\x37\x94"
+    "\xcb\x9b\xaf\xd3\x92\xc3\x0a\x07\x03\x82\xa8\xc7\xc5\x88\x8e\xed\xdf\xaf\xb0\x80\x39\x4d\x4e"
+    "\x67\x8c\xd1\x03\xa0\x06\xa0";
+
+// Attestation data part of the IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE
+constexpr inline char IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE_ATTESTATION_DATA[] =
+    "\xfb\xfc\x30\x07\x15\x4e\x4e\xcc\x8c\x0b\x6e\x02\x05\x57\xd7\xbd\x00\x14\x92\xa5\x07\x16\x15"
+    "\x56\x78\x93\x64\x86\x22\xaf\xca\x66\x3d\x24\x50\x13\x8c\x58\xa5\x01\x02\x03\x26\x20\x01\x21"
+    "\x58\x20\xc8\x39\x05\x61\x42\x33\x3e\x54\xcd\x7d\xed\x55\xb7\x64\xbd\x24\xb8\x6b\x31\x67\x07"
+    "\x4d\x37\x28\x0a\xbe\x86\x37\x13\x6c\x71\xca\x22\x58\x20\x35\x57\xa0\x4e\x67\x8b\x96\x87\x3c"
+    "\x00\xc2\x97\xf7\xf3\x5b\x60\x97\x2a\x18\x06\x48\xe5\x5a\xcb\x33\xbe\x8d\x47\x17\x56\x5a\xd0";
+
+// Parsed public key DER part of the IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE
+constexpr inline char IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE_PUBLIC_KEY_DER[] =
+    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07"
+    "\x03\x42\x00\x04\x70\x9a\xdb\x3b\x95\x96\x6b\xc0\x9c\x68\x31\xf2\xf7\x38\xfe\x19\x1d\x7c\x55"
+    "\xb7\xbf\xde\xb7\x11\x5a\x5e\xab\xef\x10\xaf\xb4\x6d\xab\x15\xf0\x1d\x37\x94\xcb\x9b\xaf\xd3"
+    "\x92\xc3\x0a\x07\x03\x82\xa8\xc7\xc5\x88\x8e\xed\xdf\xaf\xb0\x80\x39\x4d\x4e\x67\x8c\xd1";
+
+// Example Android MakeCredential request collected during manual testing
+constexpr inline char ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_REQUEST[] =
+    "\x01\x01\xa4\x01\x58\x20\x44\x13\x6f\xa3\x55\xb3\x67\x8a\x11\x46\xad\x16\xf7\xe8\x64\x9e\x94"
+    "\xfb\x4f\xc2\x1f\xe7\x7e\x83\x10\xc0\x60\xf6\x1c\xaa\xff\x8a\x02\xa2\x62\x69\x64\x68\x61\x63"
+    "\x6d\x65\x2e\x63\x6f\x6d\x64\x6e\x61\x6d\x65\x64\x41\x63\x6d\x65\x03\xa3\x62\x69\x64\x48\x00"
+    "\x01\x02\x03\x04\x05\x06\x07\x64\x6e\x61\x6d\x65\x69\x75\x73\x65\x72\x20\x6e\x61\x6d\x65\x6b"
+    "\x64\x69\x73\x70\x6c\x61\x79\x4e\x61\x6d\x65\x71\x75\x73\x65\x72\x20\x64\x69\x73\x70\x6c\x61"
+    "\x79\x20\x6e\x61\x6d\x65\x04\x82\xa2\x63\x61\x6c\x67\x26\x64\x74\x79\x70\x65\x6a\x70\x75\x62"
+    "\x6c\x69\x63\x2d\x6b\x65\x79\xa2\x63\x61\x6c\x67\x39\x01\x00\x64\x74\x79\x70\x65\x6a\x70\x75"
+    "\x62\x6c\x69\x63\x2d\x6b\x65\x79";
+
+// Example Android MakeCredential response collected during manual testing
+constexpr inline char ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE[] =
+    "\x01\x00\xa3\x01\x64\x6e\x6f\x6e\x65\x02\x58\xa4\x11\x94\x22\x8d\xa8\xfd\xbd\xee\xfd\x26\x1b"
+    "\xd7\xb6\x59\x5c\xfd\x70\xa5\x0d\x70\xc6\x40\x7b\xcf\x01\x3d\xe9\x6d\x4e\xfb\x17\xde\x45\x00"
+    "\x00\x00\x00\x53\x41\x4d\x53\x55\x4e\x47\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20\xde\x2f"
+    "\x46\x7f\xb4\xf6\xc7\xbe\x49\x42\xdb\x80\xa4\x3d\xcd\xf2\x1b\xc9\xf7\x31\x54\x9c\x7b\x58\xc8"
+    "\x2b\x22\x46\x1d\x17\x43\x1d\xa5\x01\x02\x03\x26\x20\x01\x21\x58\x20\x9f\x80\x80\xc9\xac\xf9"
+    "\x92\x66\x12\x3a\xcf\x0e\xf8\x7e\x5c\x3f\xbf\xb4\x34\xc8\x96\xa0\xe2\x54\xbe\xeb\x8c\x62\xb2"
+    "\x5d\x54\xc8\x22\x58\x20\xc2\x9f\x0d\xc8\x8d\x81\x8f\x57\x51\x46\x93\x31\xc0\x59\xce\x13\x5f"
+    "\xbb\x3d\x65\x39\xc2\x89\x68\x77\x52\xb8\x80\x0c\x86\xf9\xd6\x03\xa0";
+
+// Parsed public key DER part of the ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE
+constexpr inline char ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE_PUBLIC_KEY_DER[] =
+    "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07"
+    "\x03\x42\x00\x04\x9f\x80\x80\xc9\xac\xf9\x92\x66\x12\x3a\xcf\x0e\xf8\x7e\x5c\x3f\xbf\xb4\x34"
+    "\xc8\x96\xa0\xe2\x54\xbe\xeb\x8c\x62\xb2\x5d\x54\xc8\xc2\x9f\x0d\xc8\x8d\x81\x8f\x57\x51\x46"
+    "\x93\x31\xc0\x59\xce\x13\x5f\xbb\x3d\x65\x39\xc2\x89\x68\x77\x52\xb8\x80\x0c\x86\xf9\xd6";
+
+// Example iPhone GetAssertion request collected during manual testing
+constexpr inline char IPHONE_EXAMPLE_GET_ASSERTION_RAW_REQUEST[] =
+    "\x01\x02\xa4\x01\x68\x61\x63\x6d\x65\x2e\x63\x6f\x6d\x02\x58\x20\x44\x13\x6f\xa3\x55\xb3\x67"
+    "\x8a\x11\x46\xad\x16\xf7\xe8\x64\x9e\x94\xfb\x4f\xc2\x1f\xe7\x7e\x83\x10\xc0\x60\xf6\x1c\xaa"
+    "\xff\x8a\x03\x81\xa3\x62\x69\x64\x54\xef\x42\x5d\x64\xe3\xed\xe0\xaf\x1e\x44\xe8\xd4\xf3\xd7"
+    "\x8c\xa0\x93\x3c\x39\xfc\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\x6a"
+    "\x74\x72\x61\x6e\x73\x70\x6f\x72\x74\x73\x82\x66\x68\x79\x62\x72\x69\x64\x68\x69\x6e\x74\x65"
+    "\x72\x6e\x61\x6c\x05\xa1\x62\x75\x76\xf5";
+
+// Example iPhone GetAssertion response collected during manual testing
+constexpr inline char IPHONE_EXAMPLE_GET_ASSERTION_RAW_RESPONSE[] =
+    "\x01\x00\xa5\x01\xa2\x62\x69\x64\x54\xef\x42\x5d\x64\xe3\xed\xe0\xaf\x1e\x44\xe8\xd4\xf3\xd7"
+    "\x8c\xa0\x93\x3c\x39\xfc\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\x02"
+    "\x58\x25\x11\x94\x22\x8d\xa8\xfd\xbd\xee\xfd\x26\x1b\xd7\xb6\x59\x5c\xfd\x70\xa5\x0d\x70\xc6"
+    "\x40\x7b\xcf\x01\x3d\xe9\x6d\x4e\xfb\x17\xde\x1d\x00\x00\x00\x00\x03\x58\x48\x30\x46\x02\x21"
+    "\x00\xf7\x3d\xb9\x8b\xc4\xc4\xc5\x08\xf8\x24\x3e\xdc\x79\x04\x26\x24\x69\xb7\xa9\x4a\xa2\x5a"
+    "\xac\x49\x6c\x72\x3a\xf2\x31\x61\x7b\x4a\x02\x21\x00\xc9\x94\x3c\x24\x15\x9e\x94\xee\x55\xb3"
+    "\xba\xe9\x5c\xfd\xb9\xbd\x08\xc0\xef\x10\x2d\xe9\x75\xef\xcf\x27\x1c\x16\x5b\x69\xb9\xe1\x04"
+    "\xa3\x62\x69\x64\x48\x00\x01\x02\x03\x04\x05\x06\x07\x64\x6e\x61\x6d\x65\x60\x6b\x64\x69\x73"
+    "\x70\x6c\x61\x79\x4e\x61\x6d\x65\x60\x08\xa0";
+
+// Credential id part of the IPHONE_EXAMPLE_GET_ASSERTION_RAW_RESPONSE
+constexpr inline char IPHONE_EXAMPLE_GET_ASSERTION_RAW_RESPONSE_CREDENTIAL_ID[] =
+    "\xef\x42\x5d\x64\xe3\xed\xe0\xaf\x1e\x44\xe8\xd4\xf3\xd7\x8c\xa0\x93\x3c\x39\xfc";
+
+// Example Android GetAssertion request collected during manual testing
+constexpr inline char ANDROID_EXAMPLE_GET_ASSERTION_RAW_REQUEST[] =
+    "\x01\x02\xa4\x01\x68\x61\x63\x6d\x65\x2e\x63\x6f\x6d\x02\x58\x20\x44\x13\x6f\xa3\x55\xb3\x67"
+    "\x8a\x11\x46\xad\x16\xf7\xe8\x64\x9e\x94\xfb\x4f\xc2\x1f\xe7\x7e\x83\x10\xc0\x60\xf6\x1c\xaa"
+    "\xff\x8a\x03\x81\xa3\x62\x69\x64\x58\x20\x9e\x39\x33\x8a\xa5\xf2\x71\xee\x29\x8b\x9e\xab\x7a"
+    "\xf2\x04\x73\xce\x0a\x6a\x5d\x1d\x8e\xf7\x14\xa7\x23\x0b\xc3\x44\x87\xea\xcd\x64\x74\x79\x70"
+    "\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\x6a\x74\x72\x61\x6e\x73\x70\x6f\x72\x74\x73"
+    "\x82\x66\x68\x79\x62\x72\x69\x64\x68\x69\x6e\x74\x65\x72\x6e\x61\x6c\x05\xa1\x62\x75\x76\xf5";
+
+// Example Android GetAssertion response collected during manual testing
+constexpr inline char ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE[] =
+    "\x01\x00\xa3\x01\xa2\x62\x69\x64\x58\x20\x9e\x39\x33\x8a\xa5\xf2\x71\xee\x29\x8b\x9e\xab\x7a"
+    "\xf2\x04\x73\xce\x0a\x6a\x5d\x1d\x8e\xf7\x14\xa7\x23\x0b\xc3\x44\x87\xea\xcd\x64\x74\x79\x70"
+    "\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79\x02\x58\x25\x11\x94\x22\x8d\xa8\xfd\xbd\xee"
+    "\xfd\x26\x1b\xd7\xb6\x59\x5c\xfd\x70\xa5\x0d\x70\xc6\x40\x7b\xcf\x01\x3d\xe9\x6d\x4e\xfb\x17"
+    "\xde\x05\x00\x00\x00\x00\x03\x58\x48\x30\x46\x02\x21\x00\xc4\x34\xe3\x98\x33\xac\xd5\x9a\x88"
+    "\x5d\x00\x90\x56\x0f\x10\xde\x7a\x0e\xd0\xd4\x02\x76\xcc\x28\x74\xf0\xa9\x39\x7e\x19\x08\x19"
+    "\x02\x21\x00\xff\xf1\x13\x5b\x28\x75\xce\xe5\x1c\x9b\x1c\xaa\xfe\x6e\x41\x5d\x67\x7c\x0c\xee"
+    "\x64\xe6\x18\xd5\x81\x95\xd1\xae\xc1\xa2\xdb\x5a";
+
+// Credential id part of the ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE
+constexpr inline char ANDROID_EXAMPLE_GET_ASSERTION_RAW_RESPONSE_CREDENTIAL_ID[] =
+    "\x9e\x39\x33\x8a\xa5\xf2\x71\xee\x29\x8b\x9e\xab\x7a\xf2\x04\x73\xce\x0a\x6a\x5d\x1d\x8e\xf7"
+    "\x14\xa7\x23\x0b\xc3\x44\x87\xea\xcd";
+
+// TODO: move the rest from the ctap processor tests
+
+// Example value received from an Android authenticator during manual testing
+constexpr inline char ANDROID_EXAMPLE_RAW_UPDATE_MSG[] =
+    "\x02\xa2\x00\x58\xaf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
+    "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\xa7\x01\x58"
+    "\x98\x64\x2d\x51\x6b\x57\x6e\x54\x6d\x77\x53\x45\x3a\x41\x50\x41\x39\x31\x62\x48\x76\x72\x44"
+    "\x72\x39\x53\x64\x44\x31\x71\x39\x4a\x69\x4d\x55\x78\x52\x38\x57\x30\x41\x5a\x48\x57\x61\x56"
+    "\x31\x4d\x46\x4b\x36\x77\x4f\x56\x78\x70\x76\x38\x30\x47\x37\x59\x57\x79\x5a\x51\x72\x77\x68"
+    "\x6b\x6c\x66\x57\x55\x31\x4a\x6e\x6b\x6e\x59\x78\x4e\x6d\x45\x69\x64\x33\x4f\x4e\x59\x6f\x4d"
+    "\x77\x6f\x6a\x34\x56\x33\x51\x69\x33\x68\x6f\x59\x57\x39\x6d\x31\x71\x42\x6e\x4f\x76\x55\x69"
+    "\x63\x55\x57\x49\x68\x4d\x63\x56\x42\x64\x69\x73\x39\x69\x67\x4d\x5a\x34\x79\x2d\x48\x7a\x6b"
+    "\x31\x35\x65\x42\x4d\x63\x58\x39\x6e\x63\x6a\x69\x6b\x74\x2d\x02\x48\xc3\x38\xcc\xd2\x66\x09"
+    "\x9c\xff\x03\x58\x20\xcc\x2f\xaa\xf2\x37\x92\x3e\xda\xa1\x1f\xc8\x7e\x3e\x60\xe5\x52\xf6\xa0"
+    "\xe9\x67\x78\xa7\x31\xda\x1e\xa9\xd6\x6c\x29\xd9\xa6\x5b\x04\x58\x41\x04\x4d\x7a\xcd\xb4\x1c"
+    "\x63\x70\x56\x43\x37\xc3\x7d\x20\xa3\x05\x64\x8d\x16\x6c\xa2\x41\xaa\x96\x90\xea\x9a\xde\xd2"
+    "\x01\xab\x3d\x93\x08\x13\x54\xd9\x99\x16\x18\x80\xfd\x3b\x21\x26\xf4\xbb\xdb\x43\x35\x80\xed"
+    "\x3e\x2b\xe2\x43\x5a\x31\xcf\xc6\x05\x5d\x3e\xfe\xb5\x05\x65\x4d\x69\x20\x41\x32\x06\x58\x20"
+    "\x35\x5a\xbb\xcf\x8f\x6a\xca\xaa\xda\x49\xee\x3d\xf0\xf3\xd6\x4c\x88\x35\x1b\xd6\x69\x51\x2f"
+    "\x98\xe6\xad\x8e\x80\xcf\x50\xb0\x09\x19\x03\xe7\xf5";
index cada7c982437c928850b18b0075ca315d106c98f..4e7ba42684e2ceb4a03b933933e7c3abd566e368 100644 (file)
  *  limitations under the License
  */
 
+#include "buffer.h"
+#include "buffer_view.h"
 #include "exception.h"
 #include "message.h"
+#include "message_examples.h"
+#include "wauthn_const_buff_equals.h"
 
 #include <ctap_message_processor.h>
 #include <gtest/gtest.h>
@@ -35,7 +39,7 @@ void AssertEq(const T &left, const uint8_t *rightData, size_t rightSize)
 }
 
 // from chromium/device/fido/fido_test_data.h
-constexpr uint8_t SAMPLE_MAKE_CREDENTIAL_REQUEST[] = {
+constexpr inline uint8_t SAMPLE_MAKE_CREDENTIAL_REQUEST[] = {
     // clang-format off
     // CTAP message type (not present in chromium, added in webauthn-ble)
     0x01,
@@ -125,7 +129,7 @@ constexpr uint8_t SAMPLE_MAKE_CREDENTIAL_REQUEST[] = {
 };
 
 // from chromium/device/fido/fido_test_data.h
-constexpr uint8_t SAMPLE_GET_ASSERTION_REQUEST[] = {
+constexpr inline uint8_t SAMPLE_GET_ASSERTION_REQUEST[] = {
     // clang-format off
     // CTAP message type (not present in chromium, added in webauthn-ble)
     0x01,
@@ -195,17 +199,9 @@ constexpr uint8_t SAMPLE_GET_ASSERTION_REQUEST[] = {
 
 TEST(Messages, ParsePostHandshakeMessage1)
 {
-    // example iPhone post handshake message received after handshake
-    const uint8_t blob[] =
-        "\xa1\x01\x58\x4f\xa5\x01\x82\x68\x46\x49\x44\x4f\x5f\x32\x5f\x30\x68\x46\x49\x44\x4f\x5f"
-        "\x32\x5f\x31\x02\x81\x69\x6c\x61\x72\x67\x65\x42\x6c\x6f\x62\x03\x50\xf2\x4a\x8e\x70\xd0"
-        "\xd3\xf8\x2c\x29\x37\x32\x52\x3c\xc4\xde\x5a\x04\xa2\x62\x72\x6b\xf5\x62\x75\x76\xf5\x09"
-        "\x82\x68\x69\x6e\x74\x65\x72\x6e\x61\x6c\x66\x68\x79\x62\x72\x69\x64";
-
-    BufferView view(blob, sizeof(blob) - 1);
-
     // read post handshake message
     PostHandshakeResponse msg;
+    auto view = BUFFER_VIEW(IPHONE_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE);
     ASSERT_NO_THROW(msg.Deserialize(view));
 
     AssertEq(msg.m_versions, {"FIDO_2_0", "FIDO_2_1"});
@@ -253,17 +249,9 @@ TEST(Messages, ParsePostHandshakeMessage1)
 
 TEST(Messages, ParsePostHandshakeMessage2)
 {
-    // example Android post handshake message received after handshake
-    const uint8_t blob[] =
-        "\xa1\x01\x58\x53\xa5\x01\x82\x68\x46\x49\x44\x4f\x5f\x32\x5f\x30\x68\x46\x49\x44\x4f\x5f"
-        "\x32\x5f\x31\x02\x81\x63\x70\x72\x66\x03\x50\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-        "\x00\x00\x00\x00\x00\x04\xa4\x62\x72\x6b\xf5\x62\x75\x70\xf5\x62\x75\x76\xf5\x64\x70\x6c"
-        "\x61\x74\xf4\x09\x82\x68\x69\x6e\x74\x65\x72\x6e\x61\x6c\x66\x68\x79\x62\x72\x69\x64";
-
-    BufferView view(blob, sizeof(blob) - 1);
-
     // read post handshake message
     PostHandshakeResponse msg;
+    auto view = BUFFER_VIEW(ANDROID_EXAMPLE_POST_HANDSHAKE_RAW_RESPONSE);
     ASSERT_NO_THROW(msg.Deserialize(view));
 
     AssertEq(msg.m_versions, {"FIDO_2_0", "FIDO_2_1"});
@@ -556,24 +544,13 @@ TEST(Messages, ShutdownMessage)
 
 TEST(Messages, ParseMakeCredentialResponse1)
 {
-    // example make credential response received from iPhone authenticator (w/o CTAP message type
-    // prefix)
-    constexpr uint8_t blob[] =
-        "\x00\xa4\x01\x64\x6e\x6f\x6e\x65\x02\x58\x98\x11\x94\x22\x8d\xa8\xfd\xbd\xee\xfd\x26\x1b"
-        "\xd7\xb6\x59\x5c\xfd\x70\xa5\x0d\x70\xc6\x40\x7b\xcf\x01\x3d\xe9\x6d\x4e\xfb\x17\xde\x5d"
-        "\x00\x00\x00\x00\xfb\xfc\x30\x07\x15\x4e\x4e\xcc\x8c\x0b\x6e\x02\x05\x57\xd7\xbd\x00\x14"
-        "\x92\xa5\x07\x16\x15\x56\x78\x93\x64\x86\x22\xaf\xca\x66\x3d\x24\x50\x13\x8c\x58\xa5\x01"
-        "\x02\x03\x26\x20\x01\x21\x58\x20\xc8\x39\x05\x61\x42\x33\x3e\x54\xcd\x7d\xed\x55\xb7\x64"
-        "\xbd\x24\xb8\x6b\x31\x67\x07\x4d\x37\x28\x0a\xbe\x86\x37\x13\x6c\x71\xca\x22\x58\x20\x35"
-        "\x57\xa0\x4e\x67\x8b\x96\x87\x3c\x00\xc2\x97\xf7\xf3\x5b\x60\x97\x2a\x18\x06\x48\xe5\x5a"
-        "\xcb\x33\xbe\x8d\x47\x17\x56\x5a\xd0\x03\xa0\x06\xa0";
-
-    BufferView view(blob, sizeof(blob) - 1);
-
-    // read post handshake message
     MakeCredentialResponse msg;
+    auto view = BUFFER_VIEW(IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE);
+    view.remove_prefix(1); // remove CTAP message type prefix
     ASSERT_NO_THROW(msg.Deserialize(view));
 
+    const auto *blob =
+        reinterpret_cast<const uint8_t *>(IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE) + 1;
     ASSERT_EQ(msg.m_authDataRaw.size(), 152);
     AssertEq(msg.m_authDataRaw, blob + 11, msg.m_authDataRaw.size());
     AssertEq(msg.m_authData.m_rpIdHash, blob + 11, 32);
@@ -581,7 +558,8 @@ TEST(Messages, ParseMakeCredentialResponse1)
     ASSERT_EQ(msg.m_authData.m_flags, blob[43]);
     ASSERT_TRUE(msg.m_authData.m_attestationData.has_value());
     AssertEq(msg.m_authData.m_attestationData->m_credentialId, blob + 66, 20);
-    ASSERT_FALSE(msg.m_authData.m_attestationData->m_publicKeyDer.empty());
+    ASSERT_EQ(msg.m_authData.m_attestationData->m_publicKeyDer,
+              BUFFER(IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE_PUBLIC_KEY_DER));
     ASSERT_EQ(msg.m_authData.m_attestationData->m_alg,
               WAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256);
     AssertEq(msg.m_attestationObject, view.data(), view.size());
@@ -592,25 +570,13 @@ TEST(Messages, ParseMakeCredentialResponse1)
 
 TEST(Messages, ParseMakeCredentialResponse2)
 {
-    // example make credential response received from Android authenticator (w/o CTAP message type
-    // prefix)
-    constexpr uint8_t blob[] =
-        "\x00\xa3\x01\x64\x6e\x6f\x6e\x65\x02\x58\xa4\x11\x94\x22\x8d\xa8\xfd\xbd\xee\xfd\x26\x1b"
-        "\xd7\xb6\x59\x5c\xfd\x70\xa5\x0d\x70\xc6\x40\x7b\xcf\x01\x3d\xe9\x6d\x4e\xfb\x17\xde\x45"
-        "\x00\x00\x00\x00\x53\x41\x4d\x53\x55\x4e\x47\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x20"
-        "\x8c\x5b\x4d\x2f\x62\x5f\xa0\xcf\x61\x44\xba\x1a\xa3\x2f\x5b\x4b\x65\xff\x21\x7c\xcb\x0d"
-        "\x24\x10\x16\xa3\xb8\xc8\x2c\x94\xe6\x7f\xa5\x01\x02\x03\x26\x20\x01\x21\x58\x20\xf8\xa3"
-        "\xeb\x21\x36\xfa\xc3\xb0\xce\xae\xb5\x04\x54\x59\xff\xc1\x01\xb1\x64\xc6\x37\xf1\x42\x66"
-        "\x36\x48\x25\x68\x55\xe8\x7b\xb2\x22\x58\x20\x9b\xca\x03\x74\x3c\x7f\x9a\x4e\x64\x4b\xeb"
-        "\xb8\x8a\xa2\x15\x59\x20\x92\x86\x60\xa6\x65\x13\xad\x84\x9c\x1c\x15\xe0\x41\x7b\xe5\x03"
-        "\xa0";
-
-    BufferView view(blob, sizeof(blob) - 1);
-
-    // read post handshake message
     MakeCredentialResponse msg;
+    auto view = BUFFER_VIEW(ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE);
+    view.remove_prefix(1); // remove CTAP message type prefix
     ASSERT_NO_THROW(msg.Deserialize(view));
 
+    const auto *blob =
+        reinterpret_cast<const uint8_t *>(ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE) + 1;
     ASSERT_EQ(msg.m_authDataRaw.size(), 164);
     AssertEq(msg.m_authDataRaw, blob + 11, msg.m_authDataRaw.size());
     AssertEq(msg.m_authData.m_rpIdHash, blob + 11, 32);
@@ -618,7 +584,8 @@ TEST(Messages, ParseMakeCredentialResponse2)
     ASSERT_EQ(msg.m_authData.m_flags, blob[43]);
     ASSERT_TRUE(msg.m_authData.m_attestationData.has_value());
     AssertEq(msg.m_authData.m_attestationData->m_credentialId, blob + 66, 32);
-    ASSERT_FALSE(msg.m_authData.m_attestationData->m_publicKeyDer.empty());
+    ASSERT_EQ(msg.m_authData.m_attestationData->m_publicKeyDer,
+              BUFFER(ANDROID_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE_PUBLIC_KEY_DER));
     ASSERT_EQ(msg.m_authData.m_attestationData->m_alg,
               WAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256);
     AssertEq(msg.m_attestationObject, view.data(), view.size());
@@ -629,22 +596,13 @@ TEST(Messages, ParseMakeCredentialResponse2)
 
 TEST(Messages, ParseGetAssertionResponse)
 {
-    // sample response received from iPhone authenticator during manual testing
-    constexpr uint8_t blob[] =
-        "\x00\xa5\x01\xa2\x62\x69\x64\x54\xc7\x3c\x11\x45\x4b\x19\x08\xe5\x2c\x16\x8d\x45\x81\xf5"
-        "\x9f\xb1\x15\x00\xbf\x51\x64\x74\x79\x70\x65\x6a\x70\x75\x62\x6c\x69\x63\x2d\x6b\x65\x79"
-        "\x02\x58\x25\x11\x94\x22\x8d\xa8\xfd\xbd\xee\xfd\x26\x1b\xd7\xb6\x59\x5c\xfd\x70\xa5\x0d"
-        "\x70\xc6\x40\x7b\xcf\x01\x3d\xe9\x6d\x4e\xfb\x17\xde\x1d\x00\x00\x00\x00\x03\x58\x48\x30"
-        "\x46\x02\x21\x00\xe2\x6e\xb7\xab\x29\x48\xcb\x1c\x72\xc9\xe2\x91\xc9\x74\xff\x2b\x44\xd9"
-        "\xdf\x8a\xb2\x3c\xc9\x5a\xc0\xe7\xb8\x65\xc4\xe5\xde\x08\x02\x21\x00\xb1\xea\x2c\x5f\xf3"
-        "\x41\x71\x0f\x18\x04\x77\xe1\x73\xf9\xbc\xac\x0f\x01\x14\x91\x33\x79\xca\xe2\x87\x22\xbb"
-        "\x8b\xda\xe2\x77\x22\x04\xa3\x62\x69\x64\x48\x00\x01\x02\x03\x04\x05\x06\x07\x64\x6e\x61"
-        "\x6d\x65\x60\x6b\x64\x69\x73\x70\x6c\x61\x79\x4e\x61\x6d\x65\x60\x08\xa0";
-    BufferView view(blob, sizeof(blob) - 1);
-
     GetAssertionResponse msg;
+    auto view = BUFFER_VIEW(IPHONE_EXAMPLE_GET_ASSERTION_RAW_RESPONSE);
+    view.remove_prefix(1); // remove CTAP message type prefix
     ASSERT_NO_THROW(msg.Deserialize(view));
 
+    const auto *blob =
+        reinterpret_cast<const uint8_t *>(IPHONE_EXAMPLE_GET_ASSERTION_RAW_RESPONSE) + 1;
     ASSERT_GE(msg.m_authDataRaw.size(), 37);
     AssertEq(msg.m_authDataRaw, blob + 47, msg.m_authDataRaw.size());
     AssertEq(msg.m_authData.m_rpIdHash, blob + 47, 32);
@@ -660,40 +618,16 @@ TEST(Messages, ParseGetAssertionResponse)
 
 TEST(Messages, ParseUpdateMessage)
 {
-    // sample value received from an Android authenticator during manual testing
-    constexpr uint8_t blob[] =
-        "\xa2\x00\x58\xaf\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-        "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-        "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-        "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-        "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-        "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-        "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-        "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
-        "\x00\x00\x00\x01\xa7\x01\x58\x98\x64\x2d\x51\x6b\x57\x6e\x54\x6d\x77\x53\x45\x3a\x41\x50"
-        "\x41\x39\x31\x62\x48\x76\x72\x44\x72\x39\x53\x64\x44\x31\x71\x39\x4a\x69\x4d\x55\x78\x52"
-        "\x38\x57\x30\x41\x5a\x48\x57\x61\x56\x31\x4d\x46\x4b\x36\x77\x4f\x56\x78\x70\x76\x38\x30"
-        "\x47\x37\x59\x57\x79\x5a\x51\x72\x77\x68\x6b\x6c\x66\x57\x55\x31\x4a\x6e\x6b\x6e\x59\x78"
-        "\x4e\x6d\x45\x69\x64\x33\x4f\x4e\x59\x6f\x4d\x77\x6f\x6a\x34\x56\x33\x51\x69\x33\x68\x6f"
-        "\x59\x57\x39\x6d\x31\x71\x42\x6e\x4f\x76\x55\x69\x63\x55\x57\x49\x68\x4d\x63\x56\x42\x64"
-        "\x69\x73\x39\x69\x67\x4d\x5a\x34\x79\x2d\x48\x7a\x6b\x31\x35\x65\x42\x4d\x63\x58\x39\x6e"
-        "\x63\x6a\x69\x6b\x74\x2d\x02\x48\xc3\x38\xcc\xd2\x66\x09\x9c\xff\x03\x58\x20\xcc\x2f\xaa"
-        "\xf2\x37\x92\x3e\xda\xa1\x1f\xc8\x7e\x3e\x60\xe5\x52\xf6\xa0\xe9\x67\x78\xa7\x31\xda\x1e"
-        "\xa9\xd6\x6c\x29\xd9\xa6\x5b\x04\x58\x41\x04\x4d\x7a\xcd\xb4\x1c\x63\x70\x56\x43\x37\xc3"
-        "\x7d\x20\xa3\x05\x64\x8d\x16\x6c\xa2\x41\xaa\x96\x90\xea\x9a\xde\xd2\x01\xab\x3d\x93\x08"
-        "\x13\x54\xd9\x99\x16\x18\x80\xfd\x3b\x21\x26\xf4\xbb\xdb\x43\x35\x80\xed\x3e\x2b\xe2\x43"
-        "\x5a\x31\xcf\xc6\x05\x5d\x3e\xfe\xb5\x05\x65\x4d\x69\x20\x41\x32\x06\x58\x20\x35\x5a\xbb"
-        "\xcf\x8f\x6a\xca\xaa\xda\x49\xee\x3d\xf0\xf3\xd6\x4c\x88\x35\x1b\xd6\x69\x51\x2f\x98\xe6"
-        "\xad\x8e\x80\xcf\x50\xb0\x09\x19\x03\xe7\xf5";
-    BufferView view(blob, sizeof(blob) - 1);
-
     UpdateMessage msg;
+    auto view = BUFFER_VIEW(ANDROID_EXAMPLE_RAW_UPDATE_MSG);
+    view.remove_prefix(1); // remove CTAP message type prefix
     ASSERT_NO_THROW(msg.Deserialize(view));
 
     auto linkDataOpt = msg.GetLinkData();
     ASSERT_TRUE(linkDataOpt.has_value());
     const auto &linkData = *linkDataOpt;
 
+    const auto *blob = reinterpret_cast<const uint8_t *>(ANDROID_EXAMPLE_RAW_UPDATE_MSG) + 1;
     AssertEq(linkData.m_contactId, blob + 184, 152);
     AssertEq(linkData.m_linkId, blob + 338, 8);
     AssertEq(linkData.m_linkSecret, blob + 349, 32);
index f94e0a4beb715542e6d4a13535e0f39cb05bf1a9..cc1b4b17c91219a98c0d634384e9123e6eedc46d 100644 (file)
@@ -23,10 +23,9 @@ TEST(QrCodeShower, callback_is_called)
 {
     QrCodeShower qrCodeShower;
     int calledNumTimes = 0;
-    static constexpr auto callback = [](const char *qrContents, void *userData) {
-        EXPECT_THAT(qrContents, testing::StartsWith("FIDO:/"));
-        ++*static_cast<int *>(userData);
-    };
-    qrCodeShower.ShowQrCode({}, {}, {'m', 'c'}, true, callback, &calledNumTimes);
+    qrCodeShower.ShowQrCode({}, {}, {'m', 'c'}, true, [&](std::string &&contents) {
+        EXPECT_THAT(contents, testing::StartsWith("FIDO:/"));
+        ++calledNumTimes;
+    });
     EXPECT_EQ(calledNumTimes, 1);
 }
index d13347923c00494f0d51d917b5070c0b09f29d6b..db40d19cf115fce4be77665f6577e7525e431871 100644 (file)
@@ -60,12 +60,11 @@ class MQrCodeShower : public IQrCodeShower, public Tester {
 public:
     explicit MQrCodeShower(TestState &testState) : Tester{testState} {}
 
-    void ShowQrCode(const CryptoBuffer & /*qrSecret*/,
-                    const CryptoBuffer & /*identityKeyCompressed*/,
-                    const Hint & /*hint*/,
-                    bool /*stateAssisted*/,
-                    wauthn_cb_display_qrcode /*displayQrCodeCallback*/,
-                    void * /*displayQrCodeCallbackUserData*/) override
+    void ShowQrCode(const CryptoBuffer &,
+                    const CryptoBuffer &,
+                    const Hint &,
+                    bool,
+                    std::function<void(std::string &&)> &&) override
     {
         SCOPED_TRACE("");
         UpdateAndCheckState(Event::QR_CODE_STARTED, Event::QR_CODE_ENDED, false);
@@ -120,30 +119,31 @@ TEST(QrTransaction, Cancel)
 
         TestState testState;
         auto makeTransaction = [&] {
-            return QrTransaction(
-                nullptr,
-                nullptr,
+            auto transaction = std::make_unique<QrTransaction>();
+            transaction->Initialize(
                 {},
+                [](std::string &&) {},
                 std::make_unique<MQrCodeShower>(testState),
                 std::make_unique<
                     MBtAdvertScanner<Event, CancelCalledOn, &GenerateDecryptedBluetoothAdvert>>(
                     testState),
                 std::make_unique<MHandshake>(testState),
                 std::make_unique<MCtapMessageProcessor<Event, CancelCalledOn>>(testState));
+            return transaction;
         };
-        // Before PerformTransaction()
+        // Before Perform()
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::RESET, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::RESET, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::RESET);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
         }
         // In MQrCodeShower
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::QR_CODE_STARTED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::QR_CODE_STARTED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::QR_CODE_STARTED);
             // QrCodeShower has no Cancel()
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
@@ -151,8 +151,8 @@ TEST(QrTransaction, Cancel)
         // After MQrCodeShower
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::QR_CODE_ENDED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::QR_CODE_ENDED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::QR_CODE_ENDED);
             // QrCodeShower has no Cancel()
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
@@ -160,72 +160,72 @@ TEST(QrTransaction, Cancel)
         // In MBleAdvertScanner
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::AWAITING_BLE_ADVERT_STARTED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::AWAITING_BLE_ADVERT_STARTED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::AWAITING_BLE_ADVERT_STARTED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::BLE_ADVERT);
         }
         // After MBleAdvertScanner
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::AWAITING_BLE_ADVERT_ENDED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::AWAITING_BLE_ADVERT_ENDED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::AWAITING_BLE_ADVERT_ENDED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::BLE_ADVERT);
         }
         // In MHandshake::QrInitiatedConnect()
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::CONNECTING_STARTED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::CONNECTING_STARTED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::CONNECTING_STARTED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::HANDSHAKE);
         }
         // After MHandshake::QrInitiatedConnect()
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::CONNECTING_ENDED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::CONNECTING_ENDED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::CONNECTING_ENDED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::HANDSHAKE);
         }
         // In MHandshake::DoQrInitiatedHandshake()
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::HANDSHAKE_STARTED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::HANDSHAKE_STARTED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::HANDSHAKE_STARTED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::HANDSHAKE);
         }
         // After MHandshake::DoQrInitiatedHandshake()
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::HANDSHAKE_ENDED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::HANDSHAKE_ENDED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::HANDSHAKE_ENDED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::HANDSHAKE);
         }
         // In MCtapMessageProcessor
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::MC_OR_GA_STARTED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::MC_OR_GA_STARTED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::MC_OR_GA_STARTED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::CTAP_MESSAGE_PROCESSOR);
         }
         // After MCtapMessageProcessor
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::MC_OR_GA_ENDED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::MC_OR_GA_ENDED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::MC_OR_GA_ENDED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::CTAP_MESSAGE_PROCESSOR);
         }
         // No Cancel
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::FINISHED, transaction);
-            EXPECT_NO_THROW(transaction.PerformTransaction(clientData, options));
+            testState.Reset(Event::FINISHED, *transaction);
+            EXPECT_NO_THROW(transaction->Perform(clientData, options));
             EXPECT_EQ(testState.m_lastEvent, Event::MC_OR_GA_ENDED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
         }
@@ -242,8 +242,7 @@ public:
                     const CryptoBuffer &,
                     const Hint &,
                     bool,
-                    wauthn_cb_display_qrcode,
-                    void *) override
+                    std::function<void(std::string &&)> &&) override
     {
         m_cancelFacilitator.CancelCheck();
     }
@@ -286,24 +285,61 @@ private:
 
 } // namespace
 
-TEST(QrTransaction, cancel_from_the_other_thread)
+TEST(QrTransaction, CancelFromTheOtherThread)
 {
     auto makeTransaction = [&] {
-        return QrTransaction(
-            nullptr,
-            nullptr,
+        auto transaction = std::make_unique<QrTransaction>();
+        transaction->Initialize(
             {},
+            [](std::string &&) {},
             std::make_unique<CancellationMQrCodeShower>(),
             std::make_unique<CancellationMBtAdvertScanner<&GenerateDecryptedBluetoothAdvert>>(),
             std::make_unique<CancellationMHandshake>(),
             std::make_unique<CancellationMCtapMessageProcessor>());
+        return transaction;
     };
     TestCancelFromTheOtherThread<ITransaction>(
         400, 40, makeTransaction, [](ITransaction &transaction) {
             auto clientData = wauthn_client_data_s{};
             auto mcOptions = wauthn_pubkey_cred_creation_options_s{};
-            transaction.PerformTransaction(clientData, mcOptions);
+            transaction.Perform(clientData, mcOptions);
             auto gaOptions = wauthn_pubkey_cred_request_options_s{};
-            transaction.PerformTransaction(clientData, gaOptions);
+            transaction.Perform(clientData, gaOptions);
+        });
+}
+
+namespace {
+
+class ShouldNotBeCalledMQrCodeShower : public IQrCodeShower {
+public:
+    void ShowQrCode(const CryptoBuffer &,
+                    const CryptoBuffer &,
+                    const Hint &,
+                    bool,
+                    std::function<void(std::string &&)> &&) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+};
+
+} // namespace
+
+TEST(QrTransaction, ProcessFollowingUpdateMsgs)
+{
+    TestTransactionProcessFollowingUpdateMsgs(
+        [](std::unique_ptr<ICtapMessageProcessor> &&ctapMessageProcessor) {
+            auto transaction = std::make_unique<QrTransaction>();
+            transaction->Initialize(
+                {},
+                [](std::string &&) {
+                    ADD_FAILURE() << "This should not be called";
+                    throw std::runtime_error{""};
+                },
+                std::make_unique<ShouldNotBeCalledMQrCodeShower>(),
+                std::make_unique<ShouldNotBeCalledMBtAdvertScanner>(),
+                std::make_unique<ShouldNotBeCalledMHandshake>(),
+                std::move(ctapMessageProcessor));
+            return transaction;
         });
 }
diff --git a/tests/request_handler_tests.cpp b/tests/request_handler_tests.cpp
new file mode 100644 (file)
index 0000000..a3e29e3
--- /dev/null
@@ -0,0 +1,1378 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+#include "buffer.h"
+#include "buffer_view.h"
+#include "message_examples.h"
+#include "request_handler.h"
+#include "test_cancel_from_the_other_thread.h"
+#include "to_wauthn_const_buff.h"
+#include "wauthn_const_buff_equals.h"
+
+#include <gtest/gtest.h>
+#include <optional>
+#include <tuple>
+#include <webauthn-types.h>
+
+namespace {
+
+class ShouldNotBeUsedMQrTransaction : public IQrTransaction {
+public:
+    ShouldNotBeUsedMQrTransaction() noexcept = default;
+
+    void Initialize(Hint &&,
+                    std::function<void(std::string &&)> &&,
+                    std::unique_ptr<IQrCodeShower> &&,
+                    IBtAdvertScannerUPtr &&,
+                    std::unique_ptr<IHandshake> &&,
+                    std::unique_ptr<ICtapMessageProcessor> &&) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    MCResult Perform(const wauthn_client_data_s &,
+                     const wauthn_pubkey_cred_creation_options_s &) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    GAResult Perform(const wauthn_client_data_s &,
+                     const wauthn_pubkey_cred_request_options_s &) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    void ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)>) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    void Cancel() override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+};
+
+class ShouldNotBeUsedMStateAssistedTransaction : public IStateAssistedTransaction {
+public:
+    ShouldNotBeUsedMStateAssistedTransaction() noexcept = default;
+
+    void Initialize(Hint &&,
+                    std::unique_ptr<IHandshake> &&,
+                    IBtAdvertScannerUPtr &&,
+                    std::unique_ptr<ICtapMessageProcessor> &&) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    MCResult Perform(const wauthn_client_data_s &,
+                     const wauthn_pubkey_cred_creation_options_s &) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    GAResult Perform(const wauthn_client_data_s &,
+                     const wauthn_pubkey_cred_request_options_s &) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    void ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)>) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    void Cancel() override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+};
+
+CtapResponse::AuthData::AttestationData ExampleAttestationData()
+{
+    auto rawAttestationData =
+        BUFFER_VIEW(IPHONE_EXAMPLE_MAKE_CREDENTIAL_RAW_RESPONSE_ATTESTATION_DATA);
+    return CtapResponse::AuthData::AttestationData(rawAttestationData);
+}
+
+ICtapMessageProcessor::MCResult ExampleCtapMCResult()
+{
+    ICtapMessageProcessor::MCResult res;
+    res.response.m_authData.m_attestationData = ExampleAttestationData();
+    return res;
+}
+
+class GenericMQrTransaction : public IQrTransaction {
+public:
+    struct Options {
+        bool callQrCallback = false;
+        std::string qrCodeContents;
+        ICtapMessageProcessor::MCResult ctapMCResult = ExampleCtapMCResult();
+        ICtapMessageProcessor::GAResult ctapGAResult;
+        Buffer identityKey;
+        std::string tunnelServerDomain;
+        std::function<void(MCResult &)> mcResultProcessor = [](MCResult &) {};
+        std::function<void(GAResult &)> gaResultProcessor = [](GAResult &) {};
+        std::function<void(std::function<void(UpdateMessage &&)>)> followingCtapUpdatesProcessor =
+            [](std::function<void(UpdateMessage &&)> &&) {
+            }; // simulates no following update messages
+    };
+
+    explicit GenericMQrTransaction(Options &&options) noexcept : m_options{std::move(options)} {}
+
+    void Initialize(Hint &&hint,
+                    std::function<void(std::string &&)> &&qrCodeCallback,
+                    std::unique_ptr<IQrCodeShower> && /*qrCodeShower*/,
+                    IBtAdvertScannerUPtr && /*btAdvertScanner*/,
+                    std::unique_ptr<IHandshake> && /*handshake*/,
+                    std::unique_ptr<ICtapMessageProcessor> && /*ctapMessageProcessor*/) override
+    {
+        m_hint = std::move(hint);
+        m_qrCodeCallback = std::move(qrCodeCallback);
+    }
+
+    MCResult Perform(const wauthn_client_data_s &,
+                     const wauthn_pubkey_cred_creation_options_s &) override
+    {
+        EXPECT_EQ(m_hint, (Hint{'m', 'c'}));
+        if (m_options.callQrCallback)
+            m_qrCodeCallback(std::move(m_options.qrCodeContents));
+        auto res = MCResult{
+            m_options.ctapMCResult,
+            m_options.identityKey,
+            m_options.tunnelServerDomain,
+        };
+        m_options.mcResultProcessor(res);
+        return res;
+    }
+
+    GAResult Perform(const wauthn_client_data_s &,
+                     const wauthn_pubkey_cred_request_options_s &) override
+    {
+        EXPECT_EQ(m_hint, (Hint{'g', 'a'}));
+        if (m_options.callQrCallback)
+            m_qrCodeCallback(std::move(m_options.qrCodeContents));
+        auto res = GAResult{
+            m_options.ctapGAResult,
+            m_options.identityKey,
+            m_options.tunnelServerDomain,
+        };
+        m_options.gaResultProcessor(res);
+        return res;
+    }
+
+    void
+    ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)> updateMsgCallback) override
+    {
+        m_options.followingCtapUpdatesProcessor(updateMsgCallback);
+    }
+
+    void Cancel() override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+private:
+    Options m_options;
+    Hint m_hint = {};
+    std::function<void(std::string &&)> m_qrCodeCallback;
+};
+
+class GenericMStateAssistedTransaction : public IStateAssistedTransaction {
+public:
+    struct Options {
+        ICtapMessageProcessor::MCResult ctapMCResult = ExampleCtapMCResult();
+        ICtapMessageProcessor::GAResult ctapGAResult;
+        Buffer identityKey;
+        std::string tunnelServerDomain;
+        std::function<void(MCResult &)> mcResultProcessor = [](MCResult &) {};
+        std::function<void(GAResult &)> gaResultProcessor = [](GAResult &) {};
+        std::function<void(std::function<void(UpdateMessage &&)>)> followingCtapUpdatesProcessor =
+            [](std::function<void(UpdateMessage &&)> &&) {
+            }; // simulates no following update messages
+    };
+
+    explicit GenericMStateAssistedTransaction(Options &&options) noexcept
+    : m_options{std::move(options)}
+    {
+    }
+
+    void Initialize(Hint &&hint,
+                    std::unique_ptr<IHandshake> && /*handshake*/,
+                    IBtAdvertScannerUPtr && /*btAdvertScanner*/,
+                    std::unique_ptr<ICtapMessageProcessor> && /*ctapMessageProcessor*/) override
+    {
+        m_hint = std::move(hint);
+    }
+
+    MCResult Perform(const wauthn_client_data_s &,
+                     const wauthn_pubkey_cred_creation_options_s &) override
+    {
+        EXPECT_EQ(m_hint, (Hint{'m', 'c'}));
+        auto res = MCResult{
+            m_options.ctapMCResult,
+            m_options.identityKey,
+            m_options.tunnelServerDomain,
+        };
+        m_options.mcResultProcessor(res);
+        return res;
+    }
+
+    GAResult Perform(const wauthn_client_data_s &,
+                     const wauthn_pubkey_cred_request_options_s &) override
+    {
+        EXPECT_EQ(m_hint, (Hint{'g', 'a'}));
+        auto res = GAResult{
+            m_options.ctapGAResult,
+            m_options.identityKey,
+            m_options.tunnelServerDomain,
+        };
+        m_options.gaResultProcessor(res);
+        return res;
+    }
+
+    void
+    ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)> updateMsgCallback) override
+    {
+        m_options.followingCtapUpdatesProcessor(updateMsgCallback);
+    }
+
+    void Cancel() override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+private:
+    Options m_options;
+    Hint m_hint = {};
+};
+
+struct CommonTest {
+    wauthn_client_data_s clientData = {};
+    wauthn_client_data_s *clientDataPtr = &clientData;
+};
+
+struct MakeCredentialTest : public CommonTest {
+    wauthn_pubkey_cred_creation_options_s options = {};
+    wauthn_pubkey_cred_creation_options_s *optionsPtr = &options;
+};
+
+struct GetAssertionTest : public CommonTest {
+    wauthn_pubkey_cred_request_options_s options = {};
+    wauthn_pubkey_cred_request_options_s *optionsPtr = &options;
+};
+
+template <class... Tests>
+struct QrInitiatedTest : public Tests... {};
+
+struct LinkedData {
+    wauthn_const_buffer_s contactId = ToWauthnConstBuff("contact id");
+    wauthn_const_buffer_s linkId = ToWauthnConstBuff("link id");
+    wauthn_const_buffer_s linkSecret = ToWauthnConstBuff("link secret");
+    wauthn_const_buffer_s authenticatorPubKey = ToWauthnConstBuff("authenticator public key");
+    wauthn_const_buffer_s authenticatorName = ToWauthnConstBuff("authenticator name");
+    wauthn_const_buffer_s signature = ToWauthnConstBuff("signature");
+    wauthn_const_buffer_s tunnelServerDomain = ToWauthnConstBuff("tunnel server domain");
+    wauthn_const_buffer_s identityKey = ToWauthnConstBuff("identity key");
+
+    wauthn_hybrid_linked_data_s linkedData{
+        &contactId,
+        &linkId,
+        &linkSecret,
+        &authenticatorPubKey,
+        &authenticatorName,
+        &signature,
+        &tunnelServerDomain,
+        &identityKey,
+    };
+};
+
+template <class... Tests>
+struct StateAssistedTest : public Tests... {
+    LinkedData linkedData;
+
+    StateAssistedTest() noexcept { this->options.linked_device = &linkedData.linkedData; }
+};
+
+template <template <class> class Test, class PreTransactionCallback, class PostTransactionCallback>
+void TestQrTransactionMakeCredential(const PreTransactionCallback &preTransactionCallback,
+                                     const PostTransactionCallback &postTransactionCallback)
+{
+    SCOPED_TRACE("QR-initiated MakeCredential");
+    QrInitiatedTest<MakeCredentialTest, Test<wauthn_mc_callbacks_s>> test;
+    GenericMQrTransaction::Options options;
+    preTransactionCallback(test, options);
+    RequestHandler::Instance().Process(test.clientDataPtr,
+                                       Request<RequestKind::MC>{test.optionsPtr, test.callbacksPtr},
+                                       GenericMQrTransaction{std::move(options)},
+                                       ShouldNotBeUsedMStateAssistedTransaction{});
+    postTransactionCallback(test);
+}
+
+template <template <class> class Test, class PreTransactionCallback, class PostTransactionCallback>
+void TestQrTransactionGetAssertion(const PreTransactionCallback &preTransactionCallback,
+                                   const PostTransactionCallback &postTransactionCallback)
+{
+    SCOPED_TRACE("QR-initiated GetAssertion");
+    QrInitiatedTest<GetAssertionTest, Test<wauthn_ga_callbacks_s>> test;
+    GenericMQrTransaction::Options options;
+    preTransactionCallback(test, options);
+    RequestHandler::Instance().Process(test.clientDataPtr,
+                                       Request<RequestKind::GA>{test.optionsPtr, test.callbacksPtr},
+                                       GenericMQrTransaction{std::move(options)},
+                                       ShouldNotBeUsedMStateAssistedTransaction{});
+    postTransactionCallback(test);
+}
+
+template <template <class> class Test, class PreTransactionCallback, class PostTransactionCallback>
+void TestBothQrTransactions(const PreTransactionCallback &preTransactionCallback,
+                            const PostTransactionCallback &postTransactionCallback)
+{
+    TestQrTransactionMakeCredential<Test>(preTransactionCallback, postTransactionCallback);
+    TestQrTransactionGetAssertion<Test>(preTransactionCallback, postTransactionCallback);
+}
+
+template <template <class> class Test, class PreTransactionCallback, class PostTransactionCallback>
+void TestStateAssistedTransactionMakeCredential(
+    const PreTransactionCallback &preTransactionCallback,
+    const PostTransactionCallback &postTransactionCallback)
+{
+    SCOPED_TRACE("state-assisted MakeCredential");
+    StateAssistedTest<MakeCredentialTest, Test<wauthn_mc_callbacks_s>> test;
+    GenericMStateAssistedTransaction::Options options;
+    preTransactionCallback(test, options);
+    RequestHandler::Instance().Process(test.clientDataPtr,
+                                       Request<RequestKind::MC>{test.optionsPtr, test.callbacksPtr},
+                                       ShouldNotBeUsedMQrTransaction{},
+                                       GenericMStateAssistedTransaction{std::move(options)});
+    postTransactionCallback(test);
+}
+
+template <template <class> class Test, class PreTransactionCallback, class PostTransactionCallback>
+void TestStateAssistedTransactionGetAssertion(
+    const PreTransactionCallback &preTransactionCallback,
+    const PostTransactionCallback &postTransactionCallback)
+{
+    SCOPED_TRACE("state-assisted GetAssertion");
+    StateAssistedTest<GetAssertionTest, Test<wauthn_ga_callbacks_s>> test;
+    GenericMStateAssistedTransaction::Options options;
+    preTransactionCallback(test, options);
+    RequestHandler::Instance().Process(test.clientDataPtr,
+                                       Request<RequestKind::GA>{test.optionsPtr, test.callbacksPtr},
+                                       ShouldNotBeUsedMQrTransaction{},
+                                       GenericMStateAssistedTransaction{std::move(options)});
+    postTransactionCallback(test);
+}
+
+template <template <class> class Test, class PreTransactionCallback, class PostTransactionCallback>
+void TestBothStateAssistedTransactions(const PreTransactionCallback &preTransactionCallback,
+                                       const PostTransactionCallback &postTransactionCallback)
+{
+    TestStateAssistedTransactionMakeCredential<Test>(preTransactionCallback,
+                                                     postTransactionCallback);
+    TestStateAssistedTransactionGetAssertion<Test>(preTransactionCallback, postTransactionCallback);
+}
+
+template <template <class> class Test, class PreTransactionCallback, class PostTransactionCallback>
+void TestAll4Transactions(const PreTransactionCallback &preTransactionCallback,
+                          const PostTransactionCallback &postTransactionCallback)
+{
+    TestBothQrTransactions<Test>(preTransactionCallback, postTransactionCallback);
+    TestBothStateAssistedTransactions<Test>(preTransactionCallback, postTransactionCallback);
+}
+
+template <class WauthnCallbacks>
+struct InvalidParamsTest {
+    std::string called;
+    WauthnCallbacks callbacks = {
+        [](const char *, void *) {
+            FAIL() << "qrcode_callback should not be called when parameters are invalid;";
+        },
+        [](const auto *pubkeyCred, wauthn_error_e result, void *data) {
+            auto &test = *static_cast<InvalidParamsTest *>(data);
+            test.called += "response_callback;";
+            EXPECT_EQ(result, WAUTHN_ERROR_INVALID_PARAMETER);
+            EXPECT_EQ(pubkeyCred, nullptr);
+        },
+        [](const auto *, wauthn_error_e, void *) {
+            FAIL() << "linked_data_callback should not be called when parameters are invalid;";
+        },
+        this,
+    };
+    WauthnCallbacks *callbacksPtr = &callbacks;
+};
+
+} // namespace
+
+TEST(RequestHandler, transaction_is_not_performed_with_nullptr_callbacks)
+{
+    TestAll4Transactions<InvalidParamsTest>(
+        [](auto &test, auto &options) {
+            test.callbacksPtr = nullptr;
+            options.mcResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.gaResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.followingCtapUpdatesProcessor = [&](auto &&) {
+                test.called += "update_processor;";
+            };
+        },
+        [](auto &test) { EXPECT_EQ(test.called, ""); });
+}
+
+TEST(RequestHandler, qr_initiated_transaction_is_not_performed_with_nullptr_qrcode_callback)
+{
+    TestBothQrTransactions<InvalidParamsTest>(
+        [](auto &test, auto &options) {
+            test.callbacks.qrcode_callback = nullptr;
+            options.mcResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.gaResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.followingCtapUpdatesProcessor = [&](auto &&) {
+                test.called += "update_processor;";
+            };
+        },
+        [](auto &test) { EXPECT_EQ(test.called, "response_callback;"); });
+}
+
+namespace {
+
+template <class WauthnCallbacks>
+struct SuccessfulRequestTest {
+    std::string called;
+    WauthnCallbacks callbacks = {
+        [](const char *, void *) {},
+        [](const auto *pubkeyCred, wauthn_error_e result, void *data) {
+            auto &test = *static_cast<SuccessfulRequestTest *>(data);
+            test.called += "response_callback;";
+            EXPECT_EQ(result, WAUTHN_ERROR_NONE);
+            EXPECT_NE(pubkeyCred, nullptr);
+        },
+        [](const auto *linkedData, wauthn_error_e result, void *data) {
+            auto &test = *static_cast<SuccessfulRequestTest *>(data);
+            test.called += "linked_data_callback;";
+            EXPECT_EQ(result, WAUTHN_ERROR_NONE);
+            EXPECT_EQ(linkedData, nullptr);
+        },
+        this,
+    };
+    WauthnCallbacks *callbacksPtr = &callbacks;
+};
+
+//
+} // namespace
+
+TEST(RequestHandler, state_assisted_transaction_is_performed_with_nullptr_qrcode_callback)
+{
+    TestBothStateAssistedTransactions<SuccessfulRequestTest>(
+        [](auto &test, auto &options) {
+            test.callbacks.qrcode_callback = nullptr;
+            options.mcResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.gaResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.followingCtapUpdatesProcessor = [&](auto &&) {
+                test.called += "update_processor;";
+            };
+        },
+        [](auto &test) {
+            EXPECT_EQ(test.called,
+                      "result_processor;response_callback;update_processor;linked_data_callback;");
+        });
+}
+
+TEST(RequestHandler, transaction_is_not_performed_with_nullptr_response_callback)
+{
+    TestAll4Transactions<InvalidParamsTest>(
+        [](auto &test, auto &options) {
+            test.callbacks.response_callback = nullptr;
+            options.mcResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.gaResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.followingCtapUpdatesProcessor = [&](auto &&) {
+                test.called += "update_processor;";
+            };
+        },
+        [](auto &test) { EXPECT_EQ(test.called, ""); });
+}
+
+TEST(RequestHandler, transaction_is_not_performed_with_nullptr_linked_data_callback)
+{
+    TestAll4Transactions<InvalidParamsTest>(
+        [](auto &test, auto &options) {
+            test.callbacks.linked_data_callback = nullptr;
+            options.mcResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.gaResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.followingCtapUpdatesProcessor = [&](auto &&) {
+                test.called += "update_processor;";
+            };
+        },
+        [](auto &test) { EXPECT_EQ(test.called, "response_callback;"); });
+}
+
+TEST(RequestHandler, transaction_is_not_performed_with_nullptr_client_data)
+{
+    TestAll4Transactions<InvalidParamsTest>(
+        [](auto &test, auto &options) {
+            test.clientDataPtr = nullptr;
+            options.mcResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.gaResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.followingCtapUpdatesProcessor = [&](auto &&) {
+                test.called += "update_processor;";
+            };
+        },
+        [](auto &test) { EXPECT_EQ(test.called, "response_callback;"); });
+}
+
+TEST(RequestHandler, transaction_is_not_performed_with_nullptr_options)
+{
+    TestAll4Transactions<InvalidParamsTest>(
+        [](auto &test, auto &options) {
+            test.optionsPtr = nullptr;
+            options.mcResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.gaResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.followingCtapUpdatesProcessor = [&](auto &&) {
+                test.called += "update_processor;";
+            };
+        },
+        [](auto &test) { EXPECT_EQ(test.called, "response_callback;"); });
+}
+
+namespace {
+
+template <class WauthnCallbacks>
+struct QrCallbackTest {
+    int counter = 0;
+    std::optional<std::string> receivedQrContents;
+    std::optional<wauthn_error_e> responseCallbackRes;
+    std::optional<wauthn_error_e> linkedDataCallbackRes;
+    WauthnCallbacks callbacks = {
+        [](const char *qrContents, void *data) {
+            auto &test = *static_cast<QrCallbackTest *>(data);
+            ++test.counter;
+            if (qrContents)
+                test.receivedQrContents = std::string{qrContents};
+        },
+        [](const auto *pubkeyCred, wauthn_error_e result, void *data) {
+            auto &test = *static_cast<QrCallbackTest *>(data);
+            test.responseCallbackRes = result;
+            EXPECT_EQ(pubkeyCred == nullptr, result != WAUTHN_ERROR_NONE);
+        },
+        [](const auto *linkedData, wauthn_error_e result, void *data) {
+            auto &test = *static_cast<QrCallbackTest *>(data);
+            test.linkedDataCallbackRes = result;
+            EXPECT_EQ(linkedData == nullptr, result != WAUTHN_ERROR_NONE_AND_WAIT);
+        },
+        this,
+    };
+    const WauthnCallbacks *callbacksPtr = &callbacks;
+};
+
+} // namespace
+
+TEST(RequestHandler, qr_initiated_transaction_calls_qr_callback_once)
+{
+    TestBothQrTransactions<QrCallbackTest>(
+        [](auto & /*test*/, GenericMQrTransaction::Options &options) {
+            options.callQrCallback = true;
+        },
+        [](auto &test) {
+            EXPECT_EQ(test.counter, 1);
+            EXPECT_EQ(test.receivedQrContents, "");
+            EXPECT_EQ(test.responseCallbackRes, WAUTHN_ERROR_NONE);
+            EXPECT_EQ(test.linkedDataCallbackRes, WAUTHN_ERROR_NONE);
+        });
+}
+
+TEST(RequestHandler, qr_initiated_transaction_qr_callback_qr_contents)
+{
+    [[maybe_unused]] static constexpr auto QR_CODE_CONTENTS = std::string_view{"FIDO:/01234"};
+    TestBothQrTransactions<QrCallbackTest>(
+        [](auto & /*test*/, GenericMQrTransaction::Options &options) {
+            options.callQrCallback = true;
+            options.qrCodeContents = QR_CODE_CONTENTS;
+        },
+        [](auto &test) {
+            EXPECT_EQ(test.counter, 1);
+            EXPECT_EQ(test.receivedQrContents, QR_CODE_CONTENTS);
+            EXPECT_EQ(test.responseCallbackRes, WAUTHN_ERROR_NONE);
+            EXPECT_EQ(test.linkedDataCallbackRes, WAUTHN_ERROR_NONE);
+        });
+}
+
+TEST(RequestHandler, state_assisted_transaction_does_not_call_qr_callback)
+{
+    TestBothStateAssistedTransactions<QrCallbackTest>(
+        [](auto &, GenericMStateAssistedTransaction::Options &) {},
+        [](auto &test) {
+            EXPECT_EQ(test.counter, 0);
+            EXPECT_EQ(test.receivedQrContents, std::nullopt);
+            EXPECT_EQ(test.responseCallbackRes, WAUTHN_ERROR_NONE);
+            EXPECT_EQ(test.linkedDataCallbackRes, WAUTHN_ERROR_NONE);
+        });
+}
+
+namespace {
+
+template <class WauthnCallbacks>
+struct PotentialErrorsTest {
+    std::string called;
+    std::optional<wauthn_error_e> responseCallbackRes;
+    std::optional<wauthn_error_e> linkedDataCallbackRes;
+    WauthnCallbacks callbacks = {
+        [](const char *, void *) {},
+        [](const auto *pubkeyCred, wauthn_error_e result, void *data) {
+            auto &test = *static_cast<PotentialErrorsTest *>(data);
+            test.called += "response_callback;";
+            test.responseCallbackRes = result;
+            EXPECT_EQ(pubkeyCred == nullptr, result != WAUTHN_ERROR_NONE);
+        },
+        [](const auto *linkedData, wauthn_error_e result, void *data) {
+            auto &test = *static_cast<PotentialErrorsTest *>(data);
+            test.called += "linked_data_callback;";
+            test.linkedDataCallbackRes = result;
+            EXPECT_EQ(linkedData == nullptr, result != WAUTHN_ERROR_NONE_AND_WAIT);
+        },
+        this,
+    };
+    WauthnCallbacks *callbacksPtr = &callbacks;
+};
+
+} // namespace
+
+TEST(RequestHandler, perform_transaction_throws_std_bad_alloc)
+{
+    TestAll4Transactions<PotentialErrorsTest>(
+        [](auto &test, auto &options) {
+            options.mcResultProcessor = [](auto &) { throw std::bad_alloc{}; };
+            options.gaResultProcessor = [](auto &) { throw std::bad_alloc{}; };
+            options.followingCtapUpdatesProcessor = [&](auto &&) {
+                test.called += "update_processor;";
+            };
+        },
+        [](auto &test) {
+            EXPECT_EQ(test.called, "response_callback;");
+            EXPECT_EQ(test.responseCallbackRes, WAUTHN_ERROR_MEMORY);
+        });
+}
+
+TEST(RequestHandler, perform_transaction_throws_runtime_error)
+{
+    TestAll4Transactions<PotentialErrorsTest>(
+        [](auto &test, auto &options) {
+            options.mcResultProcessor = [](auto &) { throw std::runtime_error{"abc"}; };
+            options.gaResultProcessor = [](auto &) { throw std::runtime_error{"abc"}; };
+            options.followingCtapUpdatesProcessor = [&](auto &&) {
+                test.called += "update_processor;";
+            };
+        },
+        [](auto &test) {
+            EXPECT_EQ(test.called, "response_callback;");
+            EXPECT_EQ(test.responseCallbackRes, WAUTHN_ERROR_UNKNOWN);
+        });
+}
+
+TEST(RequestHandler, perform_transaction_throws_int)
+{
+    TestAll4Transactions<PotentialErrorsTest>(
+        [](auto &test, auto &options) {
+            options.mcResultProcessor = [](auto &) { throw 42; };
+            options.gaResultProcessor = [](auto &) { throw 42; };
+            options.followingCtapUpdatesProcessor = [&](auto &&) {
+                test.called += "update_processor;";
+            };
+        },
+        [](auto &test) {
+            EXPECT_EQ(test.called, "response_callback;");
+            EXPECT_EQ(test.responseCallbackRes, WAUTHN_ERROR_UNKNOWN);
+        });
+}
+
+TEST(RequestHandler, invalid_transport_in_make_credential_transaction_response)
+{
+    static constexpr auto preCallback = [](auto &test, auto &options) {
+        options.mcResultProcessor = [](ITransaction::MCResult &mcRes) {
+            mcRes.ctapResult.getInfo.m_transports.emplace_back("some unknown transport");
+        };
+        options.followingCtapUpdatesProcessor = [&](auto &&) {
+            test.called += "update_processor;";
+        };
+    };
+    static constexpr auto postCallback = [](auto &test) {
+        EXPECT_EQ(test.called, "response_callback;");
+        EXPECT_EQ(test.responseCallbackRes, WAUTHN_ERROR_UNKNOWN);
+    };
+    TestQrTransactionMakeCredential<PotentialErrorsTest>(preCallback, postCallback);
+    TestStateAssistedTransactionMakeCredential<PotentialErrorsTest>(preCallback, postCallback);
+}
+
+namespace {
+
+template <void (*THROW_CALLBACK)()>
+void TestResponseCallbackThrows()
+{
+    TestAll4Transactions<PotentialErrorsTest>(
+        [](auto &test, auto &options) {
+            options.mcResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.gaResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.followingCtapUpdatesProcessor = [&](auto &&) {
+                test.called += "update_processor;";
+            };
+            test.callbacks.response_callback =
+                [](const auto * /*pubkeyCred*/, wauthn_error_e result, void *data) {
+                    auto &innerTest = *static_cast<std::remove_reference_t<decltype(test)> *>(data);
+                    innerTest.called += "response_callback;";
+                    innerTest.responseCallbackRes = result;
+                    THROW_CALLBACK();
+                };
+            test.callbacks.linked_data_callback =
+                [](const auto * /*linkedData*/, wauthn_error_e result, void *data) {
+                    auto &innerTest = *static_cast<std::remove_reference_t<decltype(test)> *>(data);
+                    innerTest.called += "linked_data_callback;";
+                    innerTest.linkedDataCallbackRes = result;
+                };
+            test.callbacks.user_data = &test;
+        },
+        [](auto &test) {
+            EXPECT_EQ(test.called, "result_processor;response_callback;linked_data_callback;");
+            EXPECT_EQ(test.responseCallbackRes, WAUTHN_ERROR_NONE);
+            EXPECT_EQ(test.linkedDataCallbackRes, WAUTHN_ERROR_UNKNOWN);
+        });
+}
+
+void ThrowRuntimeError() { throw std::runtime_error{"xyz"}; }
+
+void ThrowInt() { throw 42; }
+
+} // namespace
+
+TEST(RequestHandler, response_callback_throws_runtime_error)
+{
+    TestResponseCallbackThrows<ThrowRuntimeError>();
+}
+
+TEST(RequestHandler, response_callback_throws_int) { TestResponseCallbackThrows<ThrowInt>(); }
+
+namespace {
+
+UpdateMessage ExampleUpdateMessage()
+{
+    auto bv = BUFFER_VIEW(ANDROID_EXAMPLE_RAW_UPDATE_MSG);
+    bv.remove_prefix(1); // Remove CTAP message type prefix.
+    UpdateMessage msg;
+    msg.Deserialize(bv);
+    return msg;
+}
+
+enum class WhichLinkedDataCallbackWillThrow {
+    FROM_UPDATE_PROCESSOR,
+    LAST,
+};
+
+template <void (*THROW_CALLBACK)(),
+          WhichLinkedDataCallbackWillThrow WHICH_LINKED_CALLBACK_WILL_THROW>
+void TestLinkedDataCallbackThrows()
+{
+    TestAll4Transactions<PotentialErrorsTest>(
+        [](auto &test, auto &options) {
+            options.mcResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.gaResultProcessor = [&](auto &) { test.called += "result_processor;"; };
+            options.followingCtapUpdatesProcessor = [&](auto &&updateMessageCallback) {
+                updateMessageCallback(ExampleUpdateMessage());
+                test.called += "update_processor;";
+            };
+            test.callbacks.response_callback =
+                [](const auto * /*pubkeyCred*/, wauthn_error_e result, void *data) {
+                    auto &innerTest = *static_cast<std::remove_reference_t<decltype(test)> *>(data);
+                    innerTest.called += "response_callback;";
+                    innerTest.responseCallbackRes = result;
+                };
+            test.callbacks.linked_data_callback =
+                [](const auto * /*linkedData*/, wauthn_error_e result, void *data) {
+                    auto &innerTest = *static_cast<std::remove_reference_t<decltype(test)> *>(data);
+                    innerTest.called += "linked_data_callback;";
+                    innerTest.linkedDataCallbackRes = result;
+                    switch (WHICH_LINKED_CALLBACK_WILL_THROW) {
+                    case WhichLinkedDataCallbackWillThrow::FROM_UPDATE_PROCESSOR:
+                        if (result == WAUTHN_ERROR_NONE_AND_WAIT)
+                            THROW_CALLBACK();
+                        break;
+                    case WhichLinkedDataCallbackWillThrow::LAST:
+                        if (result == WAUTHN_ERROR_NONE)
+                            THROW_CALLBACK();
+                        break;
+                    }
+                };
+            test.callbacks.user_data = &test;
+        },
+        [](auto &test) {
+            EXPECT_EQ(test.responseCallbackRes, WAUTHN_ERROR_NONE);
+            switch (WHICH_LINKED_CALLBACK_WILL_THROW) {
+            case WhichLinkedDataCallbackWillThrow::FROM_UPDATE_PROCESSOR:
+                EXPECT_EQ(test.called,
+                          "result_processor;response_callback;linked_data_callback;linked_data_"
+                          "callback;");
+                EXPECT_EQ(test.linkedDataCallbackRes, WAUTHN_ERROR_UNKNOWN);
+                break;
+            case WhichLinkedDataCallbackWillThrow::LAST:
+                EXPECT_EQ(
+                    test.called,
+                    "result_processor;response_callback;linked_data_callback;update_processor;"
+                    "linked_data_callback;");
+                EXPECT_EQ(test.linkedDataCallbackRes, WAUTHN_ERROR_NONE);
+                break;
+            }
+        });
+}
+
+} // namespace
+
+TEST(RequestHandler, linked_data_callback_throws_runtime_error)
+{
+    TestLinkedDataCallbackThrows<ThrowRuntimeError,
+                                 WhichLinkedDataCallbackWillThrow::FROM_UPDATE_PROCESSOR>();
+    TestLinkedDataCallbackThrows<ThrowRuntimeError, WhichLinkedDataCallbackWillThrow::LAST>();
+}
+
+TEST(RequestHandler, linked_data_callback_throws_int)
+{
+    TestLinkedDataCallbackThrows<ThrowInt,
+                                 WhichLinkedDataCallbackWillThrow::FROM_UPDATE_PROCESSOR>();
+    TestLinkedDataCallbackThrows<ThrowInt, WhichLinkedDataCallbackWillThrow::LAST>();
+}
+
+namespace {
+
+template <class WauthnCallbacks>
+struct ResponseCallbackArgumentsTest {
+    static constexpr const char EXAMPLE_CLIENT_JSON[] = "{\"some_json\": 42}";
+
+    std::string called;
+    bool addLinkedData = false;
+    bool nonEmptyUserId = false;
+    WauthnCallbacks callbacks = {
+        [](const char *, void *) {},
+        [](const auto *pubkeyCred, wauthn_error_e result, void *data) {
+            auto &test = *static_cast<ResponseCallbackArgumentsTest *>(data);
+            test.called += "response_callback;";
+            EXPECT_EQ(result, WAUTHN_ERROR_NONE);
+
+            ASSERT_NE(pubkeyCred, nullptr);
+            EXPECT_EQ(pubkeyCred->type, PCT_PUBLIC_KEY);
+
+            ASSERT_NE(pubkeyCred->response, nullptr);
+            EXPECT_EQ(pubkeyCred->response->client_data_json, BUFFER_VIEW(EXAMPLE_CLIENT_JSON));
+            EXPECT_EQ(pubkeyCred->response->authenticator_data, BUFFER_VIEW("auth data"));
+            if constexpr (std::is_same_v<decltype(pubkeyCred),
+                                         const wauthn_pubkey_credential_attestation_s *>) {
+                EXPECT_EQ(pubkeyCred->response->attestation_object,
+                          BUFFER_VIEW("attestation object"));
+                EXPECT_EQ(pubkeyCred->response->transports,
+                          WAUTHN_TRANSPORT_INTERNAL | WAUTHN_TRANSPORT_HYBRID);
+                auto attestationData = ExampleAttestationData();
+                EXPECT_EQ(pubkeyCred->response->subject_pubkey_info,
+                          ToBufferView(attestationData.m_publicKeyDer));
+                EXPECT_EQ(pubkeyCred->response->pubkey_alg, attestationData.m_alg);
+                EXPECT_EQ(pubkeyCred->rawId, attestationData.m_credentialId);
+                auto idInBase64 = Base64UrlEncode(attestationData.m_credentialId);
+                EXPECT_EQ(pubkeyCred->id, ToBufferView(idInBase64));
+            } else {
+                EXPECT_EQ(pubkeyCred->response->attestation_object, nullptr);
+                EXPECT_EQ(pubkeyCred->response->signature, BUFFER_VIEW("signature"));
+                if (test.nonEmptyUserId)
+                    EXPECT_EQ(pubkeyCred->response->user_handle, BUFFER_VIEW("user handle"));
+                else
+                    EXPECT_EQ(pubkeyCred->response->user_handle, nullptr);
+                EXPECT_EQ(pubkeyCred->rawId, BUFFER_VIEW("credential id"));
+                auto idInBase64 = Base64UrlEncode(BUFFER_VIEW("credential id"));
+                EXPECT_EQ(pubkeyCred->id, ToBufferView(idInBase64));
+            }
+
+            EXPECT_EQ(pubkeyCred->authenticator_attachment, AA_CROSS_PLATFORM);
+            EXPECT_EQ(pubkeyCred->extensions, nullptr);
+
+            if (test.addLinkedData) {
+                ASSERT_NE(pubkeyCred->linked_device, nullptr);
+                auto updateMessage = ExampleUpdateMessage();
+                auto &linkData = updateMessage.GetLinkData().value();
+                EXPECT_EQ(pubkeyCred->linked_device->contact_id,
+                          ToBufferView(linkData.m_contactId));
+                EXPECT_EQ(pubkeyCred->linked_device->link_id, ToBufferView(linkData.m_linkId));
+                EXPECT_EQ(pubkeyCred->linked_device->link_secret,
+                          ToBufferView(linkData.m_linkSecret));
+                EXPECT_EQ(pubkeyCred->linked_device->authenticator_pubkey,
+                          ToBufferView(linkData.m_authenticatorPublicKey));
+                EXPECT_EQ(pubkeyCred->linked_device->authenticator_name,
+                          ToBufferView(linkData.m_authenticatorName));
+                EXPECT_EQ(pubkeyCred->linked_device->signature,
+                          ToBufferView(linkData.m_handshakeSignature));
+                EXPECT_EQ(pubkeyCred->linked_device->tunnel_server_domain,
+                          BUFFER_VIEW("some.tunnel.domain"));
+                EXPECT_EQ(pubkeyCred->linked_device->identity_key,
+                          BUFFER_VIEW("client platform priv key"));
+            } else
+                EXPECT_EQ(pubkeyCred->linked_device, nullptr);
+        },
+        [](const auto *linkedData, wauthn_error_e result, void *data) {
+            auto &test = *static_cast<ResponseCallbackArgumentsTest *>(data);
+            test.called += "linked_data_callback;";
+            EXPECT_EQ(result, WAUTHN_ERROR_NONE);
+            EXPECT_EQ(linkedData, nullptr);
+        },
+        this,
+    };
+    WauthnCallbacks *callbacksPtr = &callbacks;
+};
+
+} // namespace
+
+TEST(RequestHandler, response_callback_pubkey_cred_is_valid)
+{
+    wauthn_const_buffer_s clientDataJson;
+    for (bool addLinkedData : {false, true}) {
+        for (bool nonEmptyUserId : {false, true}) {
+            TestAll4Transactions<ResponseCallbackArgumentsTest>(
+                [&](auto &test, auto &options) {
+                    // Test params
+                    test.addLinkedData = addLinkedData;
+                    test.nonEmptyUserId = nonEmptyUserId;
+                    // Transaction params
+                    clientDataJson = ToWauthnConstBuff(test.EXAMPLE_CLIENT_JSON);
+                    test.clientData.client_data_json = &clientDataJson;
+                    auto transactionResultProcessor = [&](auto &result) {
+                        result.ctapResult.response.m_authDataRaw = BUFFER("auth data");
+                        if constexpr (std::is_same_v<decltype(result), ITransaction::MCResult &>) {
+                            result.ctapResult.response.m_attestationObject =
+                                BUFFER("attestation object");
+                            result.ctapResult.getInfo.m_transports = {"internal", "hybrid"};
+                            result.ctapResult.response.m_authData.m_attestationData =
+                                ExampleAttestationData();
+                        } else {
+                            result.ctapResult.response.m_signature = BUFFER("signature");
+                            result.ctapResult.response.m_userId =
+                                nonEmptyUserId ? BUFFER("user handle") : Buffer{};
+                            result.ctapResult.response.m_credentialId = BUFFER("credential id");
+                        }
+                        if (test.addLinkedData) {
+                            result.ctapResult.upMsg = ExampleUpdateMessage();
+                            result.clientPlatformPrivKey = BUFFER("client platform priv key");
+                            result.tunnelServerDomain = "some.tunnel.domain";
+                        }
+                    };
+                    options.mcResultProcessor = transactionResultProcessor;
+                    options.gaResultProcessor = transactionResultProcessor;
+                },
+                [&](auto &test) {
+                    EXPECT_EQ(test.called, "response_callback;linked_data_callback;");
+                });
+        }
+    }
+}
+
+namespace {
+
+template <class WauthnCallbacks>
+struct LinkedDataCallbackArgumentsTest {
+    std::string called;
+    int remainingLinkedDataCallbackCalls = 0;
+    WauthnCallbacks callbacks = {
+        [](const char *, void *) {},
+        [](const auto *pubkeyCred, wauthn_error_e result, void *data) {
+            auto &test = *static_cast<LinkedDataCallbackArgumentsTest *>(data);
+            test.called += "response_callback;";
+            EXPECT_EQ(result, WAUTHN_ERROR_NONE);
+            EXPECT_NE(pubkeyCred, nullptr);
+        },
+        [](const auto *linkedData, wauthn_error_e result, void *data) {
+            auto &test = *static_cast<LinkedDataCallbackArgumentsTest *>(data);
+            test.called += "linked_data_callback;";
+            if (--test.remainingLinkedDataCallbackCalls == 0) {
+                EXPECT_EQ(result, WAUTHN_ERROR_NONE);
+                EXPECT_EQ(linkedData, nullptr);
+            } else {
+                EXPECT_EQ(result, WAUTHN_ERROR_NONE_AND_WAIT);
+                ASSERT_NE(linkedData, nullptr);
+                auto updateMessage = ExampleUpdateMessage();
+                auto &linkData = updateMessage.GetLinkData().value();
+                EXPECT_EQ(linkedData->contact_id, ToBufferView(linkData.m_contactId));
+                EXPECT_EQ(linkedData->link_id, ToBufferView(linkData.m_linkId));
+                EXPECT_EQ(linkedData->link_secret, ToBufferView(linkData.m_linkSecret));
+                EXPECT_EQ(linkedData->authenticator_pubkey,
+                          ToBufferView(linkData.m_authenticatorPublicKey));
+                EXPECT_EQ(linkedData->authenticator_name,
+                          ToBufferView(linkData.m_authenticatorName));
+                EXPECT_EQ(linkedData->signature, ToBufferView(linkData.m_handshakeSignature));
+                EXPECT_EQ(linkedData->tunnel_server_domain, BUFFER_VIEW("some.tunnel.domain"));
+                EXPECT_EQ(linkedData->identity_key, BUFFER_VIEW("client platform priv key"));
+            }
+        },
+        this,
+    };
+    WauthnCallbacks *callbacksPtr = &callbacks;
+};
+
+} // namespace
+
+TEST(RequestHandler, linked_data_callback_linked_data_is_valid)
+{
+    for (int updateMessagesNum = 0; updateMessagesNum < 10; ++updateMessagesNum) {
+        TestAll4Transactions<LinkedDataCallbackArgumentsTest>(
+            [&](auto &test, auto &options) {
+                auto transactionResultProcessor = [&](auto &result) {
+                    result.clientPlatformPrivKey = BUFFER("client platform priv key");
+                    result.tunnelServerDomain = "some.tunnel.domain";
+                };
+                options.mcResultProcessor = transactionResultProcessor;
+                options.gaResultProcessor = transactionResultProcessor;
+                options.followingCtapUpdatesProcessor = [&](auto &&updateMessageCallback) {
+                    test.called += "update_processor;";
+                    for (int i = 0; i < updateMessagesNum; ++i) {
+                        updateMessageCallback(ExampleUpdateMessage());
+                    }
+                };
+
+                test.remainingLinkedDataCallbackCalls = updateMessagesNum + 1;
+            },
+            [&](auto &test) {
+                std::string expected = "response_callback;update_processor;";
+                for (int i = 0; i < updateMessagesNum + 1; ++i) {
+                    expected += "linked_data_callback;";
+                }
+                EXPECT_EQ(test.called, expected);
+            });
+    }
+}
+
+namespace {
+
+template <class BaseTransaction>
+class CancellationTransaction : public BaseTransaction {
+    typename BaseTransaction::MCResult
+    Perform(const wauthn_client_data_s &, const wauthn_pubkey_cred_creation_options_s &) override
+    {
+        m_cancelFacilitator.CancelCheck();
+        return typename BaseTransaction::MCResult{
+            ExampleCtapMCResult(),
+            {},
+            "",
+        };
+    }
+
+    typename BaseTransaction::GAResult
+    Perform(const wauthn_client_data_s &, const wauthn_pubkey_cred_request_options_s &) override
+    {
+        m_cancelFacilitator.CancelCheck();
+        return typename BaseTransaction::GAResult{
+            {},
+            {},
+            "",
+        };
+    }
+
+    void ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)>) override
+    {
+        m_cancelFacilitator
+            .IgnorePendingCancellation(); // We only check if this function is being
+                                          // cancelled after starting, as this is disallowed.
+        try {
+            m_cancelFacilitator.CancelCheck();
+        } catch (const Exception::Cancelled &) {
+            FAIL() << "This method should not be allowed to cancel by the RequestHandler";
+        }
+    }
+
+    void Cancel() override { m_cancelFacilitator.Cancel(); }
+
+private:
+    CancelFacilitator m_cancelFacilitator;
+};
+
+class CancellationMQrTransaction : public CancellationTransaction<IQrTransaction> {
+public:
+    CancellationMQrTransaction() = default;
+
+    void Initialize(Hint &&,
+                    std::function<void(std::string &&)> &&,
+                    std::unique_ptr<IQrCodeShower> &&,
+                    IBtAdvertScannerUPtr &&,
+                    std::unique_ptr<IHandshake> &&,
+                    std::unique_ptr<ICtapMessageProcessor> &&) override
+    {
+    }
+};
+
+class CancellationMStateAssistedTransaction
+: public CancellationTransaction<IStateAssistedTransaction> {
+public:
+    CancellationMStateAssistedTransaction() = default;
+
+    void Initialize(Hint &&,
+                    std::unique_ptr<IHandshake> &&,
+                    IBtAdvertScannerUPtr &&,
+                    std::unique_ptr<ICtapMessageProcessor> &&) override
+    {
+    }
+};
+
+class RequestHandlerCancellationWrapper {
+public:
+    static void Cancel() { RequestHandler::Instance().Cancel(); }
+};
+
+template <template <class...> class Test,
+          RequestKind REQUEST_KIND,
+          class QrTransaction,
+          class StateAssistedTransaction>
+void TestCancellationFromTheOtherThread()
+{
+    auto makeCancellationTest = [&] { return RequestHandlerCancellationWrapper{}; };
+    TestCancelFromTheOtherThread<RequestHandlerCancellationWrapper>(
+        800,
+        20,
+        makeCancellationTest,
+        [](RequestHandlerCancellationWrapper &) {
+            Test<std::conditional_t<REQUEST_KIND == RequestKind::MC,
+                                    MakeCredentialTest,
+                                    GetAssertionTest>,
+                 PotentialErrorsTest<std::conditional_t<REQUEST_KIND == RequestKind::MC,
+                                                        wauthn_mc_callbacks_s,
+                                                        wauthn_ga_callbacks_s>>>
+                test;
+            RequestHandler::Instance().Process(
+                test.clientDataPtr,
+                Request<REQUEST_KIND>{test.optionsPtr, &test.callbacks},
+                QrTransaction{},
+                StateAssistedTransaction{});
+            if (test.responseCallbackRes == WAUTHN_ERROR_NONE) {
+                EXPECT_EQ(test.called, "response_callback;linked_data_callback;");
+                EXPECT_EQ(test.linkedDataCallbackRes, WAUTHN_ERROR_NONE);
+            } else if (test.responseCallbackRes == WAUTHN_ERROR_CANCELLED) {
+                EXPECT_EQ(test.called, "response_callback;");
+                throw Exception::Cancelled{};
+            } else {
+                FAIL() << test.called << '\n'
+                       << (test.responseCallbackRes
+                               ? std::to_string(static_cast<int>(*test.responseCallbackRes))
+                               : "nullopt");
+            }
+        },
+        false);
+}
+
+} // namespace
+
+TEST(RequestHandler, cancel_from_the_other_thread_qr_initiated_make_credential)
+{
+    TestCancellationFromTheOtherThread<QrInitiatedTest,
+                                       RequestKind::MC,
+                                       CancellationMQrTransaction,
+                                       ShouldNotBeUsedMStateAssistedTransaction>();
+}
+
+TEST(RequestHandler, cancel_from_the_other_thread_qr_initiated_get_assertion)
+{
+    TestCancellationFromTheOtherThread<QrInitiatedTest,
+                                       RequestKind::GA,
+                                       CancellationMQrTransaction,
+                                       ShouldNotBeUsedMStateAssistedTransaction>();
+}
+
+TEST(RequestHandler, cancel_from_the_other_thread_state_assisted_make_credential)
+{
+    TestCancellationFromTheOtherThread<StateAssistedTest,
+                                       RequestKind::MC,
+                                       ShouldNotBeUsedMQrTransaction,
+                                       CancellationMStateAssistedTransaction>();
+}
+
+TEST(RequestHandler, cancel_from_the_other_thread_state_assisted_get_assertion)
+{
+    TestCancellationFromTheOtherThread<StateAssistedTest,
+                                       RequestKind::GA,
+                                       ShouldNotBeUsedMQrTransaction,
+                                       CancellationMStateAssistedTransaction>();
+}
+
+namespace {
+
+template <size_t idx1, size_t idx2, class Func, class... Tests1, class... Tests2>
+void RunWithAllCombinationsImpl(const std::tuple<Tests1...> &tests1,
+                                const std::tuple<Tests2...> &tests2,
+                                const Func &func)
+{
+    if constexpr (idx1 == sizeof...(Tests1))
+        return;
+    else if constexpr (idx2 == sizeof...(Tests2))
+        return RunWithAllCombinationsImpl<idx1 + 1, 0>(tests1, tests2, func);
+    else {
+        func(std::get<idx1>(tests1), std::get<idx2>(tests2));
+        RunWithAllCombinationsImpl<idx1, idx2 + 1>(tests1, tests2, func);
+    }
+}
+
+template <class Func, class... Tests1, class... Tests2>
+void RunWithAllCombinations(const std::tuple<Tests1...> &tests1,
+                            const std::tuple<Tests2...> &tests2,
+                            const Func &func)
+{
+    RunWithAllCombinationsImpl<0, 0>(tests1, tests2, func);
+}
+
+template <template <class> class FirstTest, template <class> class SecondTest, class Func>
+void RunWithAll4x4Combinations(const Func &func)
+{
+    RunWithAllCombinations(
+        std::tuple{
+            [](const auto &preCallback, const auto &postCallback) {
+                TestQrTransactionMakeCredential<FirstTest>(preCallback, postCallback);
+            },
+            [](const auto &preCallback, const auto &postCallback) {
+                TestQrTransactionGetAssertion<FirstTest>(preCallback, postCallback);
+            },
+            [](const auto &preCallback, const auto &postCallback) {
+                TestStateAssistedTransactionMakeCredential<FirstTest>(preCallback, postCallback);
+            },
+            [](const auto &preCallback, const auto &postCallback) {
+                TestStateAssistedTransactionGetAssertion<FirstTest>(preCallback, postCallback);
+            },
+        },
+        std::tuple{
+            [](const auto &preCallback, const auto &postCallback) {
+                TestQrTransactionMakeCredential<SecondTest>(preCallback, postCallback);
+            },
+            [](const auto &preCallback, const auto &postCallback) {
+                TestQrTransactionGetAssertion<SecondTest>(preCallback, postCallback);
+            },
+            [](const auto &preCallback, const auto &postCallback) {
+                TestStateAssistedTransactionMakeCredential<SecondTest>(preCallback, postCallback);
+            },
+            [](const auto &preCallback, const auto &postCallback) {
+                TestStateAssistedTransactionGetAssertion<SecondTest>(preCallback, postCallback);
+            },
+        },
+        func);
+}
+
+template <class WauthnCallbacks>
+struct SecondSimultaneousRequestTest {
+    std::string called;
+    WauthnCallbacks callbacks = {
+        [](const char *, void *) {},
+        [](const auto *pubkeyCred, wauthn_error_e result, void *data) {
+            auto &test = *static_cast<SecondSimultaneousRequestTest *>(data);
+            test.called += "response_callback;";
+            EXPECT_EQ(result, WAUTHN_ERROR_NOT_ALLOWED);
+            EXPECT_EQ(pubkeyCred, nullptr);
+        },
+        [](const auto * /*linkedData*/, wauthn_error_e /*result*/, void *data) {
+            auto &test = *static_cast<SecondSimultaneousRequestTest *>(data);
+            test.called += "linked_data_callback;";
+            FAIL() << "this callback should not be called";
+        },
+        this,
+    };
+    WauthnCallbacks *callbacksPtr = &callbacks;
+};
+
+} // namespace
+
+TEST(RequestHandler, two_simultaneous_requests)
+{
+    RunWithAll4x4Combinations<SuccessfulRequestTest, SecondSimultaneousRequestTest>(
+        [&](const auto &firstRequestTest, const auto &secondRequestTest) {
+            auto startSecondRequestPromise = std::promise<void>{};
+            auto startSecondRequestFuture = startSecondRequestPromise.get_future();
+            auto secondRequestCompletedPromise = std::promise<void>{};
+            auto secondRequestCompletedFuture = secondRequestCompletedPromise.get_future();
+
+            auto secondRequestThread = std::thread{};
+
+            firstRequestTest(
+                [&](auto & /*test*/, auto &options) {
+                    // Run the second request while the transaction of the first request is in
+                    // progress.
+                    auto resultProcessor = [&](auto &) {
+                        // Start the second request.
+                        startSecondRequestPromise.set_value();
+                        // Wait for the second request to fail.
+                        secondRequestCompletedFuture.get();
+                    };
+                    options.mcResultProcessor = resultProcessor;
+                    options.gaResultProcessor = resultProcessor;
+
+                    secondRequestThread = std::thread{[&] {
+                        // Wait for the first request to enter transaction.
+                        startSecondRequestFuture.get();
+                        secondRequestTest([](auto &, auto &) {},
+                                          [](auto &innerTest) {
+                                              EXPECT_EQ(innerTest.called, "response_callback;");
+                                          });
+                        // Continue the first request.
+                        secondRequestCompletedPromise.set_value();
+                    }};
+                },
+                [&](auto &test) {
+                    EXPECT_EQ(test.called, "response_callback;linked_data_callback;");
+                    secondRequestThread.join();
+                });
+        });
+}
+
+TEST(RequestHandler, two_requests_second_one_executes_while_the_first_awaits_update_messages)
+{
+    RunWithAll4x4Combinations<SuccessfulRequestTest, SuccessfulRequestTest>(
+        [&](const auto &firstRequestTest, const auto &secondRequestTest) {
+            auto startSecondRequestPromise = std::promise<void>{};
+            auto startSecondRequestFuture = startSecondRequestPromise.get_future();
+            auto secondRequestCompletedPromise = std::promise<void>{};
+            auto secondRequestCompletedFuture = secondRequestCompletedPromise.get_future();
+
+            auto secondRequestThread = std::thread{};
+
+            firstRequestTest(
+                [&](auto & /*test*/, auto &options) {
+                    options.followingCtapUpdatesProcessor =
+                        [&](std::function<void(UpdateMessage &&)> &&) {
+                            // Start the second request.
+                            startSecondRequestPromise.set_value();
+                            // Wait for the second request to fail.
+                            secondRequestCompletedFuture.get();
+                        };
+
+                    secondRequestThread = std::thread{[&] {
+                        // Wait for the first request to enter transaction.
+                        startSecondRequestFuture.get();
+                        secondRequestTest([](auto &, auto &) {},
+                                          [](auto &innerTest) {
+                                              EXPECT_EQ(innerTest.called,
+                                                        "response_callback;linked_data_callback;");
+                                          });
+                        // Continue the first request.
+                        secondRequestCompletedPromise.set_value();
+                    }};
+                },
+                [&](auto &test) {
+                    EXPECT_EQ(test.called, "response_callback;linked_data_callback;");
+                    secondRequestThread.join();
+                });
+        });
+}
index 9ea9fa73fd976cfb1d3b33b267f495f273cfa459..b05b616fbf92ebc76ddaaa27f4ed77ebc13661ba 100644 (file)
@@ -146,90 +146,92 @@ TEST(StateAssistedTransaction, Cancel)
         auto clientData = wauthn_client_data_s{};
         TestState testState;
         auto makeTransaction = [&] {
-            return StateAssistedTransaction(
+            auto transaction = std::make_unique<StateAssistedTransaction>();
+            transaction->Initialize(
                 {},
                 std::make_unique<MHandshake>(testState),
                 std::make_unique<
                     MBtAdvertScanner<Event, CancelCalledOn, &DecryptedBluetoothAdvert>>(testState),
                 std::make_unique<MCtapMessageProcessor<Event, CancelCalledOn>>(testState));
+            return transaction;
         };
-        // Before PerformTransaction()
+        // Before Perform()
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::RESET, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::RESET, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::RESET);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
         }
         // In MHandshake::StateAssistedConnect()
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::CONNECTING_STARTED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::CONNECTING_STARTED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::CONNECTING_STARTED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::CONNECTING_OR_HANDSHAKE);
         }
         // After MHandshake::StateAssistedConnect()
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::CONNECTING_ENDED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::CONNECTING_ENDED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::CONNECTING_ENDED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::CONNECTING_OR_HANDSHAKE);
         }
         // In MBleAdvertScanner
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::AWAITING_BLE_ADVERT_STARTED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::AWAITING_BLE_ADVERT_STARTED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::AWAITING_BLE_ADVERT_STARTED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::BLE_ADVERT);
         }
         // After MBleAdvertScanner
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::AWAITING_BLE_ADVERT_ENDED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::AWAITING_BLE_ADVERT_ENDED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::AWAITING_BLE_ADVERT_ENDED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::BLE_ADVERT);
         }
         // In MHandshake::DoStateAssistedHandshake()
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::HANDSHAKE_STARTED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::HANDSHAKE_STARTED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::HANDSHAKE_STARTED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::CONNECTING_OR_HANDSHAKE);
         }
         // After MHandshake::DoStateAssistedHandshake()
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::HANDSHAKE_ENDED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::HANDSHAKE_ENDED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::HANDSHAKE_ENDED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::CONNECTING_OR_HANDSHAKE);
         }
         // In MCtapMessageProcessor
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::MC_OR_GA_STARTED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::MC_OR_GA_STARTED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::MC_OR_GA_STARTED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::CTAP_MESSAGE_PROCESSOR);
         }
         // After MCtapMessageProcessor
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::MC_OR_GA_ENDED, transaction);
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options), Cancelled);
+            testState.Reset(Event::MC_OR_GA_ENDED, *transaction);
+            EXPECT_THROW(transaction->Perform(clientData, options), Cancelled);
             EXPECT_EQ(testState.m_lastEvent, Event::MC_OR_GA_ENDED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::CTAP_MESSAGE_PROCESSOR);
         }
         // No Cancel
         {
             auto transaction = makeTransaction();
-            testState.Reset(Event::FINISHED, transaction);
-            EXPECT_NO_THROW(transaction.PerformTransaction(clientData, options));
+            testState.Reset(Event::FINISHED, *transaction);
+            EXPECT_NO_THROW(transaction->Perform(clientData, options));
             EXPECT_EQ(testState.m_lastEvent, Event::MC_OR_GA_ENDED);
             EXPECT_EQ(testState.m_cancelCalledOn, CancelCalledOn::NONE);
         }
@@ -256,14 +258,14 @@ TEST(StateAssistedTransaction, InvalidLinkData)
         auto performTransaction = [](const auto &options) {
             auto clientData = wauthn_client_data_s{};
             TestState testState;
-            auto transaction = StateAssistedTransaction(
+            auto transaction = StateAssistedTransaction();
+            transaction.Initialize(
                 {},
                 std::make_unique<MHandshake>(testState),
                 std::make_unique<
                     MBtAdvertScanner<Event, CancelCalledOn, &DecryptedBluetoothAdvert>>(testState),
                 std::make_unique<MCtapMessageProcessor<Event, CancelCalledOn>>(testState));
-            EXPECT_THROW(transaction.PerformTransaction(clientData, options),
-                         Exception::InvalidParam);
+            EXPECT_THROW(transaction.Perform(clientData, options), Exception::InvalidParam);
         };
         // MakeCredential
         {
@@ -346,14 +348,16 @@ private:
 
 } // namespace
 
-TEST(StateAssistedTransaction, cancel_from_the_other_thread)
+TEST(StateAssistedTransaction, CancelFromTheOtherThread)
 {
     auto makeTransaction = [&] {
-        return StateAssistedTransaction(
+        auto transaction = std::make_unique<StateAssistedTransaction>();
+        transaction->Initialize(
             {},
             std::make_unique<CancellationMHandshake>(),
             std::make_unique<CancellationMBtAdvertScanner<&DecryptedBluetoothAdvert>>(),
             std::make_unique<CancellationMCtapMessageProcessor>());
+        return transaction;
     };
     TestCancelFromTheOtherThread<ITransaction>(
         400, 40, makeTransaction, [](ITransaction &transaction) {
@@ -362,10 +366,23 @@ TEST(StateAssistedTransaction, cancel_from_the_other_thread)
 
             auto mcOptions = wauthn_pubkey_cred_creation_options_s{};
             mcOptions.linked_device = &linkedDevice.linkedDevice;
-            transaction.PerformTransaction(clientData, mcOptions);
+            transaction.Perform(clientData, mcOptions);
 
             auto gaOptions = wauthn_pubkey_cred_request_options_s{};
             gaOptions.linked_device = &linkedDevice.linkedDevice;
-            transaction.PerformTransaction(clientData, gaOptions);
+            transaction.Perform(clientData, gaOptions);
+        });
+}
+
+TEST(StateAssistedTransaction, ProcessFollowingUpdateMsgs)
+{
+    TestTransactionProcessFollowingUpdateMsgs(
+        [](std::unique_ptr<ICtapMessageProcessor> &&ctapMessageProcessor) {
+            auto transaction = std::make_unique<StateAssistedTransaction>();
+            transaction->Initialize({},
+                                    std::make_unique<ShouldNotBeCalledMHandshake>(),
+                                    std::make_unique<ShouldNotBeCalledMBtAdvertScanner>(),
+                                    std::move(ctapMessageProcessor));
+            return transaction;
         });
 }
index 2be43cb3544fad0962e051f473e43ac858b6d68a..7099e3107a270eb54728e96b81801ef7dedd97c4 100644 (file)
@@ -35,6 +35,13 @@ public:
     template <class Func, class CallbackOnCancel>
     void WithCancelCheck(Func &&func, CallbackOnCancel &&callbackOnCancel)
     {
+        if (m_ignorePendingCancellation &&
+            m_cancellationFuture.wait_for(std::chrono::nanoseconds{0}) !=
+                std::future_status::timeout) {
+            // Cancellation already happened, so we ignore it as requested.
+            std::forward<Func>(func)();
+            return;
+        }
         // If Cancel happens before operation.
         if (m_cancellationFuture.wait_for(std::chrono::nanoseconds{get_random(0, 100'000)}) !=
             std::future_status::timeout) {
@@ -64,33 +71,53 @@ public:
         WithCancelCheck([] {});
     }
 
+    void IgnorePendingCancellation() { m_ignorePendingCancellation = true; }
+
     // May be called only once. May be called from the other thread.
     void Cancel() { m_cancellationPromise.set_value(); }
 
 private:
+    bool m_ignorePendingCancellation = false;
     std::promise<void> m_cancellationPromise;
     std::future<void> m_cancellationFuture = m_cancellationPromise.get_future();
 };
 
+namespace detail {
+
+template <class>
+constexpr inline bool is_unique_ptr = false;
+
+template <class T>
+constexpr inline bool is_unique_ptr<std::unique_ptr<T>> = true;
+
+} // namespace detail
+
+/// @p maker may return derived class of @p Interface or unique_ptr of derived class of @p Interface
 template <class Interface, class Func, class TransactionInvoker>
 void TestCancelFromTheOtherThread(size_t reps,
                                   size_t minExpectedCancellationsNum,
                                   Func maker,
-                                  TransactionInvoker transactionInvoker)
+                                  TransactionInvoker transactionInvoker,
+                                  bool testCancellingBeforeStartingTransaction = true)
 {
+    static constexpr auto getInterfacePtr = [](auto &transaction) {
+        if constexpr (detail::is_unique_ptr<std::remove_reference_t<decltype(transaction)>>)
+            return transaction.get();
+        else
+            return &transaction;
+    };
     auto tp = std::chrono::steady_clock::now();
     constexpr size_t REPS = 32;
     for (size_t i = 0; i < REPS; ++i) {
         auto transaction = maker();
-        transactionInvoker(static_cast<Interface &>(transaction));
+        transactionInvoker(static_cast<Interface &>(*getInterfacePtr(transaction)));
     }
     const std::chrono::nanoseconds avgTimeOfFullTransaction =
         (std::chrono::steady_clock::now() - tp) / REPS;
 
-    // Test cancelling before starting the transaction.
-    {
+    if (testCancellingBeforeStartingTransaction) {
         auto transactionObj = maker();
-        Interface *transaction = &transactionObj;
+        Interface *transaction = getInterfacePtr(transactionObj);
         transaction->Cancel();
         EXPECT_THROW(transactionInvoker(*transaction), Exception::Cancelled);
     }
@@ -136,7 +163,7 @@ void TestCancelFromTheOtherThread(size_t reps,
     auto lock = std::unique_lock{mutex};
     for (size_t i = 0; i < reps; ++i) {
         auto transactionObj = maker();
-        transaction = &transactionObj;
+        transaction = getInterfacePtr(transactionObj);
         // Wake up canceller and wait for it to become ready to cancel.
         state = State::NEW_TRANSACTION_READY;
         cv.notify_all();
index 02f5c7f820c95dc746b4d78910c825060566d64a..ec959dc6e7d83d17aea2adb8c96b0386c1944aee 100644 (file)
@@ -18,8 +18,8 @@
 
 #include "bluetooth_advert.h"
 #include "common.h"
-#include "crypto/random.h"
 #include "ctap_message_processor.h"
+#include "handshake.h"
 #include "test_cancel_from_the_other_thread.h"
 #include "transaction_tester.h"
 
@@ -78,6 +78,12 @@ public:
         return {};
     }
 
+    void ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)>) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
     void Cancel() override
     {
         SCOPED_TRACE("");
@@ -118,8 +124,124 @@ public:
         return {};
     }
 
+    void ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)>) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
     void Cancel() override { m_cancelFacilitator.Cancel(); }
 
 private:
     CancelFacilitator m_cancelFacilitator;
 };
+
+class ShouldNotBeCalledMBtAdvertScanner : public BtAdvertScanner {
+public:
+    void AwaitAdvert(const CryptoBuffer &, CryptoBuffer &) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    void Cancel() override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+};
+
+class ShouldNotBeCalledMHandshake : public IHandshake {
+public:
+    virtual void
+    QrInitiatedConnect(const std::string &, const BufferView &, const BufferView &) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    HandshakeResult DoQrInitiatedHandshake(const CryptoBuffer &,
+                                           const Crypto::X9_62_P_256_Key &) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    virtual void
+    StateAssistedConnect(const std::string &, const BufferView &, const BufferView &) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    HandshakeResult DoStateAssistedHandshake(const CryptoBuffer &, const CryptoBuffer &) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    void Cancel() override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+};
+
+class UpdateMsgsProcessingMCtapMessageProcessor : public ICtapMessageProcessor {
+public:
+    void SetEncryptedTunnel(std::unique_ptr<IEncryptedTunnel>) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    MCResult MakeCredential(const wauthn_client_data_s &,
+                            const wauthn_pubkey_cred_creation_options_s &) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    GAResult GetAssertion(const wauthn_client_data_s &,
+                          const wauthn_pubkey_cred_request_options_s &) override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    void
+    ProcessFollowingUpdateMsgs(std::function<void(UpdateMessage &&)> updateMsgCallback) override
+    {
+        ++m_processFollowingUpdateMsgsCalledNum;
+        // Check the right callback is called
+        for (int i = 0; i < m_updateMsgCallbackReps; ++i) {
+            updateMsgCallback(UpdateMessage{});
+        }
+    }
+
+    void Cancel() override
+    {
+        ADD_FAILURE() << "This should not be called";
+        throw std::runtime_error{""};
+    }
+
+    int m_updateMsgCallbackReps = 0;
+    int m_processFollowingUpdateMsgsCalledNum = 0;
+};
+
+template <class TransactionMaker>
+void TestTransactionProcessFollowingUpdateMsgs(TransactionMaker &&transactionMaker)
+{
+    auto ctapMessageProcessorUPtr = std::make_unique<UpdateMsgsProcessingMCtapMessageProcessor>();
+    auto ctapMessageProcessor = ctapMessageProcessorUPtr.get();
+
+    auto transaction =
+        std::forward<TransactionMaker>(transactionMaker)(std::move(ctapMessageProcessorUPtr));
+
+    const auto updateCallbackReps = get_random(0, 17);
+    ctapMessageProcessor->m_updateMsgCallbackReps = updateCallbackReps;
+    int reps = 0;
+    transaction->ProcessFollowingUpdateMsgs([&](UpdateMessage &&) { ++reps; });
+    EXPECT_EQ(reps, updateCallbackReps);
+    EXPECT_EQ(ctapMessageProcessor->m_processFollowingUpdateMsgsCalledNum, 1);
+}
index a80ae5f5b4a0d9f3205ddc1b9c5b7ccf805606d0..22bef89ddee9579db1797d400900f906c69400e3 100644 (file)
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "transaction.h"
+
 #include <cassert>
 #include <gtest/gtest.h>
 #include <stdexcept>
index 4846a644a0116b84c348dadc76add29316b6d508..a6612d6c9343425f0d155db2e27e9aaa612039d3 100644 (file)
@@ -17,6 +17,7 @@
 #include "../get_random.h"
 #include "../test_cancel_from_the_other_thread.h"
 #include "auto_tests.h"
+#include "constants.h"
 #include "exception.h"
 #include "tunnel.h"
 
@@ -387,7 +388,7 @@ void MockedSockets::ContextDestroy(LwsContext *lwsContext) noexcept
 /*
  * Tests below are executed using fully mocked websockets implementation only (MockedSockets)
  */
-TEST(TunnelMockedTests, Cancelling)
+TEST(TunnelMockedTests, Cancel)
 {
     const std::string test = "sdfjisdjkfhdfjkghsdfkghdgkjhd";
     std::vector<uint8_t> in, out;
@@ -437,7 +438,9 @@ TEST(TunnelMockedTests, InjectedEvents)
         LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER,
         LWS_CALLBACK_CLIENT_CLOSED,
         LWS_CALLBACK_VHOST_CERT_AGING,
-        LWS_CALLBACK_PROTOCOL_INIT
+        LWS_CALLBACK_PROTOCOL_INIT,
+        LWS_CALLBACK_WSI_DESTROY, // LWS_CALLBACK_PROTOCOL_DESTROY has the same logic
+        LWS_CALLBACK_TIMER,
     };
 
     struct EventInfo {
@@ -626,13 +629,13 @@ TEST(TunnelMockedTests, InjectedEvents)
     EXPECT_LT(successes, injectionsNum);
 }
 
-TEST(TunnelMockedTests, CancellingFromOtherThread)
+TEST(TunnelMockedTests, CancelFromTheOtherThread)
 {
     class Transaction {
     public:
         Transaction() : m_tunnel(std::make_shared<DelayedSockets>()) {}
 
-        void PerformTransaction()
+        void Perform()
         {
             std::vector<uint8_t> in, out(42);
             m_tunnel.Connect(TestUrl());
@@ -649,7 +652,5 @@ TEST(TunnelMockedTests, CancellingFromOtherThread)
 
     auto makeTransaction = [&] { return Transaction(); };
     TestCancelFromTheOtherThread<Transaction>(
-        400, 40, makeTransaction, [](Transaction &transaction) {
-            transaction.PerformTransaction();
-        });
+        400, 40, makeTransaction, [](Transaction &transaction) { transaction.Perform(); });
 }
index 6d8ac64b29c230302bffde8d04e396172409c253..7f2cb831f5d5e406da8d0b95cc3dfb9def1a73d8 100644 (file)
@@ -50,6 +50,9 @@ protected:
     bool IsFinalFragment(Lws *) const noexcept override { return true; }
 
     int WriteBinary(Lws *lws, unsigned char *buf, size_t len) noexcept override;
+
+    void SetTimer(Lws *, lws_usec_t) noexcept override {}
+
     void ContextDestroy(LwsContext *lwsContext) noexcept override;
 
     void SetListener(IWebsocketsListener *listener) noexcept override { m_listener = listener; }
index a637a3ba2a20d62df2e5613d0d1a5c712e771558..8fdcef12b3967fb350e4601392fe7d348c58f993 100644 (file)
@@ -19,6 +19,7 @@
 #include "manual_tests.h"
 
 #include <cassert>
+#include <chrono>
 
 namespace {
 
@@ -169,11 +170,30 @@ TEST(TunnelDummyServerTests, TooBigMessages)
     EXPECT_THROW(tunnel.ReadBinary(), InvalidState);
 }
 
-TEST(TunnelDummyServerTests, CancellingFromOtherThread)
+TEST(TunnelDummyServerTests, CloseConnectionAfter)
+{
+    static constexpr auto REPS = 5;
+    for (int rep = 0; rep < REPS; ++rep) {
+        UnsafeTunnel tunnel;
+        EXPECT_NO_THROW(tunnel.Connect(TestUrl()));
+
+        const auto usecs = get_random(0, 1'000'000);
+        tunnel.CloseConnectionAfter(usecs);
+
+        auto tp = std::chrono::steady_clock::now();
+        EXPECT_THROW(tunnel.ReadBinary(), ExceptionTunnelClosed);
+        auto duration = std::chrono::steady_clock::now() - tp;
+
+        EXPECT_GE(duration, std::chrono::microseconds{usecs});
+        EXPECT_LT(duration, std::chrono::seconds{10});
+    }
+}
+
+TEST(TunnelDummyServerTests, CancelFromTheOtherThread)
 {
     class Transaction {
     public:
-        void PerformTransaction()
+        void Perform()
         {
             std::vector<uint8_t> in, out(42);
             m_tunnel.Connect(TestUrl());
@@ -190,9 +210,7 @@ TEST(TunnelDummyServerTests, CancellingFromOtherThread)
 
     auto makeTransaction = [&] { return Transaction(); };
     TestCancelFromTheOtherThread<Transaction>(
-        400, 40, makeTransaction, [](Transaction &transaction) {
-            transaction.PerformTransaction();
-        });
+        400, 40, makeTransaction, [](Transaction &transaction) { transaction.Perform(); });
 }
 
 int main(int argc, char *argv[])
diff --git a/tests/wauthn_const_buff_equals.h b/tests/wauthn_const_buff_equals.h
new file mode 100644 (file)
index 0000000..070a0ed
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ *  Copyright (c) 2024 Samsung Electronics Co., Ltd. All rights reserved
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+#include <common.h>
+#include <webauthn-types.h>
+
+constexpr bool operator==(const wauthn_const_buffer_s &wcb, const BufferView &bv) noexcept
+{
+    return BufferView{wcb.data, wcb.size} == bv;
+}
+
+constexpr bool operator==(const wauthn_const_buffer_s *wcb, const BufferView &bv) noexcept
+{
+    return wcb != nullptr && *wcb == bv;
+}