blob: 0657ad49380b1fdb1007eb982156375a39e168a2 [file] [log] [blame]
// Copyright (c) 2012 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 "chrome/browser/ui/views/ssl_client_certificate_selector_mac.h"
#import <Cocoa/Cocoa.h>
#import <SecurityInterface/SFChooseIdentityPanel.h>
#include <objc/runtime.h>
#include <memory>
#include <utility>
#include "base/logging.h"
#include "base/mac/foundation_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/mac/scoped_nsobject.h"
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ssl/ssl_client_auth_observer.h"
#include "chrome/browser/ssl/ssl_client_certificate_selector.h"
#include "chrome/browser/ui/views/certificate_selector.h"
#include "chrome/grit/generated_resources.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/strings/grit/components_strings.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/client_certificate_delegate.h"
#include "content/public/browser/web_contents.h"
#include "net/cert/x509_certificate.h"
#include "net/cert/x509_util_mac.h"
#include "net/ssl/ssl_cert_request_info.h"
#include "net/ssl/ssl_platform_key_mac.h"
#include "ui/base/buildflags.h"
#include "ui/base/l10n/l10n_util_mac.h"
@interface SFChooseIdentityPanel (SystemPrivate)
// A system-private interface that dismisses a panel whose sheet was started by
// -beginSheetForWindow:modalDelegate:didEndSelector:contextInfo:identities:message:
// as though the user clicked the button identified by returnCode. Verified
// present in 10.5 through 10.12.
- (void)_dismissWithCode:(NSInteger)code;
@end
namespace {
class SSLClientCertificateSelectorDelegate;
// These Clear[Window]TableViewDataSources... functions help work around a bug
// in macOS where SFChooseIdentityPanel leaks a window and some views, including
// an NSTableView. Future events may make cause the table view to query its
// dataSource, which will have been deallocated.
//
// Note that this was originally thought to be 10.12+ but this reliably crashes
// on 10.11 (says avi@).
//
// Linking against the 10.12 SDK does not "fix" this issue, since
// NSTableView.dataSource is a "weak" reference, which in non-ARC land still
// translates to "raw pointer".
//
// See https://crbug.com/653093, https://crbug.com/750242 and rdar://29409207
// for more information.
void ClearTableViewDataSources(NSView* view) {
if (auto table_view = base::mac::ObjCCast<NSTableView>(view)) {
table_view.dataSource = nil;
} else {
for (NSView* subview in view.subviews) {
ClearTableViewDataSources(subview);
}
}
}
void ClearWindowTableViewDataSources(NSWindow* window) {
ClearTableViewDataSources(window.contentView);
}
} // namespace
// This is the main class that runs the certificate selector panel. It's in
// Objective-C mainly because the only way to get a result out of that panel is
// a callback of a target/selector pair.
@interface SSLClientCertificateSelectorMac : NSObject
- (instancetype)
initWithClientCerts:(net::ClientCertIdentityList)clientCerts
delegate:(base::WeakPtr<SSLClientCertificateSelectorDelegate>)
delegate;
- (void)showSheetForWindow:(NSWindow*)window;
- (void)closeSelectorSheetWithCode:(NSModalResponse)response;
@end
// A testing helper object to run a OnceClosure when deallocated. Attach it as
// an associated object to test for deallocation of an object without
// subclassing.
@interface DeallocClosureCaller : NSObject
- (instancetype)initWithDeallocClosure:(base::OnceClosure)deallocClosure;
@end
@implementation DeallocClosureCaller {
base::OnceClosure _deallocClosure;
}
- (instancetype)initWithDeallocClosure:(base::OnceClosure)deallocClosure {
if ((self = [super init])) {
_deallocClosure = std::move(deallocClosure);
}
return self;
}
- (void)dealloc {
std::move(_deallocClosure).Run();
[super dealloc];
}
@end
namespace {
// A fully transparent, borderless web-modal dialog used to display the
// OS-provided client certificate selector.
class SSLClientCertificateSelectorDelegate
: public views::WidgetDelegateView,
public chrome::OkAndCancelableForTesting,
public SSLClientAuthObserver {
public:
SSLClientCertificateSelectorDelegate(
content::WebContents* contents,
net::SSLCertRequestInfo* cert_request_info,
net::ClientCertIdentityList client_certs,
std::unique_ptr<content::ClientCertificateDelegate> delegate)
: SSLClientAuthObserver(contents->GetBrowserContext(),
cert_request_info,
std::move(delegate)) {
StartObserving();
// Note this may call ShowSheet() synchronously or in a separate event loop
// iteration.
constrained_window::ShowWebModalDialogWithOverlayViews(
this, contents,
base::BindOnce(&SSLClientCertificateSelectorDelegate::ShowSheet,
weak_factory_.GetWeakPtr(), std::move(client_certs)));
}
~SSLClientCertificateSelectorDelegate() override {
// Note that the SFChooseIdentityPanel takes a reference to its delegate
// (|certificate_selector_|) in its -beginSheetForWindow:... method. Break
// the retain cycle by explicitly canceling the dialog.
[certificate_selector_ closeSelectorSheetWithCode:NSModalResponseAbort];
// This matches the StartObserving() call if ShowSheet() was never called
// and the request was canceled.
StopObserving();
}
// WidgetDelegate:
ui::ModalType GetModalType() const override { return ui::MODAL_TYPE_CHILD; }
// OkAndCancelableForTesting:
void ClickOkButton() override {
// Tests should not call ClickOkButton() on a sheet that has not yet been
// shown.
DCHECK(certificate_selector_);
[certificate_selector_ closeSelectorSheetWithCode:NSModalResponseOK];
}
void ClickCancelButton() override {
// Tests should not call ClickCancelButton() on a sheet that has not yet
// been shown.
DCHECK(certificate_selector_);
[certificate_selector_ closeSelectorSheetWithCode:NSModalResponseCancel];
}
// SSLClientAuthObserver implementation:
void OnCertSelectedByNotification() override {
[certificate_selector_ closeSelectorSheetWithCode:NSModalResponseStop];
}
void SetDeallocClosureForTesting(base::OnceClosure dealloc_closure) {
dealloc_closure_ = std::move(dealloc_closure);
SetDeallocClosureIfReady();
}
base::OnceClosure GetCancellationCallback() {
return base::BindOnce(&SSLClientCertificateSelectorDelegate::CloseSelector,
weak_factory_.GetWeakPtr());
}
void CloseWidgetWithReason(views::Widget::ClosedReason reason) {
GetWidget()->CloseWithReason(reason);
}
private:
void CloseSelector() {
cancelled_ = true;
if (certificate_selector_) {
[certificate_selector_ closeSelectorSheetWithCode:NSModalResponseStop];
} else {
CloseWidgetWithReason(views::Widget::ClosedReason::kUnspecified);
}
}
void ShowSheet(net::ClientCertIdentityList client_certs,
views::Widget* overlay_window) {
DCHECK(!certificate_selector_);
if (cancelled_) {
// If CloseSelector() is called before the sheet is shown, it should
// synchronously destroy the dialog, which means ShowSheet() cannot later
// be called, but check for this in case the dialog logic changes.
NOTREACHED();
return;
}
certificate_selector_.reset([[SSLClientCertificateSelectorMac alloc]
initWithClientCerts:std::move(client_certs)
delegate:weak_factory_.GetWeakPtr()]);
SetDeallocClosureIfReady();
[certificate_selector_ showSheetForWindow:overlay_window->GetNativeWindow()
.GetNativeNSWindow()];
}
// Attaches |dealloc_closure_| to |certificate_selector_| if both are created.
// |certificate_selector_| is not created until ShowSheet(), so this method
// allows SetDeallocClosureForTesting() to take effect when ShowSheet() is
// deferred.
void SetDeallocClosureIfReady() {
if (!certificate_selector_ || dealloc_closure_.is_null())
return;
base::scoped_nsobject<DeallocClosureCaller> caller(
[[DeallocClosureCaller alloc]
initWithDeallocClosure:std::move(dealloc_closure_)]);
// The use of the caller as the key is deliberate; nothing needs to ever
// look it up, so it's a convenient unique value.
objc_setAssociatedObject(certificate_selector_.get(), caller, caller,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
base::scoped_nsobject<SSLClientCertificateSelectorMac> certificate_selector_;
bool cancelled_ = false;
base::OnceClosure dealloc_closure_;
base::WeakPtrFactory<SSLClientCertificateSelectorDelegate> weak_factory_{
this};
DISALLOW_COPY_AND_ASSIGN(SSLClientCertificateSelectorDelegate);
};
} // namespace
@implementation SSLClientCertificateSelectorMac {
// The list of SecIdentityRefs offered to the user.
base::scoped_nsobject<NSMutableArray> _secIdentities;
// The corresponding list of ClientCertIdentities.
net::ClientCertIdentityList _certIdentities;
// A C++ object to report the client certificate selection to.
base::WeakPtr<SSLClientCertificateSelectorDelegate> _delegate;
base::scoped_nsobject<SFChooseIdentityPanel> _panel;
}
- (instancetype)
initWithClientCerts:(net::ClientCertIdentityList)clientCerts
delegate:(base::WeakPtr<SSLClientCertificateSelectorDelegate>)
delegate {
if ((self = [super init])) {
_delegate = delegate;
_certIdentities = std::move(clientCerts);
_secIdentities.reset([[NSMutableArray alloc] init]);
for (const auto& cert : _certIdentities) {
DCHECK(cert->sec_identity_ref());
[_secIdentities addObject:(id)cert->sec_identity_ref()];
}
}
return self;
}
// The selector sheet ended. There are four possibilities for the return code.
//
// These two return codes are actually generated by the SFChooseIdentityPanel,
// although for testing purposes the OkAndCancelableForTesting implementation
// will also generate them to simulate the user clicking buttons.
//
// - NSModalResponseOK/Cancel: The user clicked the "OK" or "Cancel" button; the
// SSL auth system needs to be told of this choice.
//
// These two return codes are generated by the
// SSLClientCertificateSelectorDelegate to force the SFChooseIdentityPanel to be
// closed for various reasons.
//
// - NSModalResponseAbort: The user closed the owning tab; the SSL auth system
// needs to be told of this cancellation.
// - NSModalResponseStop: The SSL auth system already has an answer; just tear
// down the dialog.
//
// Note that there is a disagreement between the docs and the SDK header file as
// to the type of the return code. It has empirically been determined to be an
// int, not an NSInteger. rdar://45344010
- (void)sheetDidEnd:(NSWindow*)sheet
returnCode:(int)returnCode
context:(void*)context {
views::Widget::ClosedReason closedReason =
views::Widget::ClosedReason::kUnspecified;
if (_delegate) {
if (returnCode == NSModalResponseAbort) {
_delegate->CancelCertificateSelection();
} else if (returnCode == NSModalResponseOK ||
returnCode == NSModalResponseCancel) {
net::ClientCertIdentity* cert = nullptr;
if (returnCode == NSModalResponseOK) {
NSUInteger index = [_secIdentities indexOfObject:(id)[_panel identity]];
if (index != NSNotFound)
cert = _certIdentities[index].get();
closedReason = views::Widget::ClosedReason::kAcceptButtonClicked;
} else {
closedReason = views::Widget::ClosedReason::kCancelButtonClicked;
}
if (cert) {
_delegate->CertificateSelected(
cert->certificate(),
CreateSSLPrivateKeyForSecIdentity(cert->certificate(),
cert->sec_identity_ref())
.get());
} else {
_delegate->CertificateSelected(nullptr, nullptr);
}
} else {
DCHECK_EQ(NSModalResponseStop, returnCode);
_delegate->StopObserving();
}
} else {
// This should be impossible, assuming _dismissWithCode: synchronously calls
// this method. (SSLClientCertificateSelectorDelegate calls
// closeSelectorSheetWithCode: on destruction.)
NOTREACHED();
}
// See comment at definition; this works around a bug.
ClearWindowTableViewDataSources(sheet);
// Do not release SFChooseIdentityPanel here. Its -_okClicked: method, after
// calling out to this method, keeps accessing its ivars, and if panel_ is the
// last reference keeping it alive, it will crash.
_panel.autorelease();
if (_delegate) {
// This asynchronously releases |self|.
_delegate->CloseWidgetWithReason(closedReason);
}
}
- (void)showSheetForWindow:(NSWindow*)window {
// Get the message to display:
NSString* message = l10n_util::GetNSStringF(
IDS_CLIENT_CERT_DIALOG_TEXT,
base::ASCIIToUTF16(
_delegate->cert_request_info()->host_and_port.ToString()));
// Create and set up a system choose-identity panel.
_panel.reset([[SFChooseIdentityPanel alloc] init]);
[_panel setInformativeText:message];
[_panel setDefaultButtonTitle:l10n_util::GetNSString(IDS_OK)];
[_panel setAlternateButtonTitle:l10n_util::GetNSString(IDS_CANCEL)];
base::ScopedCFTypeRef<SecPolicyRef> sslPolicy;
if (net::x509_util::CreateSSLClientPolicy(sslPolicy.InitializeInto()) ==
noErr) {
[_panel setPolicies:(id)sslPolicy.get()];
}
NSString* title = l10n_util::GetNSString(IDS_CLIENT_CERT_DIALOG_TITLE);
[_panel beginSheetForWindow:window
modalDelegate:self
didEndSelector:@selector(sheetDidEnd:returnCode:context:)
contextInfo:nil
identities:_secIdentities
message:title];
}
- (void)closeSelectorSheetWithCode:(NSModalResponse)response {
// Closing the sheet using -[NSApp endSheet:] doesn't work, so use the private
// method. If the sheet is already closed then this is a message send to nil
// and thus a no-op.
[_panel _dismissWithCode:response];
}
@end
namespace chrome {
base::OnceClosure ShowSSLClientCertificateSelector(
content::WebContents* contents,
net::SSLCertRequestInfo* cert_request_info,
net::ClientCertIdentityList client_certs,
std::unique_ptr<content::ClientCertificateDelegate> delegate) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Not all WebContentses can show modal dialogs.
//
// TODO(davidben): Move this hook to the WebContentsDelegate and only try to
// show a dialog in Browser's implementation. https://crbug.com/456255
if (!CertificateSelector::CanShow(contents))
return base::OnceClosure();
auto* selector_delegate = new SSLClientCertificateSelectorDelegate(
contents, cert_request_info, std::move(client_certs),
std::move(delegate));
return selector_delegate->GetCancellationCallback();
}
OkAndCancelableForTesting* ShowSSLClientCertificateSelectorMacForTesting(
content::WebContents* contents,
net::SSLCertRequestInfo* cert_request_info,
net::ClientCertIdentityList client_certs,
std::unique_ptr<content::ClientCertificateDelegate> delegate,
base::OnceClosure dealloc_closure) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
auto* dialog_delegate = new SSLClientCertificateSelectorDelegate(
contents, cert_request_info, std::move(client_certs),
std::move(delegate));
dialog_delegate->SetDeallocClosureForTesting(std::move(dealloc_closure));
return dialog_delegate;
}
} // namespace chrome