1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "net/cert/cert_database.h"
6
7#include <Security/Security.h>
8
9#include "base/logging.h"
10#include "base/mac/mac_logging.h"
11#include "base/message_loop/message_loop.h"
12#include "base/observer_list_threadsafe.h"
13#include "base/process/process_handle.h"
14#include "base/single_thread_task_runner.h"
15#include "base/synchronization/lock.h"
16#include "crypto/mac_security_services_lock.h"
17#include "net/base/net_errors.h"
18#include "net/cert/x509_certificate.h"
19
20namespace net {
21
22// Helper that observes events from the Keychain and forwards them to the
23// given CertDatabase.
24class CertDatabase::Notifier {
25 public:
26  // Creates a new Notifier that will forward Keychain events to |cert_db|.
27  // |message_loop| must refer to a thread with an associated CFRunLoop - a
28  // TYPE_UI thread. Events will be dispatched from this message loop.
29  Notifier(CertDatabase* cert_db, base::MessageLoop* message_loop)
30      : cert_db_(cert_db),
31        registered_(false),
32        called_shutdown_(false) {
33    // Ensure an associated CFRunLoop.
34    DCHECK(base::MessageLoopForUI::IsCurrent());
35    task_runner_ = message_loop->message_loop_proxy();
36    task_runner_->PostTask(FROM_HERE,
37                           base::Bind(&Notifier::Init,
38                                      base::Unretained(this)));
39  }
40
41  // Should be called from the |task_runner_|'s thread. Use Shutdown()
42  // to shutdown on arbitrary threads.
43  ~Notifier() {
44    DCHECK(called_shutdown_);
45    // Only unregister from the same thread where registration was performed.
46    if (registered_ && task_runner_->RunsTasksOnCurrentThread())
47      SecKeychainRemoveCallback(&Notifier::KeychainCallback);
48  }
49
50  void Shutdown() {
51    called_shutdown_ = true;
52    if (!task_runner_->DeleteSoon(FROM_HERE, this)) {
53      // If the task runner is no longer running, it's safe to just delete
54      // the object, since no further events will or can be delivered by
55      // Keychain Services.
56      delete this;
57    }
58  }
59
60 private:
61  void Init() {
62    SecKeychainEventMask event_mask =
63        kSecKeychainListChangedMask | kSecTrustSettingsChangedEventMask;
64    OSStatus status = SecKeychainAddCallback(&Notifier::KeychainCallback,
65                                             event_mask, this);
66    if (status == noErr)
67      registered_ = true;
68  }
69
70  // SecKeychainCallback function that receives notifications from securityd
71  // and forwards them to the |cert_db_|.
72  static OSStatus KeychainCallback(SecKeychainEvent keychain_event,
73                                   SecKeychainCallbackInfo* info,
74                                   void* context);
75
76  CertDatabase* const cert_db_;
77  scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
78  bool registered_;
79  bool called_shutdown_;
80};
81
82// static
83OSStatus CertDatabase::Notifier::KeychainCallback(
84    SecKeychainEvent keychain_event,
85    SecKeychainCallbackInfo* info,
86    void* context) {
87  Notifier* that = reinterpret_cast<Notifier*>(context);
88
89  if (info->version > SEC_KEYCHAIN_SETTINGS_VERS1) {
90    NOTREACHED();
91    return errSecWrongSecVersion;
92  }
93
94  if (info->pid == base::GetCurrentProcId()) {
95    // Ignore events generated by the current process, as the assumption is
96    // that they have already been handled. This may miss events that
97    // originated as a result of spawning native dialogs that allow the user
98    // to modify Keychain settings. However, err on the side of missing
99    // events rather than sending too many events.
100    return errSecSuccess;
101  }
102
103  switch (keychain_event) {
104    case kSecKeychainListChangedEvent:
105    case kSecTrustSettingsChangedEvent:
106      that->cert_db_->NotifyObserversOfCACertChanged(NULL);
107      break;
108  }
109
110  return errSecSuccess;
111}
112
113void CertDatabase::SetMessageLoopForKeychainEvents() {
114  // Shutdown will take care to delete the notifier on the right thread.
115  if (notifier_.get())
116    notifier_.release()->Shutdown();
117
118  notifier_.reset(new Notifier(this, base::MessageLoopForUI::current()));
119}
120
121CertDatabase::CertDatabase()
122    : observer_list_(new ObserverListThreadSafe<Observer>) {
123}
124
125CertDatabase::~CertDatabase() {
126  // Shutdown will take care to delete the notifier on the right thread.
127  if (notifier_.get())
128    notifier_.release()->Shutdown();
129}
130
131int CertDatabase::CheckUserCert(X509Certificate* cert) {
132  if (!cert)
133    return ERR_CERT_INVALID;
134  if (cert->HasExpired())
135    return ERR_CERT_DATE_INVALID;
136
137  // Verify the Keychain already has the corresponding private key:
138  SecIdentityRef identity = NULL;
139  OSStatus err = SecIdentityCreateWithCertificate(NULL, cert->os_cert_handle(),
140                                                  &identity);
141  if (err == errSecItemNotFound)
142    return ERR_NO_PRIVATE_KEY_FOR_CERT;
143
144  if (err != noErr || !identity) {
145    // TODO(snej): Map the error code more intelligently.
146    return ERR_CERT_INVALID;
147  }
148
149  CFRelease(identity);
150  return OK;
151}
152
153int CertDatabase::AddUserCert(X509Certificate* cert) {
154  OSStatus err;
155  {
156    base::AutoLock locked(crypto::GetMacSecurityServicesLock());
157    err = SecCertificateAddToKeychain(cert->os_cert_handle(), NULL);
158  }
159  switch (err) {
160    case noErr:
161      CertDatabase::NotifyObserversOfCertAdded(cert);
162      // Fall through.
163    case errSecDuplicateItem:
164      return OK;
165    default:
166      OSSTATUS_LOG(ERROR, err) << "CertDatabase failed to add cert to keychain";
167      // TODO(snej): Map the error code more intelligently.
168      return ERR_ADD_USER_CERT_FAILED;
169  }
170}
171
172}  // namespace net
173