diff --git a/atom/browser/api/atom_api_dialog.cc b/atom/browser/api/atom_api_dialog.cc index cc849e026ea1..cb9f17f704fb 100644 --- a/atom/browser/api/atom_api_dialog.cc +++ b/atom/browser/api/atom_api_dialog.cc @@ -8,11 +8,13 @@ #include "atom/browser/api/atom_api_window.h" #include "atom/browser/native_window.h" +#include "atom/browser/ui/certificate_trust.h" #include "atom/browser/ui/file_dialog.h" #include "atom/browser/ui/message_box.h" #include "atom/common/native_mate_converters/callback.h" #include "atom/common/native_mate_converters/file_path_converter.h" #include "atom/common/native_mate_converters/image_converter.h" +#include "atom/common/native_mate_converters/net_converter.h" #include "native_mate/dictionary.h" #include "atom/common/node_includes.h" @@ -127,6 +129,10 @@ void Initialize(v8::Local exports, v8::Local unused, dict.SetMethod("showErrorBox", &atom::ShowErrorBox); dict.SetMethod("showOpenDialog", &ShowOpenDialog); dict.SetMethod("showSaveDialog", &ShowSaveDialog); +#if defined(OS_MACOSX) + dict.SetMethod("showCertificateTrustDialog", + &certificate_trust::ShowCertificateTrust); +#endif } } // namespace diff --git a/atom/browser/ui/certificate_trust.h b/atom/browser/ui/certificate_trust.h new file mode 100644 index 000000000000..7cbf31ea41fb --- /dev/null +++ b/atom/browser/ui/certificate_trust.h @@ -0,0 +1,29 @@ +// Copyright (c) 2017 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ATOM_BROWSER_UI_CERTIFICATE_TRUST_H_ +#define ATOM_BROWSER_UI_CERTIFICATE_TRUST_H_ + +#include + +#include "base/callback_forward.h" +#include "base/memory/ref_counted.h" +#include "net/cert/x509_certificate.h" + +namespace atom { +class NativeWindow; +} // namespace atom + +namespace certificate_trust { + +typedef base::Callback ShowTrustCallback; + +void ShowCertificateTrust(atom::NativeWindow* parent_window, + const scoped_refptr& cert, + const std::string& message, + const ShowTrustCallback& callback); + +} // namespace certificate_trust + +#endif // ATOM_BROWSER_UI_CERTIFICATE_TRUST_H_ diff --git a/atom/browser/ui/certificate_trust_mac.mm b/atom/browser/ui/certificate_trust_mac.mm new file mode 100644 index 000000000000..e0888dd3ea24 --- /dev/null +++ b/atom/browser/ui/certificate_trust_mac.mm @@ -0,0 +1,112 @@ +// Copyright (c) 2017 GitHub, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "atom/browser/ui/certificate_trust.h" + +#import +#import + +#include "atom/browser/native_window.h" +#include "base/strings/sys_string_conversions.h" +#include "net/cert/cert_database.h" + +@interface TrustDelegate : NSObject { + @private + certificate_trust::ShowTrustCallback callback_; + SFCertificateTrustPanel* panel_; + scoped_refptr cert_; + SecTrustRef trust_; + CFArrayRef cert_chain_; + SecPolicyRef sec_policy_; +} + +- (id)initWithCallback:(const certificate_trust::ShowTrustCallback&)callback + panel:(SFCertificateTrustPanel*)panel + cert:(const scoped_refptr&)cert + trust:(SecTrustRef)trust + certChain:(CFArrayRef)certChain + secPolicy:(SecPolicyRef)secPolicy; + +- (void)panelDidEnd:(NSWindow*)sheet + returnCode:(int)returnCode + contextInfo:(void*)contextInfo; + +@end + +@implementation TrustDelegate + +- (void)dealloc { + [panel_ release]; + CFRelease(trust_); + CFRelease(cert_chain_); + CFRelease(sec_policy_); + + [super dealloc]; +} + +- (id)initWithCallback:(const certificate_trust::ShowTrustCallback&)callback + panel:(SFCertificateTrustPanel*)panel + cert:(const scoped_refptr&)cert + trust:(SecTrustRef)trust + certChain:(CFArrayRef)certChain + secPolicy:(SecPolicyRef)secPolicy { + if ((self = [super init])) { + callback_ = callback; + panel_ = panel; + cert_ = cert; + trust_ = trust; + cert_chain_ = certChain; + sec_policy_ = secPolicy; + } + + return self; +} + +- (void)panelDidEnd:(NSWindow*)sheet + returnCode:(int)returnCode + contextInfo:(void*)contextInfo { + auto cert_db = net::CertDatabase::GetInstance(); + // This forces Chromium to reload the certificate since it might be trusted + // now. + cert_db->NotifyObserversCertDBChanged(cert_.get()); + + callback_.Run(); + + [self autorelease]; +} + +@end + +namespace certificate_trust { + +void ShowCertificateTrust(atom::NativeWindow* parent_window, + const scoped_refptr& cert, + const std::string& message, + const ShowTrustCallback& callback) { + auto sec_policy = SecPolicyCreateBasicX509(); + auto cert_chain = cert->CreateOSCertChainForCert(); + SecTrustRef trust = nullptr; + SecTrustCreateWithCertificates(cert_chain, sec_policy, &trust); + + NSWindow* window = parent_window ? + parent_window->GetNativeWindow() : + nil; + auto msg = base::SysUTF8ToNSString(message); + + auto panel = [[SFCertificateTrustPanel alloc] init]; + auto delegate = [[TrustDelegate alloc] initWithCallback:callback + panel:panel + cert:cert + trust:trust + certChain:cert_chain + secPolicy:sec_policy]; + [panel beginSheetForWindow:window + modalDelegate:delegate + didEndSelector:@selector(panelDidEnd:returnCode:contextInfo:) + contextInfo:nil + trust:trust + message:msg]; +} + +} // namespace certificate_trust diff --git a/atom/common/native_mate_converters/net_converter.cc b/atom/common/native_mate_converters/net_converter.cc index 94fff2ff6027..b78bc5b8e118 100644 --- a/atom/common/native_mate_converters/net_converter.cc +++ b/atom/common/native_mate_converters/net_converter.cc @@ -26,6 +26,27 @@ namespace mate { +namespace { + +bool CertFromData(const std::string& data, + scoped_refptr* out) { + auto cert_list = net::X509Certificate::CreateCertificateListFromBytes( + data.c_str(), data.length(), + net::X509Certificate::FORMAT_SINGLE_CERTIFICATE); + if (cert_list.empty()) + return false; + + auto leaf_cert = cert_list.front(); + if (!leaf_cert) + return false; + + *out = leaf_cert; + + return true; +} + +} // namespace + // static v8::Local Converter::ToV8( v8::Isolate* isolate, const net::AuthChallengeInfo* val) { @@ -73,6 +94,37 @@ v8::Local Converter>::ToV8( return dict.GetHandle(); } +bool Converter>::FromV8( + v8::Isolate* isolate, v8::Local val, + scoped_refptr* out) { + mate::Dictionary dict; + if (!ConvertFromV8(isolate, val, &dict)) + return false; + + std::string data; + dict.Get("data", &data); + scoped_refptr leaf_cert; + if (!CertFromData(data, &leaf_cert)) + return false; + + scoped_refptr parent; + if (dict.Get("issuerCert", &parent)) { + auto parents = std::vector( + parent->GetIntermediateCertificates()); + parents.insert(parents.begin(), parent->os_cert_handle()); + auto cert = net::X509Certificate::CreateFromHandle( + leaf_cert->os_cert_handle(), parents); + if (!cert) + return false; + + *out = cert; + } else { + *out = leaf_cert; + } + + return true; +} + // static v8::Local Converter::ToV8( v8::Isolate* isolate, const net::CertPrincipal& val) { diff --git a/atom/common/native_mate_converters/net_converter.h b/atom/common/native_mate_converters/net_converter.h index 33117ca974f1..9e3128fdb546 100644 --- a/atom/common/native_mate_converters/net_converter.h +++ b/atom/common/native_mate_converters/net_converter.h @@ -33,6 +33,10 @@ template<> struct Converter> { static v8::Local ToV8(v8::Isolate* isolate, const scoped_refptr& val); + + static bool FromV8(v8::Isolate* isolate, + v8::Local val, + scoped_refptr* out); }; template<> diff --git a/docs/api/dialog.md b/docs/api/dialog.md index 8885c315144b..51760c2d59db 100644 --- a/docs/api/dialog.md +++ b/docs/api/dialog.md @@ -175,6 +175,20 @@ it is usually used to report errors in early stage of startup. If called before the app `ready`event on Linux, the message will be emitted to stderr, and no GUI dialog will appear. +### `dialog.showCertificateTrustDialog([browserWindow, ]options, callback)` _macOS_ + +* `browserWindow` BrowserWindow (optional) +* `options` Object + * `certificate` [Certificate](structures/certificate.md) - The certificate to trust/import. + * `message` String - The message to display to the user. +* `callback` Function + +Displays a modal dialog that shows a message and certificate information, and +gives the user the option of trusting/importing the certificate. + +The `browserWindow` argument allows the dialog to attach itself to a parent +window, making it modal. + ## Sheets On macOS, dialogs are presented as sheets attached to a window if you provide diff --git a/electron.gyp b/electron.gyp index 3dec2ee8a072..ee8120097495 100644 --- a/electron.gyp +++ b/electron.gyp @@ -549,6 +549,8 @@ '$(SDKROOT)/System/Library/Frameworks/Carbon.framework', '$(SDKROOT)/System/Library/Frameworks/QuartzCore.framework', '$(SDKROOT)/System/Library/Frameworks/Quartz.framework', + '$(SDKROOT)/System/Library/Frameworks/Security.framework', + '$(SDKROOT)/System/Library/Frameworks/SecurityInterface.framework', ], }, 'mac_bundle': 1, diff --git a/filenames.gypi b/filenames.gypi index 935d7033153a..192d953287d7 100644 --- a/filenames.gypi +++ b/filenames.gypi @@ -279,6 +279,8 @@ 'atom/browser/ui/accelerator_util_views.cc', 'atom/browser/ui/atom_menu_model.cc', 'atom/browser/ui/atom_menu_model.h', + 'atom/browser/ui/certificate_trust.h', + 'atom/browser/ui/certificate_trust_mac.mm', 'atom/browser/ui/cocoa/atom_menu_controller.h', 'atom/browser/ui/cocoa/atom_menu_controller.mm', 'atom/browser/ui/cocoa/atom_touch_bar.h', diff --git a/lib/browser/api/dialog.js b/lib/browser/api/dialog.js index 834a6a6d7d8d..964461804769 100644 --- a/lib/browser/api/dialog.js +++ b/lib/browser/api/dialog.js @@ -280,6 +280,27 @@ module.exports = { showErrorBox: function (...args) { return binding.showErrorBox(...args) + }, + + showCertificateTrustDialog: function (...args) { + let [window, options, callback] = parseArgs(...args) + + if (options == null || typeof options !== 'object') { + throw new TypeError('options must be an object') + } + + let {certificate, message} = options + if (certificate == null || typeof certificate !== 'object') { + throw new TypeError('certificate must be an object') + } + + if (message == null) { + message = '' + } else if (typeof message !== 'string') { + throw new TypeError('message must be a string') + } + + return binding.showCertificateTrustDialog(window, certificate, message, callback) } } diff --git a/spec/api-dialog-spec.js b/spec/api-dialog-spec.js index 6b245c2b65f7..601e28ca53ca 100644 --- a/spec/api-dialog-spec.js +++ b/spec/api-dialog-spec.js @@ -93,4 +93,20 @@ describe('dialog module', () => { }, /Error processing argument at index 1/) }) }) + + describe('showCertificateTrustDialog', () => { + it('throws errors when the options are invalid', () => { + assert.throws(() => { + dialog.showCertificateTrustDialog() + }, /options must be an object/) + + assert.throws(() => { + dialog.showCertificateTrustDialog({}) + }, /certificate must be an object/) + + assert.throws(() => { + dialog.showCertificateTrustDialog({certificate: {}, message: false}) + }, /message must be a string/) + }) + }) })