Make Credential response parsing 42/307942/13
authorKrzysztof Jackiewicz <k.jackiewicz@samsung.com>
Thu, 14 Mar 2024 10:16:06 +0000 (11:16 +0100)
committerKrzysztof Jackiewicz <k.jackiewicz@samsung.com>
Tue, 26 Mar 2024 12:58:48 +0000 (13:58 +0100)
Change-Id: I5f6211a7a4d42a50cb277003c42a4addffa0c078

srcs/exception.h
srcs/message.cpp
srcs/message.h
tests/message_tests.cpp

index e7f4af1102bf972fd785b0019c0c05049cee31e2..76e819e951ed88bbf7fd1917fe130be602ec5cd9 100644 (file)
@@ -55,12 +55,17 @@ typedef Exception<WAUTHN_ERROR_NO_SUCH_SERVICE> NoSuchService;
 typedef Exception<WAUTHN_ERROR_ACCESS_DENIED> AccessDenied;
 typedef Exception<WAUTHN_ERROR_MEMORY> MemoryError;
 typedef Exception<WAUTHN_ERROR_CANCELLED> Cancelled;
+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_CANCELLED() LOGGED_THROW(Exception::Cancelled, "Operation cancelled")
 #define THROW_ENCODING(...) LOGGED_THROW(Exception::EncodingFailed, __VA_ARGS__)
 #define THROW_MEMORY() LOGGED_THROW(Exception::MemoryError, "Memory error")
-#define THROW_INVALID_PARAM(...) LOGGED_THROW(Exception::InvalidParam, __VA_ARGS__)
+#define THROW_CANCELLED() LOGGED_THROW(Exception::Cancelled, "Operation cancelled")
+#define THROW_TIMEOUT(...) LOGGED_THROW(Exception::Timeout, __VA_ARGS__)
index 4017f0fbb05c9248970cf52aff94b2957f21d884..89fd634ab630f864f69e0505dd43f019748595e1 100644 (file)
 #include "exception.h"
 #include "message.h"
 
