#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;
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)
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;
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);
}
}
}
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());
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());
+}