nsClientAuthRemember.cpp (14008B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- 2 * 3 * This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "nsClientAuthRemember.h" 8 9 #include "mozilla/BasePrincipal.h" 10 #include "mozilla/RefPtr.h" 11 #include "nsCRT.h" 12 #include "nsINSSComponent.h" 13 #include "nsPrintfCString.h" 14 #include "nsNSSComponent.h" 15 #include "nsIDataStorage.h" 16 #include "nsIObserverService.h" 17 #include "nsNetUtil.h" 18 #include "nsPromiseFlatString.h" 19 #include "nsThreadUtils.h" 20 #include "cert.h" 21 #include "nspr.h" 22 #include "pk11pub.h" 23 #include "certdb.h" 24 #include "sechash.h" 25 26 #include "nsJSUtils.h" 27 28 #ifdef XP_MACOSX 29 # include <CoreFoundation/CoreFoundation.h> 30 # include <Security/Security.h> 31 # include "KeychainSecret.h" // for ScopedCFType 32 #endif // XP_MACOSX 33 34 using namespace mozilla; 35 using namespace mozilla::psm; 36 37 NS_IMPL_ISUPPORTS(nsClientAuthRememberService, nsIClientAuthRememberService) 38 NS_IMPL_ISUPPORTS(nsClientAuthRemember, nsIClientAuthRememberRecord) 39 40 NS_IMETHODIMP 41 nsClientAuthRemember::GetAsciiHost(/*out*/ nsACString& aAsciiHost) { 42 aAsciiHost = mAsciiHost; 43 return NS_OK; 44 } 45 46 NS_IMETHODIMP 47 nsClientAuthRemember::GetDbKey(/*out*/ nsACString& aDBKey) { 48 aDBKey = mDBKey; 49 return NS_OK; 50 } 51 52 NS_IMETHODIMP 53 nsClientAuthRemember::GetEntryKey(/*out*/ nsACString& aEntryKey) { 54 aEntryKey.Assign(mAsciiHost); 55 aEntryKey.Append(','); 56 // This used to include the SHA-256 hash of the server certificate. 57 aEntryKey.Append(','); 58 aEntryKey.Append(mOriginAttributesSuffix); 59 return NS_OK; 60 } 61 62 nsresult nsClientAuthRememberService::Init() { 63 if (!NS_IsMainThread()) { 64 NS_ERROR("nsClientAuthRememberService::Init called off the main thread"); 65 return NS_ERROR_NOT_SAME_THREAD; 66 } 67 68 nsCOMPtr<nsIDataStorageManager> dataStorageManager( 69 do_GetService("@mozilla.org/security/datastoragemanager;1")); 70 if (!dataStorageManager) { 71 return NS_ERROR_FAILURE; 72 } 73 nsresult rv = 74 dataStorageManager->Get(nsIDataStorageManager::ClientAuthRememberList, 75 getter_AddRefs(mClientAuthRememberList)); 76 if (NS_FAILED(rv)) { 77 return rv; 78 } 79 if (!mClientAuthRememberList) { 80 return NS_ERROR_FAILURE; 81 } 82 83 return NS_OK; 84 } 85 86 NS_IMETHODIMP 87 nsClientAuthRememberService::ForgetRememberedDecision(const nsACString& key) { 88 nsresult rv = mClientAuthRememberList->Remove( 89 PromiseFlatCString(key), nsIDataStorage::DataType::Persistent); 90 if (NS_FAILED(rv)) { 91 return rv; 92 } 93 rv = mClientAuthRememberList->Remove(PromiseFlatCString(key), 94 nsIDataStorage::DataType::Temporary); 95 if (NS_FAILED(rv)) { 96 return rv; 97 } 98 nsCOMPtr<nsINSSComponent> nssComponent(do_GetService(NS_NSSCOMPONENT_CID)); 99 if (!nssComponent) { 100 return NS_ERROR_NOT_AVAILABLE; 101 } 102 return nssComponent->ClearSSLExternalAndInternalSessionCache(); 103 } 104 105 NS_IMETHODIMP 106 nsClientAuthRememberService::GetDecisions( 107 nsTArray<RefPtr<nsIClientAuthRememberRecord>>& results) { 108 nsTArray<RefPtr<nsIDataStorageItem>> decisions; 109 nsresult rv = mClientAuthRememberList->GetAll(decisions); 110 if (NS_FAILED(rv)) { 111 return rv; 112 } 113 114 for (const auto& decision : decisions) { 115 nsIDataStorage::DataType type; 116 rv = decision->GetType(&type); 117 if (NS_FAILED(rv)) { 118 return rv; 119 } 120 if (type == nsIDataStorage::DataType::Persistent || 121 type == nsIDataStorage::DataType::Temporary) { 122 nsAutoCString key; 123 rv = decision->GetKey(key); 124 if (NS_FAILED(rv)) { 125 return rv; 126 } 127 nsAutoCString value; 128 rv = decision->GetValue(value); 129 if (NS_FAILED(rv)) { 130 return rv; 131 } 132 RefPtr<nsIClientAuthRememberRecord> tmp = 133 new nsClientAuthRemember(key, value); 134 135 results.AppendElement(tmp); 136 } 137 } 138 139 return NS_OK; 140 } 141 142 NS_IMETHODIMP 143 nsClientAuthRememberService::ClearRememberedDecisions() { 144 nsresult rv = mClientAuthRememberList->Clear(); 145 if (NS_FAILED(rv)) { 146 return rv; 147 } 148 nsCOMPtr<nsINSSComponent> nssComponent(do_GetService(NS_NSSCOMPONENT_CID)); 149 if (!nssComponent) { 150 return NS_ERROR_NOT_AVAILABLE; 151 } 152 return nssComponent->ClearSSLExternalAndInternalSessionCache(); 153 } 154 155 NS_IMETHODIMP 156 nsClientAuthRememberService::DeleteDecisionsByHost( 157 const nsACString& aHostName, JS::Handle<JS::Value> aOriginAttributes, 158 JSContext* aCx) { 159 OriginAttributes attrs; 160 if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { 161 return NS_ERROR_INVALID_ARG; 162 } 163 bool isPrivateContext = attrs.IsPrivateBrowsing(); 164 165 nsTArray<RefPtr<nsIDataStorageItem>> decisions; 166 nsresult rv = mClientAuthRememberList->GetAll(decisions); 167 if (NS_FAILED(rv)) { 168 return rv; 169 } 170 171 for (const auto& decision : decisions) { 172 nsIDataStorage::DataType type; 173 nsresult rv = decision->GetType(&type); 174 if (NS_FAILED(rv)) { 175 return rv; 176 } 177 bool isPrivateDecision = type == nsIDataStorage::DataType::Private; 178 if (isPrivateContext == isPrivateDecision) { 179 nsAutoCString key; 180 rv = decision->GetKey(key); 181 if (NS_FAILED(rv)) { 182 return rv; 183 } 184 nsAutoCString value; 185 rv = decision->GetValue(value); 186 if (NS_FAILED(rv)) { 187 return rv; 188 } 189 RefPtr<nsIClientAuthRememberRecord> tmp = 190 new nsClientAuthRemember(key, value); 191 nsAutoCString asciiHost; 192 tmp->GetAsciiHost(asciiHost); 193 if (asciiHost.Equals(aHostName)) { 194 rv = mClientAuthRememberList->Remove(key, type); 195 if (NS_FAILED(rv)) { 196 return rv; 197 } 198 } 199 } 200 } 201 nsCOMPtr<nsINSSComponent> nssComponent(do_GetService(NS_NSSCOMPONENT_CID)); 202 if (!nssComponent) { 203 return NS_ERROR_NOT_AVAILABLE; 204 } 205 return nssComponent->ClearSSLExternalAndInternalSessionCache(); 206 } 207 208 NS_IMETHODIMP 209 nsClientAuthRememberService::RememberDecisionScriptable( 210 const nsACString& aHostName, JS::Handle<JS::Value> aOriginAttributes, 211 nsIX509Cert* aClientCert, Duration aDuration, JSContext* aCx) { 212 OriginAttributes attrs; 213 if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { 214 return NS_ERROR_INVALID_ARG; 215 } 216 return RememberDecision(aHostName, attrs, aClientCert, aDuration); 217 } 218 219 NS_IMETHODIMP 220 nsClientAuthRememberService::RememberDecision( 221 const nsACString& aHostName, const OriginAttributes& aOriginAttributes, 222 nsIX509Cert* aClientCert, Duration aDuration) { 223 if (aHostName.IsEmpty()) { 224 return NS_ERROR_INVALID_ARG; 225 } 226 227 // If a decision is to only be used once, it doesn't need to be remembered in 228 // any way. 229 if (aDuration == nsIClientAuthRememberService::Duration::Once) { 230 return NS_OK; 231 } 232 233 // aClientCert == nullptr means: remember that user does not want to use a 234 // cert 235 if (aClientCert) { 236 nsAutoCString dbkey; 237 nsresult rv = aClientCert->GetDbKey(dbkey); 238 if (NS_FAILED(rv)) { 239 return rv; 240 } 241 return AddEntryToList(aHostName, aOriginAttributes, dbkey, aDuration); 242 } 243 return AddEntryToList(aHostName, aOriginAttributes, 244 nsClientAuthRemember::SentinelValue, aDuration); 245 } 246 247 #ifdef XP_MACOSX 248 // On macOS, users can add "identity preference" items in the keychain. These 249 // can be added via the Keychain Access tool. These specify mappings from 250 // URLs/wildcards like "*.mozilla.org" to specific client certificates. This 251 // function retrieves the preferred client certificate for a hostname by 252 // querying a system API that checks for these identity preferences. 253 nsresult CheckForPreferredCertificate(const nsACString& aHostName, 254 nsACString& aCertDBKey) { 255 aCertDBKey.Truncate(); 256 // SecIdentityCopyPreferred seems to expect a proper URI which it can use 257 // for prefix and wildcard matches. 258 // We don't have the full URL but we can turn the hostname into a URI with 259 // an authority section, so that it matches against macOS identity preferences 260 // like `*.foo.com`. If we know that this connection is always going to be 261 // https, then we should put that in the URI as well, so that it matches 262 // identity preferences like `https://foo.com/` as well. If we can plumb 263 // the path or the full URL into this function we could also match identity 264 // preferences like `https://foo.com/bar/` but for now we cannot. 265 nsPrintfCString fakeUrl("//%s/", PromiseFlatCString(aHostName).get()); 266 ScopedCFType<CFStringRef> host(::CFStringCreateWithCString( 267 kCFAllocatorDefault, fakeUrl.get(), kCFStringEncodingUTF8)); 268 if (!host) { 269 return NS_ERROR_UNEXPECTED; 270 } 271 ScopedCFType<SecIdentityRef> identity( 272 ::SecIdentityCopyPreferred(host.get(), NULL, NULL)); 273 if (!identity) { 274 // No preferred identity for this hostname, leave aCertDBKey empty and 275 // return 276 return NS_OK; 277 } 278 SecCertificateRef certRefRaw = NULL; 279 OSStatus copyResult = 280 ::SecIdentityCopyCertificate(identity.get(), &certRefRaw); 281 ScopedCFType<SecCertificateRef> certRef(certRefRaw); 282 if (copyResult != errSecSuccess || certRef.get() == NULL) { 283 return NS_ERROR_UNEXPECTED; 284 } 285 ScopedCFType<CFDataRef> der(::SecCertificateCopyData(certRef.get())); 286 if (!der) { 287 return NS_ERROR_UNEXPECTED; 288 } 289 290 nsTArray<uint8_t> derArray(::CFDataGetBytePtr(der.get()), 291 ::CFDataGetLength(der.get())); 292 nsCOMPtr<nsIX509Cert> cert(new nsNSSCertificate(std::move(derArray))); 293 return cert->GetDbKey(aCertDBKey); 294 } 295 #endif 296 297 void nsClientAuthRememberService::Migrate() { 298 auto migrated = mMigrated.Lock(); 299 if (*migrated) { 300 return; 301 } 302 *migrated = true; 303 304 nsTArray<RefPtr<nsIDataStorageItem>> decisions; 305 nsresult rv = mClientAuthRememberList->GetAll(decisions); 306 if (NS_FAILED(rv)) { 307 return; 308 } 309 for (const auto& decision : decisions) { 310 nsIDataStorage::DataType type; 311 if (NS_FAILED(decision->GetType(&type))) { 312 continue; 313 } 314 if (type != nsIDataStorage::DataType::Persistent) { 315 continue; 316 } 317 nsAutoCString key; 318 if (NS_FAILED(decision->GetKey(key))) { 319 continue; 320 } 321 nsAutoCString value; 322 if (NS_FAILED(decision->GetValue(value))) { 323 continue; 324 } 325 RefPtr<nsClientAuthRemember> entry(new nsClientAuthRemember(key, value)); 326 nsAutoCString newKey; 327 if (NS_FAILED(entry->GetEntryKey(newKey))) { 328 continue; 329 } 330 if (newKey != key) { 331 if (NS_FAILED(mClientAuthRememberList->Remove( 332 key, nsIDataStorage::DataType::Persistent))) { 333 continue; 334 } 335 if (NS_FAILED(mClientAuthRememberList->Put( 336 newKey, value, nsIDataStorage::DataType::Persistent))) { 337 continue; 338 } 339 } 340 } 341 } 342 343 NS_IMETHODIMP 344 nsClientAuthRememberService::HasRememberedDecision( 345 const nsACString& aHostName, const OriginAttributes& aOriginAttributes, 346 nsACString& aCertDBKey, bool* aRetVal) { 347 NS_ENSURE_ARG_POINTER(aRetVal); 348 if (aHostName.IsEmpty()) { 349 return NS_ERROR_INVALID_ARG; 350 } 351 352 *aRetVal = false; 353 aCertDBKey.Truncate(); 354 355 Migrate(); 356 357 nsAutoCString entryKey; 358 RefPtr<nsClientAuthRemember> entry( 359 new nsClientAuthRemember(aHostName, aOriginAttributes)); 360 nsresult rv = entry->GetEntryKey(entryKey); 361 if (NS_FAILED(rv)) { 362 return rv; 363 } 364 365 nsTArray<nsIDataStorage::DataType> typesToTry; 366 if (aOriginAttributes.IsPrivateBrowsing()) { 367 typesToTry.AppendElement(nsIDataStorage::DataType::Private); 368 } else { 369 typesToTry.AppendElement(nsIDataStorage::DataType::Persistent); 370 typesToTry.AppendElement(nsIDataStorage::DataType::Temporary); 371 } 372 373 for (const auto& storageType : typesToTry) { 374 nsAutoCString listEntry; 375 rv = mClientAuthRememberList->Get(entryKey, storageType, listEntry); 376 if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE) { 377 return rv; 378 } 379 if (NS_SUCCEEDED(rv) && !listEntry.IsEmpty()) { 380 if (!listEntry.Equals(nsClientAuthRemember::SentinelValue)) { 381 aCertDBKey = listEntry; 382 } 383 *aRetVal = true; 384 return NS_OK; 385 } 386 } 387 388 #ifdef XP_MACOSX 389 rv = CheckForPreferredCertificate(aHostName, aCertDBKey); 390 if (NS_FAILED(rv)) { 391 return rv; 392 } 393 if (!aCertDBKey.IsEmpty()) { 394 *aRetVal = true; 395 return NS_OK; 396 } 397 #endif 398 399 return NS_OK; 400 } 401 402 NS_IMETHODIMP 403 nsClientAuthRememberService::HasRememberedDecisionScriptable( 404 const nsACString& aHostName, JS::Handle<JS::Value> aOriginAttributes, 405 nsACString& aCertDBKey, JSContext* aCx, bool* aRetVal) { 406 OriginAttributes attrs; 407 if (!aOriginAttributes.isObject() || !attrs.Init(aCx, aOriginAttributes)) { 408 return NS_ERROR_INVALID_ARG; 409 } 410 return HasRememberedDecision(aHostName, attrs, aCertDBKey, aRetVal); 411 } 412 413 nsresult nsClientAuthRememberService::AddEntryToList( 414 const nsACString& aHostName, const OriginAttributes& aOriginAttributes, 415 const nsACString& aDBKey, Duration aDuration) { 416 nsAutoCString entryKey; 417 RefPtr<nsClientAuthRemember> entry( 418 new nsClientAuthRemember(aHostName, aOriginAttributes)); 419 nsresult rv = entry->GetEntryKey(entryKey); 420 if (NS_FAILED(rv)) { 421 return rv; 422 } 423 424 nsIDataStorage::DataType storageType; 425 if (aOriginAttributes.IsPrivateBrowsing()) { 426 storageType = nsIDataStorage::DataType::Private; 427 } else if (aDuration == nsIClientAuthRememberService::Duration::Permanent) { 428 storageType = nsIDataStorage::DataType::Persistent; 429 } else if (aDuration == nsIClientAuthRememberService::Duration::Session) { 430 storageType = nsIDataStorage::DataType::Temporary; 431 } else { 432 return NS_ERROR_INVALID_ARG; 433 } 434 435 nsCString tmpDbKey(aDBKey); 436 rv = mClientAuthRememberList->Put(entryKey, tmpDbKey, storageType); 437 if (NS_FAILED(rv)) { 438 return rv; 439 } 440 441 return NS_OK; 442 } 443 444 bool nsClientAuthRememberService::IsPrivateBrowsingKey( 445 const nsCString& entryKey) { 446 const int32_t separator = entryKey.Find(":"); 447 nsCString suffix; 448 if (separator >= 0) { 449 entryKey.Left(suffix, separator); 450 } else { 451 suffix = entryKey; 452 } 453 return OriginAttributes::IsPrivateBrowsing(suffix); 454 }