+#include <openssl/bn.h> // TODO move it to Crypto?
+#include <openssl/ec.h>
+#include <openssl/ossl_typ.h>
+#include <openssl/x509.h>
 #include <regex>
 
 namespace {
 
-constexpr char PUBLIC_KEY[] = "public-key";
+enum class StatusCode : uint8_t {
+    CTAP1_ERR_SUCCES = 0x00,
+    CTAP2_OK = 0x00,                    // Indicates successful response.
+    CTAP1_ERR_INVALID_COMMAND = 0x01,   // The command is not a valid CTAP command.
+    CTAP1_ERR_INVALID_PARAMETER = 0x02, // The command included an invalid parameter.
+    CTAP1_ERR_INVALID_LENGTH = 0x03,    // Invalid message or item length.
+    CTAP1_ERR_INVALID_SEQ = 0x04,       // Invalid message sequencing.
+    CTAP1_ERR_TIMEOUT = 0x05,           // Message timed out.
+    CTAP1_ERR_CHANNEL_BUSY =
+        0x06, // Channel busy. Client SHOULD retry the request after a short delay. Note that the
+              // client MAY abort the transaction if the command is no longer relevant.
+    CTAP1_ERR_LOCK_REQUIRED = 0x0A,        // Command requires channel lock.
+    CTAP1_ERR_INVALID_CHANNEL = 0x0B,      // Command not allowed on this cid.
+    CTAP2_ERR_CBOR_UNEXPECTED_TYPE = 0x11, // Invalid/unexpected CBOR error.
+    CTAP2_ERR_INVALID_CBOR = 0x12,         // Error when parsing CBOR.
+    CTAP2_ERR_MISSING_PARAMETER = 0x14,    // Missing non-optional parameter.
+    CTAP2_ERR_LIMIT_EXCEEDED = 0x15,       // Limit for number of items exceeded.
+    CTAP2_ERR_FP_DATABASE_FULL = 0x17, // Fingerprint data base is full, e.g., during enrollment.
+    CTAP2_ERR_LARGE_BLOB_STORAGE_FULL =
+        0x18, // Large blob storage is full. (See § 6.10.3 Large, per-credential blobs.)
+    CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19,   // Valid credential found in the exclude list.
+    CTAP2_ERR_PROCESSING = 0x21,            // Processing (Lengthy operation is in progress).
+    CTAP2_ERR_INVALID_CREDENTIAL = 0x22,    // Credential not valid for the authenticator.
+    CTAP2_ERR_USER_ACTION_PENDING = 0x23,   // Authentication is waiting for user interaction.
+    CTAP2_ERR_OPERATION_PENDING = 0x24,     // Processing, lengthy operation is in progress.
+    CTAP2_ERR_NO_OPERATIONS = 0x25,         // No request is pending.
+    CTAP2_ERR_UNSUPPORTED_ALGORITHM = 0x26, // Authenticator does not support requested algorithm.
+    CTAP2_ERR_OPERATION_DENIED = 0x27,      // Not authorized for requested operation.
+    CTAP2_ERR_KEY_STORE_FULL = 0x28,        // Internal key storage is full.
+    CTAP2_ERR_UNSUPPORTED_OPTION = 0x2B,    // Unsupported option.
+    CTAP2_ERR_INVALID_OPTION = 0x2C,        // Not a valid option for current operation.
+    CTAP2_ERR_KEEPALIVE_CANCEL = 0x2D,      // Pending keep alive was cancelled.
+    CTAP2_ERR_NO_CREDENTIALS = 0x2E,        // No valid credentials provided.
+    CTAP2_ERR_USER_ACTION_TIMEOUT = 0x2F,   // A user action timeout occurred.
+    CTAP2_ERR_NOT_ALLOWED =
+        0x30, // Continuation command, such as, authenticatorGetNextAssertion not allowed.
+    CTAP2_ERR_PIN_INVALID = 0x31,      // PIN Invalid.
+    CTAP2_ERR_PIN_BLOCKED = 0x32,      // PIN Blocked.
+    CTAP2_ERR_PIN_AUTH_INVALID = 0x33, // PIN authentication,pinUvAuthParam, verification failed.
+    CTAP2_ERR_PIN_AUTH_BLOCKED =
+        0x34, // PIN authentication using pinUvAuthToken blocked. Requires power cycle to reset.
+    CTAP2_ERR_PIN_NOT_SET = 0x35,   // No PIN has been set.
+    CTAP2_ERR_PUAT_REQUIRED = 0x36, // A pinUvAuthToken is required for the selected operation. See
+                                    // also the pinUvAuthToken option ID.
+    CTAP2_ERR_PIN_POLICY_VIOLATION =
+        0x37, // PIN policy violation. Currently only enforces minimum length.
+    CTAP2_ERR_REQUEST_TOO_LARGE =
+        0x39, // Authenticator cannot handle this request due to memory constraints.
+    CTAP2_ERR_ACTION_TIMEOUT = 0x3A,    // The current operation has timed out.
+    CTAP2_ERR_UP_REQUIRED = 0x3B,       // User presence is required for the requested operation.
+    CTAP2_ERR_UV_BLOCKED = 0x3C,        // built-in user verification is disabled.
+    CTAP2_ERR_INTEGRITY_FAILURE = 0x3D, // A checksum did not match.
+    CTAP2_ERR_INVALID_SUBCOMMAND =
+        0x3E, // The requested subcommand is either invalid or not implemented.
+    CTAP2_ERR_UV_INVALID =
+        0x3F, // built-in user verification unsuccessful. The platform SHOULD retry.
+    CTAP2_ERR_UNAUTHORIZED_PERMISSION =
+        0x40,                   // The permissions parameter contains an unauthorized permission.
+    CTAP1_ERR_OTHER = 0x7F,     // Other unspecified error.
+    CTAP2_ERR_SPEC_LAST = 0xDF, // CTAP 2 spec last error.
+    CTAP2_ERR_EXTENSION_FIRST = 0xE0, // Extension specific error.
+    CTAP2_ERR_EXTENSION_LAST = 0xEF,  // Extension specific error.
+    CTAP2_ERR_VENDOR_FIRST = 0xF0,    // Vendor specific error.
+    CTAP2_ERR_VENDOR_LAST = 0xFF,     // Vendor specific error.
+};
+
+constexpr int64_t KEY_KTY = 1;
+constexpr int64_t KEY_ALG = 3;
+constexpr int64_t KEY_CRV = -1;
+constexpr int64_t KEY_X = -2;
+constexpr int64_t KEY_Y = -3;
+
+enum class KeyType : int64_t {
+    OKP = 1,
+    EC2 = 2,
+    RSA = 3,
+};
+
+/* RFC 8152 - 13.1. Elliptic Curve Keys
+ *    +---------+-------+----------+------------------------------------+
+ *    | Name    | Value | Key Type | Description                        |
+ *    +---------+-------+----------+------------------------------------+
+ *    | P-256   | 1     | EC2      | NIST P-256 also known as secp256r1 |
+ *    | P-384   | 2     | EC2      | NIST P-384 also known as secp384r1 |
+ *    | P-521   | 3     | EC2      | NIST P-521 also known as secp521r1 |
+ *    | X25519  | 4     | OKP      | X25519 for use w/ ECDH only        |
+ *    | X448    | 5     | OKP      | X448 for use w/ ECDH only          |
+ *    | Ed25519 | 6     | OKP      | Ed25519 for use w/ EdDSA only      |
+ *    | Ed448   | 7     | OKP      | Ed448 for use w/ EdDSA only        |
+ *    +---------+-------+----------+------------------------------------+
+ */
+enum class Curve : int64_t {
+    P256 = 1,
+    P384 = 2,
+    P521 = 3,
+};
+
+constexpr char CREDENTIAL_TYPE_PUBLIC_KEY[] = "public-key";
+
+constexpr size_t AAGUID_SIZE = 16;
 
 // Get Info response
 constexpr int64_t KEY_GI_BUFFER = 0x01;
@@ -66,6 +169,14 @@ constexpr int64_t KEY_MC_CMD_PIN_UV_AUTH_PROTOCOL = 0x09;
 constexpr int64_t KEY_MC_CMD_ENTERPRISE_ATTESTATION = 0x0A;
 constexpr int64_t KEY_MC_CMD_ATTESTATION_FORMAT_PREFERENCE = 0x0B;
 
+// Make Credential response
+constexpr int64_t KEY_MC_RSP_FMT = 0x01;
+constexpr int64_t KEY_MC_RSP_AUTH_DATA = 0x02;
+constexpr int64_t KEY_MC_RSP_ATT_STMT = 0x03;
+constexpr int64_t KEY_MC_RSP_EP_ATT = 0x04;
+constexpr int64_t KEY_MC_RSP_LARGE_BLOB_KEY = 0x05;
+constexpr int64_t KEY_MC_RSP_UNSIGNED_EXTENSIONS_OUTPUT = 0x06;
+
 void SerializePubkeyCredDescriptors(CborEncoding::SortedMap &map,
                                     const CborEncoding::Key &key,
                                     const wauthn_pubkey_cred_descriptors_s &credentials)
@@ -86,7 +197,7 @@ void SerializePubkeyCredDescriptors(CborEncoding::SortedMap &map,
         publicKeyCredDescriptorMap.AppendByteStringAt("id", *desc.id);
         if (desc.type != PCT_PUBLIC_KEY)
             THROW_INVALID_PARAM("Unexpected credential type");
-        publicKeyCredDescriptorMap.AppendTextStringZAt("type", PUBLIC_KEY);
+        publicKeyCredDescriptorMap.AppendTextStringZAt("type", CREDENTIAL_TYPE_PUBLIC_KEY);
 
         if (desc.transports == WAUTHN_TRANSPORT_NONE)
             continue;
@@ -245,7 +356,7 @@ void MakeCredentialCommand::Serialize(Buffer &output) const
 
                     if (m_options.pubkey_cred_params->params[i].type != PCT_PUBLIC_KEY)
                         THROW_INVALID_PARAM("Invalid credential type");
-                    paramMap.AppendTextStringZAt("type", PUBLIC_KEY);
+                    paramMap.AppendTextStringZAt("type", CREDENTIAL_TYPE_PUBLIC_KEY);
                 }
             }
         }
