| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "device/fido/device_response_converter.h" |
| |
| #include <memory> |
| #include <string> |
| #include <utility> |
| |
| #include "base/containers/span.h" |
| #include "base/i18n/streaming_utf8_validator.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/optional.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "components/cbor/diagnostic_writer.h" |
| #include "components/cbor/reader.h" |
| #include "components/cbor/writer.h" |
| #include "components/device_event_log/device_event_log.h" |
| #include "device/fido/authenticator_data.h" |
| #include "device/fido/authenticator_supported_options.h" |
| #include "device/fido/fido_constants.h" |
| #include "device/fido/opaque_attestation_statement.h" |
| |
| namespace device { |
| |
| namespace { |
| |
| constexpr size_t kResponseCodeLength = 1; |
| |
| ProtocolVersion ConvertStringToProtocolVersion(base::StringPiece version) { |
| if (version == kCtap2Version) |
| return ProtocolVersion::kCtap; |
| if (version == kU2fVersion) |
| return ProtocolVersion::kU2f; |
| |
| return ProtocolVersion::kUnknown; |
| } |
| |
| } // namespace |
| |
| using CBOR = cbor::Value; |
| |
| CtapDeviceResponseCode GetResponseCode(base::span<const uint8_t> buffer) { |
| if (buffer.empty()) |
| return CtapDeviceResponseCode::kCtap2ErrInvalidCBOR; |
| |
| auto code = static_cast<CtapDeviceResponseCode>(buffer[0]); |
| return base::ContainsValue(GetCtapResponseCodeList(), code) |
| ? code |
| : CtapDeviceResponseCode::kCtap2ErrInvalidCBOR; |
| } |
| |
| // Decodes byte array response from authenticator to CBOR value object and |
| // checks for correct encoding format. |
| base::Optional<AuthenticatorMakeCredentialResponse> |
| ReadCTAPMakeCredentialResponse(FidoTransportProtocol transport_used, |
| const base::Optional<cbor::Value>& cbor) { |
| if (!cbor || !cbor->is_map()) |
| return base::nullopt; |
| |
| const auto& decoded_map = cbor->GetMap(); |
| auto it = decoded_map.find(CBOR(1)); |
| if (it == decoded_map.end() || !it->second.is_string()) |
| return base::nullopt; |
| auto format = it->second.GetString(); |
| |
| it = decoded_map.find(CBOR(2)); |
| if (it == decoded_map.end() || !it->second.is_bytestring()) |
| return base::nullopt; |
| |
| auto authenticator_data = |
| AuthenticatorData::DecodeAuthenticatorData(it->second.GetBytestring()); |
| if (!authenticator_data) |
| return base::nullopt; |
| |
| it = decoded_map.find(CBOR(3)); |
| if (it == decoded_map.end() || !it->second.is_map()) |
| return base::nullopt; |
| |
| return AuthenticatorMakeCredentialResponse( |
| transport_used, |
| AttestationObject(std::move(*authenticator_data), |
| std::make_unique<OpaqueAttestationStatement>( |
| format, it->second.Clone()))); |
| } |
| |
| base::Optional<AuthenticatorGetAssertionResponse> ReadCTAPGetAssertionResponse( |
| const base::Optional<cbor::Value>& cbor) { |
| if (!cbor || !cbor->is_map()) |
| return base::nullopt; |
| |
| auto& response_map = cbor->GetMap(); |
| |
| auto it = response_map.find(CBOR(2)); |
| if (it == response_map.end() || !it->second.is_bytestring()) |
| return base::nullopt; |
| |
| auto auth_data = |
| AuthenticatorData::DecodeAuthenticatorData(it->second.GetBytestring()); |
| if (!auth_data) |
| return base::nullopt; |
| |
| it = response_map.find(CBOR(3)); |
| if (it == response_map.end() || !it->second.is_bytestring()) |
| return base::nullopt; |
| |
| auto signature = it->second.GetBytestring(); |
| AuthenticatorGetAssertionResponse response(std::move(*auth_data), |
| std::move(signature)); |
| |
| it = response_map.find(CBOR(1)); |
| if (it != response_map.end()) { |
| auto credential = |
| PublicKeyCredentialDescriptor::CreateFromCBORValue(it->second); |
| if (!credential) |
| return base::nullopt; |
| response.SetCredential(std::move(*credential)); |
| } |
| |
| it = response_map.find(CBOR(4)); |
| if (it != response_map.end()) { |
| auto user = PublicKeyCredentialUserEntity::CreateFromCBORValue(it->second); |
| if (!user) |
| return base::nullopt; |
| response.SetUserEntity(std::move(*user)); |
| } |
| |
| it = response_map.find(CBOR(5)); |
| if (it != response_map.end()) { |
| if (!it->second.is_unsigned()) |
| return base::nullopt; |
| |
| response.SetNumCredentials(it->second.GetUnsigned()); |
| } |
| |
| return base::Optional<AuthenticatorGetAssertionResponse>(std::move(response)); |
| } |
| |
| base::Optional<AuthenticatorGetInfoResponse> ReadCTAPGetInfoResponse( |
| base::span<const uint8_t> buffer) { |
| if (buffer.size() <= kResponseCodeLength || |
| GetResponseCode(buffer) != CtapDeviceResponseCode::kSuccess) |
| return base::nullopt; |
| |
| cbor::Reader::DecoderError error; |
| base::Optional<CBOR> decoded_response = |
| cbor::Reader::Read(buffer.subspan(1), &error); |
| |
| if (!decoded_response) { |
| FIDO_LOG(ERROR) << "-> (CBOR parse error from GetInfo response '" |
| << cbor::Reader::ErrorCodeToString(error) |
| << "' from raw message " |
| << base::HexEncode(buffer.data(), buffer.size()) << ")"; |
| return base::nullopt; |
| } |
| |
| if (!decoded_response->is_map()) |
| return base::nullopt; |
| |
| FIDO_LOG(DEBUG) << "-> " << cbor::DiagnosticWriter::Write(*decoded_response); |
| const auto& response_map = decoded_response->GetMap(); |
| |
| auto it = response_map.find(CBOR(1)); |
| if (it == response_map.end() || !it->second.is_array()) { |
| return base::nullopt; |
| } |
| |
| base::flat_set<ProtocolVersion> protocol_versions; |
| base::flat_set<base::StringPiece> advertised_protocols; |
| for (const auto& version : it->second.GetArray()) { |
| if (!version.is_string()) |
| return base::nullopt; |
| const std::string& version_string = version.GetString(); |
| |
| if (!advertised_protocols.insert(version_string).second) { |
| // Duplicate versions are not allowed. |
| return base::nullopt; |
| } |
| |
| auto protocol = ConvertStringToProtocolVersion(version_string); |
| if (protocol == ProtocolVersion::kUnknown) { |
| FIDO_LOG(DEBUG) << "Unexpected protocol version received."; |
| continue; |
| } |
| |
| if (!protocol_versions.insert(protocol).second) { |
| // A duplicate value will have already caused an error therefore hitting |
| // this suggests that |ConvertStringToProtocolVersion| is non-injective. |
| NOTREACHED(); |
| return base::nullopt; |
| } |
| } |
| |
| if (protocol_versions.empty()) |
| return base::nullopt; |
| |
| it = response_map.find(CBOR(3)); |
| if (it == response_map.end() || !it->second.is_bytestring() || |
| it->second.GetBytestring().size() != kAaguidLength) { |
| return base::nullopt; |
| } |
| |
| AuthenticatorGetInfoResponse response( |
| std::move(protocol_versions), |
| base::make_span<kAaguidLength>(it->second.GetBytestring())); |
| |
| AuthenticatorSupportedOptions options; |
| it = response_map.find(CBOR(2)); |
| if (it != response_map.end()) { |
| if (!it->second.is_array()) |
| return base::nullopt; |
| |
| std::vector<std::string> extensions; |
| for (const auto& extension : it->second.GetArray()) { |
| if (!extension.is_string()) |
| return base::nullopt; |
| |
| const std::string& extension_str = extension.GetString(); |
| if (extension_str == kExtensionCredProtect) { |
| options.supports_cred_protect = true; |
| } |
| extensions.push_back(extension_str); |
| } |
| response.extensions = std::move(extensions); |
| } |
| |
| it = response_map.find(CBOR(4)); |
| if (it != response_map.end()) { |
| if (!it->second.is_map()) |
| return base::nullopt; |
| |
| const auto& option_map = it->second.GetMap(); |
| auto option_map_it = option_map.find(CBOR(kPlatformDeviceMapKey)); |
| if (option_map_it != option_map.end()) { |
| if (!option_map_it->second.is_bool()) |
| return base::nullopt; |
| |
| options.is_platform_device = option_map_it->second.GetBool(); |
| } |
| |
| option_map_it = option_map.find(CBOR(kResidentKeyMapKey)); |
| if (option_map_it != option_map.end()) { |
| if (!option_map_it->second.is_bool()) |
| return base::nullopt; |
| |
| options.supports_resident_key = option_map_it->second.GetBool(); |
| } |
| |
| option_map_it = option_map.find(CBOR(kUserPresenceMapKey)); |
| if (option_map_it != option_map.end()) { |
| if (!option_map_it->second.is_bool()) |
| return base::nullopt; |
| |
| options.supports_user_presence = option_map_it->second.GetBool(); |
| } |
| |
| option_map_it = option_map.find(CBOR(kUserVerificationMapKey)); |
| if (option_map_it != option_map.end()) { |
| if (!option_map_it->second.is_bool()) |
| return base::nullopt; |
| |
| if (option_map_it->second.GetBool()) { |
| options.user_verification_availability = AuthenticatorSupportedOptions:: |
| UserVerificationAvailability::kSupportedAndConfigured; |
| } else { |
| options.user_verification_availability = AuthenticatorSupportedOptions:: |
| UserVerificationAvailability::kSupportedButNotConfigured; |
| } |
| } |
| |
| option_map_it = option_map.find(CBOR(kClientPinMapKey)); |
| if (option_map_it != option_map.end()) { |
| if (!option_map_it->second.is_bool()) |
| return base::nullopt; |
| |
| if (option_map_it->second.GetBool()) { |
| options.client_pin_availability = AuthenticatorSupportedOptions:: |
| ClientPinAvailability::kSupportedAndPinSet; |
| } else { |
| options.client_pin_availability = AuthenticatorSupportedOptions:: |
| ClientPinAvailability::kSupportedButPinNotSet; |
| } |
| } |
| |
| option_map_it = option_map.find(CBOR(kCredentialManagementMapKey)); |
| if (option_map_it != option_map.end()) { |
| if (!option_map_it->second.is_bool()) { |
| return base::nullopt; |
| } |
| options.supports_credential_management = option_map_it->second.GetBool(); |
| } |
| |
| option_map_it = option_map.find(CBOR(kCredentialManagementPreviewMapKey)); |
| if (option_map_it != option_map.end()) { |
| if (!option_map_it->second.is_bool()) { |
| return base::nullopt; |
| } |
| options.supports_credential_management_preview = |
| option_map_it->second.GetBool(); |
| } |
| |
| response.options = std::move(options); |
| } |
| |
| it = response_map.find(CBOR(5)); |
| if (it != response_map.end()) { |
| if (!it->second.is_unsigned()) |
| return base::nullopt; |
| |
| response.max_msg_size = it->second.GetUnsigned(); |
| } |
| |
| it = response_map.find(CBOR(6)); |
| if (it != response_map.end()) { |
| if (!it->second.is_array()) |
| return base::nullopt; |
| |
| std::vector<uint8_t> supported_pin_protocols; |
| for (const auto& protocol : it->second.GetArray()) { |
| if (!protocol.is_unsigned()) |
| return base::nullopt; |
| |
| supported_pin_protocols.push_back(protocol.GetUnsigned()); |
| } |
| response.pin_protocols = std::move(supported_pin_protocols); |
| } |
| |
| return base::Optional<AuthenticatorGetInfoResponse>(std::move(response)); |
| } |
| |
| static base::Optional<std::string> FixInvalidUTF8String( |
| base::span<const uint8_t> utf8_bytes) { |
| // CTAP2 devices must store at least 64 bytes of any string. |
| if (utf8_bytes.size() < 64) { |
| FIDO_LOG(ERROR) << "Not accepting invalid UTF-8 string because it's only " |
| << utf8_bytes.size() << " bytes long"; |
| return base::nullopt; |
| } |
| |
| base::StreamingUtf8Validator validator; |
| base::StreamingUtf8Validator::State state; |
| size_t longest_valid_prefix_len = 0; |
| |
| for (size_t i = 0; i < utf8_bytes.size(); i++) { |
| state = |
| validator.AddBytes(reinterpret_cast<const char*>(&utf8_bytes[i]), 1); |
| switch (state) { |
| case base::StreamingUtf8Validator::VALID_ENDPOINT: |
| longest_valid_prefix_len = i + 1; |
| break; |
| |
| case base::StreamingUtf8Validator::INVALID: |
| return base::nullopt; |
| |
| case base::StreamingUtf8Validator::VALID_MIDPOINT: |
| break; |
| } |
| } |
| |
| switch (state) { |
| case base::StreamingUtf8Validator::VALID_ENDPOINT: |
| // |base::IsStringUTF8|, which the CBOR code uses, is stricter than |
| // |StreamingUtf8Validator| in that the former rejects ranges of code |
| // points that should never appear. Therefore, if this case occurs, the |
| // string is structurally valid as UTF-8, but includes invalid code points |
| // and thus we reject it. |
| return base::nullopt; |
| |
| case base::StreamingUtf8Validator::INVALID: |
| // This shouldn't happen because we should return immediately if |
| // |INVALID| occurs. |
| NOTREACHED(); |
| return base::nullopt; |
| |
| case base::StreamingUtf8Validator::VALID_MIDPOINT: { |
| // This string has been truncated. This is the case that we expect to |
| // have to handle since CTAP2 devices are permitted to truncate strings |
| // without reference to UTF-8. |
| const std::string candidate( |
| reinterpret_cast<const char*>(utf8_bytes.data()), |
| longest_valid_prefix_len); |
| // Check that the result is now acceptable to |IsStringUTF8|, which is |
| // stricter than |StreamingUtf8Validator|. Without this, a string could |
| // have both contained invalid code-points /and/ been truncated, and this |
| // function would only have corrected the latter issue. |
| if (base::IsStringUTF8(candidate)) { |
| return candidate; |
| } |
| return base::nullopt; |
| } |
| } |
| } |
| |
| typedef bool (*PathPredicate)(const std::vector<const cbor::Value*>&); |
| |
| static base::Optional<cbor::Value> FixInvalidUTF8Value( |
| const cbor::Value& v, |
| std::vector<const cbor::Value*>* path, |
| PathPredicate predicate) { |
| switch (v.type()) { |
| case cbor::Value::Type::INVALID_UTF8: { |
| if (!predicate(*path)) { |
| return base::nullopt; |
| } |
| base::Optional<std::string> maybe_fixed( |
| FixInvalidUTF8String(v.GetInvalidUTF8())); |
| if (!maybe_fixed) { |
| return base::nullopt; |
| } |
| return cbor::Value(*maybe_fixed); |
| } |
| |
| case cbor::Value::Type::UNSIGNED: |
| case cbor::Value::Type::NEGATIVE: |
| case cbor::Value::Type::BYTE_STRING: |
| case cbor::Value::Type::STRING: |
| case cbor::Value::Type::TAG: |
| case cbor::Value::Type::SIMPLE_VALUE: |
| case cbor::Value::Type::NONE: |
| return v.Clone(); |
| |
| case cbor::Value::Type::ARRAY: { |
| const cbor::Value::ArrayValue& old_array = v.GetArray(); |
| cbor::Value::ArrayValue new_array; |
| new_array.reserve(old_array.size()); |
| |
| for (const auto& child : old_array) { |
| base::Optional<cbor::Value> maybe_fixed = |
| FixInvalidUTF8Value(child, path, predicate); |
| if (!maybe_fixed) { |
| return base::nullopt; |
| } |
| new_array.emplace_back(std::move(*maybe_fixed)); |
| } |
| |
| return cbor::Value(new_array); |
| } |
| |
| case cbor::Value::Type::MAP: { |
| const cbor::Value::MapValue& old_map = v.GetMap(); |
| cbor::Value::MapValue new_map; |
| new_map.reserve(old_map.size()); |
| |
| for (const auto& it : old_map) { |
| switch (it.first.type()) { |
| case cbor::Value::Type::INVALID_UTF8: |
| // Invalid strings in map keys are not supported. |
| return base::nullopt; |
| |
| case cbor::Value::Type::UNSIGNED: |
| case cbor::Value::Type::NEGATIVE: |
| case cbor::Value::Type::STRING: |
| break; |
| |
| default: |
| // Other types are not permitted as map keys in CTAP2. |
| return base::nullopt; |
| } |
| |
| path->push_back(&it.first); |
| base::Optional<cbor::Value> maybe_fixed = |
| FixInvalidUTF8Value(it.second, path, predicate); |
| path->pop_back(); |
| if (!maybe_fixed) { |
| return base::nullopt; |
| } |
| |
| new_map.emplace(it.first.Clone(), std::move(*maybe_fixed)); |
| } |
| |
| return cbor::Value(new_map); |
| } |
| } |
| } |
| |
| // ContainsInvalidUTF8 returns true if any element of |v| (recursively) contains |
| // a string with invalid UTF-8. It bases this determination purely on the type |
| // of the nodes and doesn't actually check the contents of the strings |
| // themselves. |
| static bool ContainsInvalidUTF8(const cbor::Value& v) { |
| switch (v.type()) { |
| case cbor::Value::Type::INVALID_UTF8: |
| return true; |
| |
| case cbor::Value::Type::UNSIGNED: |
| case cbor::Value::Type::NEGATIVE: |
| case cbor::Value::Type::BYTE_STRING: |
| case cbor::Value::Type::STRING: |
| case cbor::Value::Type::TAG: |
| case cbor::Value::Type::SIMPLE_VALUE: |
| case cbor::Value::Type::NONE: |
| return false; |
| |
| case cbor::Value::Type::ARRAY: { |
| const cbor::Value::ArrayValue& array = v.GetArray(); |
| for (const auto& child : array) { |
| if (ContainsInvalidUTF8(child)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| case cbor::Value::Type::MAP: { |
| const cbor::Value::MapValue& map = v.GetMap(); |
| for (const auto& it : map) { |
| if (ContainsInvalidUTF8(it.first) || ContainsInvalidUTF8(it.second)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| } |
| } |
| |
| base::Optional<cbor::Value> FixInvalidUTF8(cbor::Value in, |
| PathPredicate predicate) { |
| if (!ContainsInvalidUTF8(in)) { |
| // Common case that everything is fine. |
| return in; |
| } |
| |
| std::vector<const cbor::Value*> path; |
| return FixInvalidUTF8Value(in, &path, predicate); |
| } |
| |
| } // namespace device |