Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2022 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | #include "rkp_factory_extraction_lib.h" |
| 18 | |
| 19 | #include <aidl/android/hardware/security/keymint/IRemotelyProvisionedComponent.h> |
Seth Moore | 7fc83ab | 2023-02-24 16:25:07 -0800 | [diff] [blame] | 20 | #include <android-base/properties.h> |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 21 | #include <android/binder_manager.h> |
| 22 | #include <cppbor.h> |
Seth Moore | b84a1fb | 2022-09-13 12:02:49 -0700 | [diff] [blame] | 23 | #include <cstddef> |
| 24 | #include <cstdint> |
| 25 | #include <cstring> |
| 26 | #include <iterator> |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 27 | #include <keymaster/cppcose/cppcose.h> |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 28 | #include <remote_prov/remote_prov_utils.h> |
| 29 | #include <sys/random.h> |
| 30 | |
| 31 | #include <memory> |
| 32 | #include <optional> |
| 33 | #include <string> |
| 34 | #include <string_view> |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 35 | #include <unordered_set> |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 36 | #include <vector> |
| 37 | |
| 38 | #include "cppbor_parse.h" |
| 39 | |
| 40 | using aidl::android::hardware::security::keymint::DeviceInfo; |
| 41 | using aidl::android::hardware::security::keymint::IRemotelyProvisionedComponent; |
| 42 | using aidl::android::hardware::security::keymint::MacedPublicKey; |
| 43 | using aidl::android::hardware::security::keymint::ProtectedData; |
| 44 | using aidl::android::hardware::security::keymint::RpcHardwareInfo; |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 45 | using aidl::android::hardware::security::keymint::remote_prov::BccEntryData; |
Seth Moore | 0475678 | 2022-09-13 16:09:15 -0700 | [diff] [blame] | 46 | using aidl::android::hardware::security::keymint::remote_prov::EekChain; |
| 47 | using aidl::android::hardware::security::keymint::remote_prov::generateEekChain; |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 48 | using aidl::android::hardware::security::keymint::remote_prov::getProdEekChain; |
| 49 | using aidl::android::hardware::security::keymint::remote_prov::jsonEncodeCsrWithBuild; |
Seth Moore | 0475678 | 2022-09-13 16:09:15 -0700 | [diff] [blame] | 50 | using aidl::android::hardware::security::keymint::remote_prov::parseAndValidateFactoryDeviceInfo; |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 51 | using aidl::android::hardware::security::keymint::remote_prov::verifyFactoryCsr; |
Seth Moore | 0475678 | 2022-09-13 16:09:15 -0700 | [diff] [blame] | 52 | using aidl::android::hardware::security::keymint::remote_prov::verifyFactoryProtectedData; |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 53 | |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 54 | using cppbor::Array; |
| 55 | using cppbor::Map; |
| 56 | using cppbor::Null; |
| 57 | template <class T> using ErrMsgOr = cppcose::ErrMsgOr<T>; |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 58 | |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 59 | constexpr size_t kVersionWithoutSuperencryption = 3; |
| 60 | |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 61 | std::vector<uint8_t> generateChallenge() { |
| 62 | std::vector<uint8_t> challenge(kChallengeSize); |
| 63 | |
| 64 | ssize_t bytesRemaining = static_cast<ssize_t>(challenge.size()); |
| 65 | uint8_t* writePtr = challenge.data(); |
| 66 | while (bytesRemaining > 0) { |
| 67 | int bytesRead = getrandom(writePtr, bytesRemaining, /*flags=*/0); |
| 68 | if (bytesRead < 0) { |
| 69 | if (errno == EINTR) { |
| 70 | continue; |
| 71 | } else { |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 72 | std::cerr << "generateChallenge: getrandom returned an error with errno " << errno |
| 73 | << ": " << strerror(errno) << ". Exiting..." << std::endl; |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 74 | exit(-1); |
| 75 | } |
| 76 | } |
| 77 | bytesRemaining -= bytesRead; |
| 78 | writePtr += bytesRead; |
| 79 | } |
| 80 | |
| 81 | return challenge; |
| 82 | } |
| 83 | |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 84 | CborResult<Array> composeCertificateRequestV1(const ProtectedData& protectedData, |
| 85 | const DeviceInfo& verifiedDeviceInfo, |
| 86 | const std::vector<uint8_t>& challenge, |
| 87 | const std::vector<uint8_t>& keysToSignMac, |
Sean Thomas | 6d713f2 | 2024-11-08 00:01:23 +0000 | [diff] [blame^] | 88 | const RpcHardwareInfo& rpcHardwareInfo) { |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 89 | Array macedKeysToSign = Array() |
| 90 | .add(Map().add(1, 5).encode()) // alg: hmac-sha256 |
| 91 | .add(Map()) // empty unprotected headers |
| 92 | .add(Null()) // nil for the payload |
| 93 | .add(keysToSignMac); // MAC as returned from the HAL |
| 94 | |
Seth Moore | b84a1fb | 2022-09-13 12:02:49 -0700 | [diff] [blame] | 95 | ErrMsgOr<std::unique_ptr<Map>> parsedVerifiedDeviceInfo = |
Sean Thomas | 6d713f2 | 2024-11-08 00:01:23 +0000 | [diff] [blame^] | 96 | parseAndValidateFactoryDeviceInfo(verifiedDeviceInfo.deviceInfo, rpcHardwareInfo); |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 97 | if (!parsedVerifiedDeviceInfo) { |
Seth Moore | b84a1fb | 2022-09-13 12:02:49 -0700 | [diff] [blame] | 98 | return {nullptr, parsedVerifiedDeviceInfo.moveMessage()}; |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 99 | } |
| 100 | |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 101 | auto [parsedProtectedData, ignore2, errMsg] = cppbor::parse(protectedData.protectedData); |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 102 | if (!parsedProtectedData) { |
Seth Moore | b84a1fb | 2022-09-13 12:02:49 -0700 | [diff] [blame] | 103 | std::cerr << "Error parsing protected data: '" << errMsg << "'" << std::endl; |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 104 | return {nullptr, errMsg}; |
| 105 | } |
| 106 | |
Seth Moore | b84a1fb | 2022-09-13 12:02:49 -0700 | [diff] [blame] | 107 | Array deviceInfo = Array().add(parsedVerifiedDeviceInfo.moveValue()).add(Map()); |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 108 | |
| 109 | auto certificateRequest = std::make_unique<Array>(); |
| 110 | (*certificateRequest) |
| 111 | .add(std::move(deviceInfo)) |
| 112 | .add(challenge) |
| 113 | .add(std::move(parsedProtectedData)) |
| 114 | .add(std::move(macedKeysToSign)); |
Seth Moore | b84a1fb | 2022-09-13 12:02:49 -0700 | [diff] [blame] | 115 | return {std::move(certificateRequest), ""}; |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 116 | } |
| 117 | |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 118 | CborResult<Array> getCsrV1(std::string_view componentName, IRemotelyProvisionedComponent* irpc) { |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 119 | std::vector<uint8_t> keysToSignMac; |
| 120 | std::vector<MacedPublicKey> emptyKeys; |
| 121 | DeviceInfo verifiedDeviceInfo; |
| 122 | ProtectedData protectedData; |
| 123 | RpcHardwareInfo hwInfo; |
| 124 | ::ndk::ScopedAStatus status = irpc->getHardwareInfo(&hwInfo); |
| 125 | if (!status.isOk()) { |
| 126 | std::cerr << "Failed to get hardware info for '" << componentName |
Robert Shih | 6c3e15b | 2023-12-18 20:09:02 -0800 | [diff] [blame] | 127 | << "'. Description: " << status.getDescription() << "." << std::endl; |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 128 | return {nullptr, status.getDescription()}; |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 129 | } |
| 130 | |
| 131 | const std::vector<uint8_t> eek = getProdEekChain(hwInfo.supportedEekCurve); |
| 132 | const std::vector<uint8_t> challenge = generateChallenge(); |
| 133 | status = irpc->generateCertificateRequest( |
| 134 | /*test_mode=*/false, emptyKeys, eek, challenge, &verifiedDeviceInfo, &protectedData, |
| 135 | &keysToSignMac); |
| 136 | if (!status.isOk()) { |
| 137 | std::cerr << "Bundle extraction failed for '" << componentName |
Robert Shih | 6c3e15b | 2023-12-18 20:09:02 -0800 | [diff] [blame] | 138 | << "'. Description: " << status.getDescription() << "." << std::endl; |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 139 | return {nullptr, status.getDescription()}; |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 140 | } |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 141 | return composeCertificateRequestV1(protectedData, verifiedDeviceInfo, challenge, keysToSignMac, |
Sean Thomas | 6d713f2 | 2024-11-08 00:01:23 +0000 | [diff] [blame^] | 142 | hwInfo); |
Seth Moore | 708da93 | 2022-08-18 14:38:05 -0700 | [diff] [blame] | 143 | } |
Seth Moore | 0475678 | 2022-09-13 16:09:15 -0700 | [diff] [blame] | 144 | |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 145 | std::optional<std::string> selfTestGetCsrV1(std::string_view componentName, |
| 146 | IRemotelyProvisionedComponent* irpc) { |
Seth Moore | 0475678 | 2022-09-13 16:09:15 -0700 | [diff] [blame] | 147 | std::vector<uint8_t> keysToSignMac; |
| 148 | std::vector<MacedPublicKey> emptyKeys; |
| 149 | DeviceInfo verifiedDeviceInfo; |
| 150 | ProtectedData protectedData; |
| 151 | RpcHardwareInfo hwInfo; |
| 152 | ::ndk::ScopedAStatus status = irpc->getHardwareInfo(&hwInfo); |
| 153 | if (!status.isOk()) { |
| 154 | std::cerr << "Failed to get hardware info for '" << componentName |
Robert Shih | 6c3e15b | 2023-12-18 20:09:02 -0800 | [diff] [blame] | 155 | << "'. Description: " << status.getDescription() << "." << std::endl; |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 156 | return status.getDescription(); |
Seth Moore | 0475678 | 2022-09-13 16:09:15 -0700 | [diff] [blame] | 157 | } |
| 158 | |
| 159 | const std::vector<uint8_t> eekId = {0, 1, 2, 3, 4, 5, 6, 7}; |
| 160 | ErrMsgOr<EekChain> eekChain = generateEekChain(hwInfo.supportedEekCurve, /*length=*/3, eekId); |
| 161 | if (!eekChain) { |
| 162 | std::cerr << "Error generating test EEK certificate chain: " << eekChain.message(); |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 163 | return eekChain.message(); |
Seth Moore | 0475678 | 2022-09-13 16:09:15 -0700 | [diff] [blame] | 164 | } |
| 165 | const std::vector<uint8_t> challenge = generateChallenge(); |
| 166 | status = irpc->generateCertificateRequest( |
| 167 | /*test_mode=*/true, emptyKeys, eekChain->chain, challenge, &verifiedDeviceInfo, |
| 168 | &protectedData, &keysToSignMac); |
| 169 | if (!status.isOk()) { |
| 170 | std::cerr << "Error generating test cert chain for '" << componentName |
Robert Shih | 6c3e15b | 2023-12-18 20:09:02 -0800 | [diff] [blame] | 171 | << "'. Description: " << status.getDescription() << "." << std::endl; |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 172 | return status.getDescription(); |
Seth Moore | 0475678 | 2022-09-13 16:09:15 -0700 | [diff] [blame] | 173 | } |
| 174 | |
Sean Thomas | 6d713f2 | 2024-11-08 00:01:23 +0000 | [diff] [blame^] | 175 | auto result = verifyFactoryProtectedData(verifiedDeviceInfo, /*keysToSign=*/{}, keysToSignMac, |
| 176 | protectedData, *eekChain, eekId, hwInfo, |
| 177 | std::string(componentName), challenge); |
Seth Moore | 0475678 | 2022-09-13 16:09:15 -0700 | [diff] [blame] | 178 | |
Seth Moore | dff09d0 | 2023-05-31 09:38:47 -0700 | [diff] [blame] | 179 | if (!result) { |
| 180 | std::cerr << "Self test failed for IRemotelyProvisionedComponent '" << componentName |
| 181 | << "'. Error message: '" << result.message() << "'." << std::endl; |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 182 | return result.message(); |
Seth Moore | dff09d0 | 2023-05-31 09:38:47 -0700 | [diff] [blame] | 183 | } |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 184 | return std::nullopt; |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 185 | } |
| 186 | |
| 187 | CborResult<Array> composeCertificateRequestV3(const std::vector<uint8_t>& csr) { |
Seth Moore | 7fc83ab | 2023-02-24 16:25:07 -0800 | [diff] [blame] | 188 | const std::string kFingerprintProp = "ro.build.fingerprint"; |
| 189 | |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 190 | auto [parsedCsr, _, csrErrMsg] = cppbor::parse(csr); |
| 191 | if (!parsedCsr) { |
| 192 | return {nullptr, csrErrMsg}; |
| 193 | } |
| 194 | if (!parsedCsr->asArray()) { |
| 195 | return {nullptr, "CSR is not a CBOR array."}; |
| 196 | } |
| 197 | |
Seth Moore | 7fc83ab | 2023-02-24 16:25:07 -0800 | [diff] [blame] | 198 | if (!::android::base::WaitForPropertyCreation(kFingerprintProp)) { |
| 199 | return {nullptr, "Unable to read build fingerprint"}; |
| 200 | } |
| 201 | |
| 202 | Map unverifiedDeviceInfo = |
| 203 | Map().add("fingerprint", ::android::base::GetProperty(kFingerprintProp, /*default=*/"")); |
| 204 | parsedCsr->asArray()->add(std::move(unverifiedDeviceInfo)); |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 205 | return {std::unique_ptr<Array>(parsedCsr.release()->asArray()), ""}; |
| 206 | } |
| 207 | |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 208 | CborResult<Array> getCsrV3(std::string_view componentName, IRemotelyProvisionedComponent* irpc, |
| 209 | bool selfTest, bool allowDegenerate, bool requireUdsCerts) { |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 210 | std::vector<uint8_t> csr; |
| 211 | std::vector<MacedPublicKey> emptyKeys; |
| 212 | const std::vector<uint8_t> challenge = generateChallenge(); |
| 213 | |
Sean Thomas | 6d713f2 | 2024-11-08 00:01:23 +0000 | [diff] [blame^] | 214 | RpcHardwareInfo hwInfo; |
| 215 | auto status = irpc->getHardwareInfo(&hwInfo); |
| 216 | if (!status.isOk()) { |
| 217 | std::cerr << "Failed to get hardware info for '" << componentName |
| 218 | << "'. Description: " << status.getDescription() << "." << std::endl; |
| 219 | return {nullptr, status.getDescription()}; |
| 220 | } |
| 221 | |
| 222 | status = irpc->generateCertificateRequestV2(emptyKeys, challenge, &csr); |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 223 | if (!status.isOk()) { |
| 224 | std::cerr << "Bundle extraction failed for '" << componentName |
Robert Shih | 6c3e15b | 2023-12-18 20:09:02 -0800 | [diff] [blame] | 225 | << "'. Description: " << status.getDescription() << "." << std::endl; |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 226 | return {nullptr, status.getDescription()}; |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 227 | } |
| 228 | |
Seth Moore | dff09d0 | 2023-05-31 09:38:47 -0700 | [diff] [blame] | 229 | if (selfTest) { |
Sean Thomas | 6d713f2 | 2024-11-08 00:01:23 +0000 | [diff] [blame^] | 230 | auto result = verifyFactoryCsr(/*keysToSign=*/cppbor::Array(), csr, hwInfo, |
| 231 | std::string(componentName), challenge, allowDegenerate, |
| 232 | requireUdsCerts); |
Seth Moore | dff09d0 | 2023-05-31 09:38:47 -0700 | [diff] [blame] | 233 | if (!result) { |
| 234 | std::cerr << "Self test failed for IRemotelyProvisionedComponent '" << componentName |
| 235 | << "'. Error message: '" << result.message() << "'." << std::endl; |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 236 | return {nullptr, result.message()}; |
Seth Moore | dff09d0 | 2023-05-31 09:38:47 -0700 | [diff] [blame] | 237 | } |
| 238 | } |
| 239 | |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 240 | return composeCertificateRequestV3(csr); |
| 241 | } |
| 242 | |
Seth Moore | dff09d0 | 2023-05-31 09:38:47 -0700 | [diff] [blame] | 243 | CborResult<Array> getCsr(std::string_view componentName, IRemotelyProvisionedComponent* irpc, |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 244 | bool selfTest, bool allowDegenerate, bool requireUdsCerts) { |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 245 | RpcHardwareInfo hwInfo; |
| 246 | auto status = irpc->getHardwareInfo(&hwInfo); |
| 247 | if (!status.isOk()) { |
| 248 | std::cerr << "Failed to get hardware info for '" << componentName |
Robert Shih | 6c3e15b | 2023-12-18 20:09:02 -0800 | [diff] [blame] | 249 | << "'. Description: " << status.getDescription() << "." << std::endl; |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 250 | return {nullptr, status.getDescription()}; |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 251 | } |
| 252 | |
| 253 | if (hwInfo.versionNumber < kVersionWithoutSuperencryption) { |
Seth Moore | dff09d0 | 2023-05-31 09:38:47 -0700 | [diff] [blame] | 254 | if (selfTest) { |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 255 | auto errMsg = selfTestGetCsrV1(componentName, irpc); |
| 256 | if (errMsg) { |
| 257 | return {nullptr, *errMsg}; |
| 258 | } |
Seth Moore | dff09d0 | 2023-05-31 09:38:47 -0700 | [diff] [blame] | 259 | } |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 260 | return getCsrV1(componentName, irpc); |
| 261 | } else { |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 262 | return getCsrV3(componentName, irpc, selfTest, allowDegenerate, requireUdsCerts); |
Tri Vo | ee773a2 | 2022-10-26 16:07:52 -0700 | [diff] [blame] | 263 | } |
| 264 | } |
Alice Wang | 16e3442 | 2024-06-07 12:41:22 +0000 | [diff] [blame] | 265 | |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 266 | std::unordered_set<std::string> parseCommaDelimited(const std::string& input) { |
| 267 | std::stringstream ss(input); |
| 268 | std::unordered_set<std::string> result; |
| 269 | while (ss.good()) { |
| 270 | std::string name; |
| 271 | std::getline(ss, name, ','); |
Sean Thomas | 3f38c1c | 2024-11-05 10:51:56 +0000 | [diff] [blame] | 272 | if (!name.empty()) { |
| 273 | result.insert(name); |
| 274 | } |
Alice Wang | 16e3442 | 2024-06-07 12:41:22 +0000 | [diff] [blame] | 275 | } |
Sean Thomas | 61c3ed5 | 2024-10-16 11:25:42 +0000 | [diff] [blame] | 276 | return result; |
| 277 | } |