@@ -316,6 +427,223 @@ void MakeCredentialCommand::Serialize(Buffer &output) const
     output.resize(prefixSize + encoder.GetBufferSize());
 }
 
+void CtapResponse::Deserialize(BufferView &input)
+{
+    // Deserialize CTAP response status code
+    if (input.size() < 1)
+        THROW_UNKNOWN("Incomplete message received");
+
+    auto status = static_cast<StatusCode>(input[0]);
+    auto uStatus = static_cast<unsigned short>(status);
+    switch (status) {
+    case StatusCode::CTAP2_OK: break;
+
+    case StatusCode::CTAP2_ERR_CBOR_UNEXPECTED_TYPE:
+    case StatusCode::CTAP2_ERR_INVALID_CBOR:
+        THROW_ENCODING("Invalid CTAP command encoding. Status: " << uStatus);
+
+    case StatusCode::CTAP2_ERR_PROCESSING:
+    case StatusCode::CTAP2_ERR_USER_ACTION_PENDING:
+    case StatusCode::CTAP2_ERR_OPERATION_PENDING:
+        THROW_INVALID_STATE("Authenticator is busy. Status: " << uStatus);
+
+    case StatusCode::CTAP2_ERR_UNSUPPORTED_ALGORITHM:
+    case StatusCode::CTAP2_ERR_UNSUPPORTED_OPTION:
+        THROW_UNSUPPORTED("Unsupported CTAP option. Status: " << uStatus);
+
+    case StatusCode::CTAP2_ERR_MISSING_PARAMETER:
+    case StatusCode::CTAP2_ERR_INVALID_OPTION:
+    case StatusCode::CTAP2_ERR_NO_CREDENTIALS:
+    case StatusCode::CTAP2_ERR_REQUEST_TOO_LARGE:
+    case StatusCode::CTAP2_ERR_INVALID_SUBCOMMAND:
+    case StatusCode::CTAP2_ERR_INTEGRITY_FAILURE:
+    case StatusCode::CTAP2_ERR_PIN_POLICY_VIOLATION:
+    case StatusCode::CTAP2_ERR_INVALID_CREDENTIAL:
+        THROW_INVALID_PARAM("Invalid CTAP parameter. Status: " << uStatus);
+
+    case StatusCode::CTAP2_ERR_OPERATION_DENIED:
+    case StatusCode::CTAP2_ERR_PIN_INVALID:
+    case StatusCode::CTAP2_ERR_PIN_AUTH_INVALID:
+    case StatusCode::CTAP2_ERR_UP_REQUIRED:
+    case StatusCode::CTAP2_ERR_UV_INVALID:
+    case StatusCode::CTAP2_ERR_UNAUTHORIZED_PERMISSION:
+    case StatusCode::CTAP2_ERR_PUAT_REQUIRED:
+        THROW_PERMISSION_DENIED("Not authorized for requested CTAP operation. Status: " << uStatus);
+
+    case StatusCode::CTAP2_ERR_KEY_STORE_FULL:
+        THROW_INVALID_STATE("Key store is full. Status: " << uStatus);
+
+    case StatusCode::CTAP2_ERR_USER_ACTION_TIMEOUT:
+    case StatusCode::CTAP2_ERR_ACTION_TIMEOUT:
+        THROW_TIMEOUT("Timeout occurred. Status: " << uStatus);
+
+    case StatusCode::CTAP2_ERR_PIN_BLOCKED:
+    case StatusCode::CTAP2_ERR_PIN_AUTH_BLOCKED:
+    case StatusCode::CTAP2_ERR_PIN_NOT_SET:
+    case StatusCode::CTAP2_ERR_UV_BLOCKED:
+    case StatusCode::CTAP2_ERR_NOT_ALLOWED:
+        THROW_INVALID_STATE("Invalid authenticator state. Status: " << uStatus);
+
+    default: THROW_UNKNOWN("CTAP command failed with status " << uStatus);
+    }
+
+    input.remove_prefix(1);
+}
+
+CtapResponse::AuthData::AttestationData::AttestationData(BufferView &attestationDataView)
+{
+    if (attestationDataView.size() < AAGUID_SIZE)
+        THROW_UNKNOWN("Auth data too short");
+    auto aaguid = BufferView(attestationDataView.data(), AAGUID_SIZE);
+    attestationDataView.remove_prefix(aaguid.size());
+
+    uint16_t credentialIdLen;
+    if (attestationDataView.size() < sizeof(credentialIdLen))
+        THROW_UNKNOWN("Auth data too short");
+    memcpy(&credentialIdLen, attestationDataView.data(), sizeof(credentialIdLen));
+    credentialIdLen = be16toh(credentialIdLen);
+    attestationDataView.remove_prefix(sizeof(credentialIdLen));
+
+    if (attestationDataView.size() < credentialIdLen)
+        THROW_UNKNOWN("Auth data too short");
+    m_credentialId = BufferView(attestationDataView.data(), credentialIdLen);
+    attestationDataView.remove_prefix(credentialIdLen);
+
+    auto publicKeyParser =
+        CborParsing::Parser::Create(attestationDataView.data(), attestationDataView.size());
+    auto credentialPublicKeyMap = publicKeyParser.EnterMap();
+
+    auto kty = credentialPublicKeyMap.GetInt64At(KEY_KTY).value(); // Key Type
+    auto alg = credentialPublicKeyMap.GetInt64At(KEY_ALG).value(); // wauthn_cose_algorithm_e
+    auto crv = credentialPublicKeyMap.GetInt64At(KEY_CRV).value(); // Value
+    auto x = credentialPublicKeyMap.GetByteStringAt(KEY_X).value();
+    auto y = credentialPublicKeyMap.GetByteStringAt(KEY_Y).value();
+
+    if (static_cast<KeyType>(kty) == KeyType::EC2) {
+        // https://datatracker.ietf.org/doc/html/rfc8152#section-13.1
+        struct AlgInfo {
+            int nid;
+            wauthn_cose_algorithm_e alg;
+        };
+
+        static const std::unordered_map<Curve, AlgInfo> NAME2ALG = {
+            {Curve::P256, {NID_X9_62_prime256v1, WAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256}},
+            {Curve::P384, {NID_secp384r1, WAUTHN_COSE_ALGORITHM_ECDSA_P384_WITH_SHA384}       },
+            {Curve::P521, {NID_secp521r1, WAUTHN_COSE_ALGORITHM_ECDSA_P521_WITH_SHA512}       },
+        };
+        auto algInfo = NAME2ALG.find(static_cast<Curve>(crv));
+        if (algInfo == NAME2ALG.end())
+            THROW_UNKNOWN("Unsupported 'crv'");
+        if (alg != algInfo->second.alg)
+            THROW_UNKNOWN("'Alg' doesn't match 'crv'");
+
+        // TODO move it to crypto?
+        BIGNUM *bx = BN_new();
+        if (!bx)
+            THROW_MEMORY();
+        auto cleanupBx = OnScopeExit([&bx] { BN_free(bx); });
+
+        BIGNUM *by = BN_new();
+        if (!by)
+            THROW_MEMORY();
+        auto cleanupBy = OnScopeExit([&by] { BN_free(by); });
+
+        // TODO compressed format
+        if (BN_bin2bn(x.data(), x.size(), bx) == nullptr)
+            THROW_UNKNOWN("Bignum x creation failed");
+        if (BN_bin2bn(y.data(), y.size(), by) == nullptr)
+            THROW_UNKNOWN("Bignum y creation failed");
+
+        auto ecKey = EC_KEY_new_by_curve_name(algInfo->second.nid);
+        if (!ecKey)
+            THROW_UNKNOWN("EC_KEY_new_by_curve_name() failed");
+        auto cleanupEcKey = OnScopeExit([&ecKey] { EC_KEY_free(ecKey); });
+
+        if (EC_KEY_set_public_key_affine_coordinates(ecKey, bx, by) != 1)
+            THROW_UNKNOWN("EC_KEY_set_public_key_affine_coordinates() failed");
+
+        m_publicKeyDer.resize(200);
+        auto publicKeyDerData = m_publicKeyDer.data();
+        int ret = i2d_EC_PUBKEY(ecKey, &publicKeyDerData); // deprecated in 3.0
+        if (ret < 0)
+            THROW_UNKNOWN("i2d_EC_PUBKEY() failed");
+
+        m_publicKeyDer.resize(ret);
+
+        m_alg = algInfo->second.alg;
+    } else {
+        // TODO other formats?
+        THROW_UNKNOWN("Invalid 'kty' value " << kty);
+    }
+}
+
+void CtapResponse::AuthData::Deserialize(const Buffer &authDataRaw)
+{
+    BufferView authDataView(authDataRaw.data(), authDataRaw.size());
+
+    uint32_t signCount;
+    static constexpr size_t RP_ID_SIZE = 32;
+    static constexpr size_t MIN_SIZE = RP_ID_SIZE + sizeof(m_flags) + sizeof(signCount);
+    if (authDataView.size() < MIN_SIZE)
+        THROW_UNKNOWN("Auth data too short");
+
+    m_rpIdHash = BufferView(authDataView.data(), RP_ID_SIZE);
+    authDataView.remove_prefix(m_rpIdHash.size());
+
+    static_assert(sizeof(m_flags) == 1);
+    m_flags = authDataView.front();
+    authDataView.remove_prefix(sizeof(m_flags));
+
+    memcpy(&signCount, authDataView.data(), sizeof(signCount));
+    signCount = be32toh(signCount);
+    authDataView.remove_prefix(sizeof(signCount));
+
+    if (!authDataView.empty())
+        m_attestationData = AttestationData(authDataView);
+}
+
+void CtapResponse::DeserializeAuthData(Buffer authData)
+{
+    // Raw authenticator data buffer: https://www.w3.org/TR/webauthn-3/#authenticator-data
+    m_authDataRaw = std::move(authData);
+
+    m_authData.Deserialize(m_authDataRaw);
+}
+
+void MakeCredentialResponse::Deserialize(BufferView &input)
+{
+    CtapResponse::Deserialize(input);
+
+    // see https://www.w3.org/TR/webauthn-2/#fig-attStructs
+
+    m_attestationObject.assign(input.begin(), input.end());
+
+    auto helper = CborParsing::Parser::Create(input.data(), input.size());
+    auto map = helper.EnterMap();
+
+    // "packed", "tpm", "android-key", "android-safetynet", "fido-u2f", "none", "apple"
+    m_format = map.GetTextStringAt(KEY_MC_RSP_FMT).value();
+
+    DeserializeAuthData(map.GetByteStringAt(KEY_MC_RSP_AUTH_DATA).value());
+
+    if (!m_authData.m_attestationData.has_value())
+        THROW_UNKNOWN("Missing attestation data in Make Credential response");
+
+    if (auto attStmtMap = map.EnterMapAt(KEY_MC_RSP_ATT_STMT)) {
+        // TODO it depends on the m_format, in a sample response the format was 'none' and this
+        // map was empty.
+    }
+    m_epAtt = map.GetBooleanAt(KEY_MC_RSP_EP_ATT);
+    m_largeBlobKey = map.GetByteStringAt(KEY_MC_RSP_LARGE_BLOB_KEY).value_or(Buffer{});
+
+    // TODO KEY_MC_RSP_UNSIGNED_EXTENSIONS_OUTPUT
+}
+
+void MakeCredentialResponse::Notify(IMessageObserver &observer)
+{
+    observer.HandleMakeCredentialResponse(*this);
+}
+
 void PostHandshakeResponse::Deserialize(BufferView &input)
 {
     auto helper = CborParsing::Parser::Create(input.data(), input.size());
index bb98b48655647a5be43a088b8ce6c95ea9989e4c..d03575faf6b1b38b2fc97a0d0be92a3f9f17d53b 100644 (file)
@@ -23,6 +23,8 @@
 #include <unordered_map>
 #include <webauthn-types.h>
 
+class IMessageObserver;
+
 enum class MessageType : uint8_t {
     SHUTDOWN = 0, // sent only by  the client
     CTAP = 1,     // CTAP2 command
@@ -130,4 +132,59 @@ private:
     const wauthn_pubkey_cred_creation_options_s &m_options;
 };
 
+class IIncomingNotifyingMessage : public IIncomingMessage {
+public:
+    virtual void Notify(IMessageObserver &observer) = 0;
+};
+
+// Contains status code (1B) | CBOR map
+class CtapResponse : public IIncomingNotifyingMessage {
+public:
+    void Deserialize(BufferView &input) override;
+
+    // Buffer containing Authenticator data
+    Buffer m_authDataRaw;
+
+    // Authenticator data: https://www.w3.org/TR/webauthn-3/#authenticator-data
+    struct AuthData {
+        void Deserialize(const Buffer &authDataRaw);
+
+        /*
+         *  Attestation data / Attested credential data
+         *  https://www.w3.org/TR/webauthn-3/#attested-credential-data
+         */
+        struct AttestationData {
+            explicit AttestationData(BufferView &attestationDataView);
+
+            BufferView m_credentialId;
+            Buffer m_publicKeyDer;
+            wauthn_cose_algorithm_e m_alg;
+        };
+
+        BufferView m_rpIdHash;
+        uint8_t m_flags;
+        std::optional<AttestationData> m_attestationData;
+    } m_authData;
+
+protected:
+    void DeserializeAuthData(Buffer authData);
+};
+
+class MakeCredentialResponse : public CtapResponse {
+public:
+    void Deserialize(BufferView &input) override;
+    void Notify(IMessageObserver &observer) override;
+
+    Buffer m_attestationObject;
+    std::string m_format;
+    std::optional<bool> m_epAtt;
+    Buffer m_largeBlobKey;
+};
+
+class IMessageObserver {
+public:
+    virtual void HandleMakeCredentialResponse(const MakeCredentialResponse &response) = 0;
+    virtual ~IMessageObserver() = default;
+};
+
 void ValidateDomain(const char *rpId);
index bf8c17513d98bc03836943d7a3c778b3ed6e4008..d749c17e025fa3b005bc2fe72f40b08e6b16f67b 100644 (file)
@@ -423,3 +423,74 @@ TEST(Messages, ShutdownMessage)
     ASSERT_NO_THROW(shutdown.Serialize(buffer));
     AssertEq(buffer, {0x00});
 }
+
+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\xa3\x79\xa6\xf6\xee\xaf\xb9\xa5\x5e\x37\x8c"
+        "\x11\x80\x34\xe2\x75\x1e\x68\x2f\xab\x9f\x2d\x30\xab\x13\xd2\x12\x55\x86\xce\x19\x47\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;
+    ASSERT_NO_THROW(msg.Deserialize(view));
+
+    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);
+    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_alg,
+              WAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256);
+    AssertEq(msg.m_attestationObject, view.data(), view.size());
+    ASSERT_EQ(msg.m_format, "none");
+    ASSERT_FALSE(msg.m_epAtt.has_value());
+    ASSERT_TRUE(msg.m_largeBlobKey.empty());
+}
+
+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\xa3\x79\xa6\xf6\xee\xaf\xb9\xa5\x5e\x37\x8c"
+        "\x11\x80\x34\xe2\x75\x1e\x68\x2f\xab\x9f\x2d\x30\xab\x13\xd2\x12\x55\x86\xce\x19\x47\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;
+    ASSERT_NO_THROW(msg.Deserialize(view));
+
+    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);
+    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_alg,
+              WAUTHN_COSE_ALGORITHM_ECDSA_P256_WITH_SHA256);
+    AssertEq(msg.m_attestationObject, view.data(), view.size());
+    ASSERT_EQ(msg.m_format, "none");
+    ASSERT_FALSE(msg.m_epAtt.has_value());
+    ASSERT_TRUE(msg.m_largeBlobKey.empty());
+}