From 293de19c2bdf65e2489fec72144dd7beaef43203 Mon Sep 17 00:00:00 2001 From: Pritesh Nandgaonkar Date: Wed, 12 Aug 2020 04:42:26 -0700 Subject: [PATCH] Add toggle in the settings for cert exchange medium Summary: This diff adds a toggle setting in wilde which will enable certificate exchange through www. Right now it just sends the information about which medium to be used for cert exchange to Flipper JS and its client side. But its implementation is not done yet. ### Flow for Wilde Whenever user changes the setting(or when user logs out) we set the state of exchange medium and accordingly set/reset authtoken. Note at no given point we remove already existing certificates. ### Context for OSS With this diff we introduce another way to do certificate exchange. Before this diff, we did certificate exchange by accessing the file system of app. But it turns out it's not possible to do that in applications signed by enterprise certs. Thus with this diff one can write their FlipperKitCertificateProvider and fetch the certificate from WWW. Reviewed By: jknoxville Differential Revision: D22896320 fbshipit-source-id: 55aef7028a62e71ba9c02f9f79acaab41d09c0c6 --- FlipperKit.podspec | 2 +- desktop/app/src/server.tsx | 31 +++++++++++++-- desktop/app/src/utils/CertificateProvider.tsx | 9 +++++ docs/extending/establishing-a-connection.mdx | 17 +++++++-- iOS/FlipperKit/FlipperClient.h | 11 ++++++ iOS/FlipperKit/FlipperClient.mm | 21 ++++++++-- .../FlipperKitCertificateProvider.h | 33 ++++++++++++++++ xplat/Flipper/ConnectionContextStore.cpp | 2 - .../FlipperCertificateExchangeMedium.h | 8 ++++ xplat/Flipper/FlipperCertificateProvider.h | 38 +++++++++++++++++++ xplat/Flipper/FlipperClient.cpp | 11 ++++++ xplat/Flipper/FlipperClient.h | 5 +++ xplat/Flipper/FlipperConnectionManager.h | 13 +++++++ .../Flipper/FlipperConnectionManagerImpl.cpp | 18 ++++++++- xplat/Flipper/FlipperConnectionManagerImpl.h | 4 ++ .../FlipperConnectionManagerMock.h | 8 ++++ 16 files changed, 215 insertions(+), 16 deletions(-) create mode 100644 iOS/FlipperKit/FlipperKitCertificateProvider.h create mode 100644 xplat/Flipper/FlipperCertificateExchangeMedium.h create mode 100644 xplat/Flipper/FlipperCertificateProvider.h diff --git a/FlipperKit.podspec b/FlipperKit.podspec index b889e25a4..1e1b587a6 100644 --- a/FlipperKit.podspec +++ b/FlipperKit.podspec @@ -79,7 +79,7 @@ Pod::Spec.new do |spec| ss.dependency 'Flipper', '~>'+flipperkit_version ss.compiler_flags = folly_compiler_flags ss.source_files = 'iOS/FlipperKit/*.{h,m,mm}', 'iOS/FlipperKit/CppBridge/*.{h,mm}' - ss.public_header_files = 'iOS/FlipperKit/**/{FlipperDiagnosticsViewController,FlipperStateUpdateListener,FlipperClient,FlipperPlugin,FlipperConnection,FlipperResponder,SKMacros}.h' + ss.public_header_files = 'iOS/FlipperKit/**/{FlipperDiagnosticsViewController,FlipperStateUpdateListener,FlipperClient,FlipperPlugin,FlipperConnection,FlipperResponder,SKMacros,FlipperKitCertificateProvider}.h' header_search_paths = "\"$(PODS_ROOT)/FlipperKit/iOS/FlipperKit/\" \"$(PODS_ROOT)/Headers/Private/FlipperKit/\" \"$(PODS_ROOT)/boost-for-react-native\" \"$(PODS_ROOT)/boost-for-react-native\"" ss.pod_target_xcconfig = { "USE_HEADERMAP" => "NO", "ONLY_ACTIVE_ARCH": "YES", diff --git a/desktop/app/src/server.tsx b/desktop/app/src/server.tsx index 348b66f71..0afb889b9 100644 --- a/desktop/app/src/server.tsx +++ b/desktop/app/src/server.tsx @@ -7,7 +7,10 @@ * @format */ -import {SecureServerConfig} from './utils/CertificateProvider'; +import { + SecureServerConfig, + CertificateExchangeMedium, +} from './utils/CertificateProvider'; import {Logger} from './fb-interfaces/Logger'; import {ClientQuery} from './Client'; import {Store} from './reducers/index'; @@ -45,6 +48,18 @@ type ClientCsrQuery = { csr_path?: string | undefined; }; +function transformCertificateExchangeMediumToType( + medium: number | undefined, +): CertificateExchangeMedium { + if (medium === 1) { + return 'FS_ACCESS'; + } else if (medium === 2) { + return 'WWW'; + } else { + return 'FS_ACCESS'; + } +} + declare interface Server { on(event: 'new-client', callback: (client: Client) => void): this; on(event: 'error', callback: (err: Error) => void): this; @@ -347,11 +362,12 @@ class Server extends EventEmitter { method: 'signCertificate'; csr: string; destination: string; + medium: number | undefined; // OSS's older Client SDK might not send medium information. This is not an issue for internal FB users, as Flipper release is insync with client SDK through launcher. } = rawData; if (json.method === 'signCertificate') { console.debug('CSR received from device', 'server'); - const {csr, destination} = json; + const {csr, destination, medium} = json; return new Single((subscriber) => { subscriber.onSubscribe(undefined); reportPlatformFailures( @@ -359,6 +375,7 @@ class Server extends EventEmitter { csr, clientData.os, destination, + transformCertificateExchangeMediumToType(medium), ), 'processCertificateSigningRequest', ) @@ -396,6 +413,7 @@ class Server extends EventEmitter { method: 'signCertificate'; csr: string; destination: string; + medium: number | undefined; } | undefined; try { @@ -407,9 +425,14 @@ class Server extends EventEmitter { if (json && json.method === 'signCertificate') { console.debug('CSR received from device', 'server'); - const {csr, destination} = json; + const {csr, destination, medium} = json; this.certificateProvider - .processCertificateSigningRequest(csr, clientData.os, destination) + .processCertificateSigningRequest( + csr, + clientData.os, + destination, + transformCertificateExchangeMediumToType(medium), + ) .catch((e) => { console.error(e); }); diff --git a/desktop/app/src/utils/CertificateProvider.tsx b/desktop/app/src/utils/CertificateProvider.tsx index 7f594503d..dfa0bb31c 100644 --- a/desktop/app/src/utils/CertificateProvider.tsx +++ b/desktop/app/src/utils/CertificateProvider.tsx @@ -25,6 +25,8 @@ import os from 'os'; import {Client as ADBClient} from 'adbkit'; import {Store} from '../reducers/index'; +export type CertificateExchangeMedium = 'FS_ACCESS' | 'WWW'; + const tmpFile = promisify(tmp.file) as ( options?: FileOptions, ) => Promise; @@ -96,7 +98,14 @@ export default class CertificateProvider { unsanitizedCsr: string, os: string, appDirectory: string, + medium: CertificateExchangeMedium, ): Promise<{deviceId: string}> { + // TODO: Add implementations for each of these conditions + if (medium === 'FS_ACCESS') { + // Use IDB for cert exchange + } else if (medium === 'WWW') { + // Use WWWW + } const csr = this.santitizeString(unsanitizedCsr); if (csr === '') { return Promise.reject(new Error(`Received empty CSR from ${os} device`)); diff --git a/docs/extending/establishing-a-connection.mdx b/docs/extending/establishing-a-connection.mdx index 10599912e..064e7d25c 100644 --- a/docs/extending/establishing-a-connection.mdx +++ b/docs/extending/establishing-a-connection.mdx @@ -25,9 +25,12 @@ This is achieved through the following steps: * Desktop app starts an insecure server on port 8089. * Mobile app connects to localhost:8089 and sends a Certificate Signing Request to the desktop app. * Desktop app uses it's private key (this is generated once and stored in ~/.flipper) to sign a client certificate for the mobile app. -* The desktop uses ADB (for Android), or the mounted file system (for iOS simulators) to write the following files to the mobile app's private data partition +* Along with the Certificate Signing Request, mobile app also lets the desktop app know which certificate exchange medium to use. +* If the chosen Certificate Exchange Medium is FS_ACCESS, the desktop uses ADB (for Android), or the mounted file system (for iOS simulators) to write the following files to the mobile app's private data partition * Server certificate that the mobile app can now trust. * Client certificate for the mobile app to use going forward. +* If the chosen Certificate Exchange Medium is WWW, the desktop app will use your implementation of Certificate Uploader to upload the certificates. + * Once uploaded the desktop app will reply back with the device id, which can be used by Certificate Provider on the client side to fetch those certificates. * Now the mobile app knows which server certificate it can trust, and can connect to the secure server. This allows the mobile app to trust a certificate if and only if, it is stored inside its internal data partition. Typically it's only possible to write there with physical access to the device (i.e. through ADB or a mounted simulator). @@ -42,6 +45,7 @@ localhost:8089/sonar?os={OS} &device={DEVICE} &app={APP} &sdk_version={SDK_VERSION} + &medium={CERTIFICATE_EXCHANGE_MEDIUM} ``` On that connection, send the following payload: @@ -49,15 +53,20 @@ On that connection, send the following payload: Request = { "method": "signCertificate", "csr": string, - "destination": string + "destination": string, + "medium": int } ``` Where `csr` is a Certificate Signing Request the client has generated, and `destination` identifies a location accessible to both the client and Flipper desktop, where the certificate should be placed. -The Subject Common Name (CN=...) must be included in the CSR, and your `CertificateProvider` implementation in Flipper may use this in combination with the `destination` to determine where to put the certificate. +The Subject Common Name (CN=...) must be included in the CSR, and your `CertificateProvider` implementation in Flipper may use this in combination with the `destination` to determine where to put the certificate. This will ask Flipper desktop to generate a client certificate, using the CSR provided, and put it into the specified `destination`. Depending on the client, `destination` can have a different meaning. A basic example would be a file path, that both the desktop and the client have access to. With this Flipper desktop could write the certificate to that path. A more involved example is that of the Android Client, where destination specifies a relative path inside an app container. And the Subject Common Name determines which app container. Together these two pieces of information form an absolute file path inside an android device. -For Flipper desktop to work with a given Client type, it needs to be modified to know how to correctly interpret the `destination` argument, and deploy certificates to it. You can see the current implementations in [CertificateProvider.tsx](https://github.com/facebook/flipper/blob/master/desktop/app/src/utils/CertificateProvider.tsx). +For Flipper desktop to work with a given Client type, it needs to be modified to know how to correctly interpret the `destination` argument, and deploy certificates to it. + +`destination` field may not be relevant if your `medium` value is more than 1. `medium=1`(default) means Flipper should do certificate exchange by directly putting certificates at `destination` in the sandbox of the app. `medium=2` means Flipper will use Certificate Uploader and Provider to upload certificates and download it on the client side respectively. + +You can see the current implementations in [CertificateProvider.tsx](https://github.com/facebook/flipper/blob/master/desktop/app/src/utils/CertificateProvider.tsx). diff --git a/iOS/FlipperKit/FlipperClient.h b/iOS/FlipperKit/FlipperClient.h index 65f52877c..3dd3f7d3d 100644 --- a/iOS/FlipperKit/FlipperClient.h +++ b/iOS/FlipperKit/FlipperClient.h @@ -8,6 +8,7 @@ #ifdef FB_SONARKIT_ENABLED #import +#import "FlipperKitCertificateProvider.h" #import "FlipperPlugin.h" #import "FlipperStateUpdateListener.h" @@ -64,6 +65,16 @@ Subscribe a ViewController to state update change notifications */ - (void)subscribeForUpdates:(id)controller; +/** +Sets the certificate provider responsible for obtaining certificates +*/ +- (void)setCertificateProvider:(id)provider; + +/** + Get the certificate provider of Flipper Client +*/ +- (id)getCertificateProvider; + // initializers are disabled. You must use `+[FlipperClient sharedClient]` // instance. - (instancetype)init NS_UNAVAILABLE; diff --git a/iOS/FlipperKit/FlipperClient.mm b/iOS/FlipperKit/FlipperClient.mm index 93624d33d..c28e095c3 100644 --- a/iOS/FlipperKit/FlipperClient.mm +++ b/iOS/FlipperKit/FlipperClient.mm @@ -8,15 +8,17 @@ #if FB_SONARKIT_ENABLED #import "FlipperClient.h" +#import #import #import #include #include +#include #import "FlipperClient+Testing.h" #import "FlipperCppWrapperPlugin.h" +#import "FlipperKitCertificateProvider.h" #import "SKEnvironmentVariables.h" #include "SKStateUpdateCPPWrapper.h" - #if !TARGET_OS_SIMULATOR #import #endif @@ -27,6 +29,7 @@ using WrapperPlugin = facebook::flipper::FlipperCppWrapperPlugin; facebook::flipper::FlipperClient* _cppClient; folly::ScopedEventBaseThread sonarThread; folly::ScopedEventBaseThread connectionThread; + id _certProvider; #if !TARGET_OS_SIMULATOR FKPortForwardingServer* _secureServer; FKPortForwardingServer* _insecureServer; @@ -46,7 +49,6 @@ using WrapperPlugin = facebook::flipper::FlipperCppWrapperPlugin; }); return sharedClient; } - - (instancetype)init { if (self = [super init]) { UIDevice* device = [UIDevice currentDevice]; @@ -57,9 +59,7 @@ using WrapperPlugin = facebook::flipper::FlipperCppWrapperPlugin; NSString* appId = [bundle bundleIdentifier]; NSString* privateAppDirectory = NSSearchPathForDirectoriesInDomains( NSApplicationSupportDirectory, NSUserDomainMask, YES)[0]; - NSFileManager* manager = [NSFileManager defaultManager]; - if ([manager fileExistsAtPath:privateAppDirectory isDirectory:NULL] == NO && ![manager createDirectoryAtPath:privateAppDirectory withIntermediateDirectories:YES @@ -99,6 +99,19 @@ using WrapperPlugin = facebook::flipper::FlipperCppWrapperPlugin; return self; } +- (void)setCertificateProvider:(id)provider { + _certProvider = provider; + std::shared_ptr* prov = + static_cast< + std::shared_ptr*>( + [provider getCPPCertificateProvider]); + _cppClient->setCertificateProvider(*prov); +} + +- (id)getCertificateProvider { + return _certProvider; +} + - (void)refreshPlugins { _cppClient->refreshPlugins(); } diff --git a/iOS/FlipperKit/FlipperKitCertificateProvider.h b/iOS/FlipperKit/FlipperKitCertificateProvider.h new file mode 100644 index 000000000..b98d713dc --- /dev/null +++ b/iOS/FlipperKit/FlipperKitCertificateProvider.h @@ -0,0 +1,33 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#ifdef FB_SONARKIT_ENABLED + +#import + +typedef enum FlipperCertificateExchangeMedium + FlipperKitCertificateExchangeMedium; +/** +Represents a CPP Certificate Provider to be used by FlipperClient +*/ +@protocol FlipperKitCertificateProvider + +- (_Nonnull instancetype)initCPPCertificateProvider; + +- (void* _Nonnull) + getCPPCertificateProvider; // Returning it as void* as the file needs to + // have no cpp for it to be compatible with + // Swift. The pointer returned should point to + // std::shared_ptr +- (void)setCertificateExchangeMedium: + (FlipperKitCertificateExchangeMedium)medium; + +@optional +- (void)setAuthToken:(nullable NSString*)authToken; +@end + +#endif diff --git a/xplat/Flipper/ConnectionContextStore.cpp b/xplat/Flipper/ConnectionContextStore.cpp index b1d448ab2..59be9b3ba 100644 --- a/xplat/Flipper/ConnectionContextStore.cpp +++ b/xplat/Flipper/ConnectionContextStore.cpp @@ -8,12 +8,10 @@ #include "ConnectionContextStore.h" #include #include -#include #include #include #include "CertificateUtils.h" #include "Log.h" - using namespace facebook::flipper; static constexpr auto CSR_FILE_NAME = "app.csr"; diff --git a/xplat/Flipper/FlipperCertificateExchangeMedium.h b/xplat/Flipper/FlipperCertificateExchangeMedium.h new file mode 100644 index 000000000..552b081e5 --- /dev/null +++ b/xplat/Flipper/FlipperCertificateExchangeMedium.h @@ -0,0 +1,8 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +enum FlipperCertificateExchangeMedium { FS_ACCESS = 1, WWW = 2 }; diff --git a/xplat/Flipper/FlipperCertificateProvider.h b/xplat/Flipper/FlipperCertificateProvider.h new file mode 100644 index 000000000..1b751fafa --- /dev/null +++ b/xplat/Flipper/FlipperCertificateProvider.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include +#include "FlipperCertificateExchangeMedium.h" +namespace facebook { +namespace flipper { + +/** + * Represents a FlipperCertificateProvider which is responsible for obtaining + * Flipper TLS certificates. + */ +class FlipperCertificateProvider { + public: + virtual ~FlipperCertificateProvider() {} + + /** + * Gets certificates downloaded at a path, which is passed as an argument. + */ + virtual void getCertificates( + const std::string& path, + const std::string& deviceID) = 0; + + virtual void setCertificateExchangeMedium( + const FlipperCertificateExchangeMedium medium) = 0; + + virtual FlipperCertificateExchangeMedium getCertificateExchangeMedium() = 0; +}; + +} // namespace flipper +} // namespace facebook diff --git a/xplat/Flipper/FlipperClient.cpp b/xplat/Flipper/FlipperClient.cpp index 6b897d581..44659fc40 100644 --- a/xplat/Flipper/FlipperClient.cpp +++ b/xplat/Flipper/FlipperClient.cpp @@ -73,6 +73,17 @@ void FlipperClient::addPlugin(std::shared_ptr plugin) { }); } +void FlipperClient::setCertificateProvider( + const std::shared_ptr provider) { + socket_->setCertificateProvider(provider); + log("cpp setCertificateProvider called"); +} + +std::shared_ptr +FlipperClient::getCertificateProvider() { + return socket_->getCertificateProvider(); +} + void FlipperClient::removePlugin(std::shared_ptr plugin) { performAndReportError([this, plugin]() { log("FlipperClient::removePlugin " + plugin->identifier()); diff --git a/xplat/Flipper/FlipperClient.h b/xplat/Flipper/FlipperClient.h index 28071e818..31b71e8ac 100644 --- a/xplat/Flipper/FlipperClient.h +++ b/xplat/Flipper/FlipperClient.h @@ -10,6 +10,7 @@ #include #include #include +#include "FlipperCertificateProvider.h" #include "FlipperConnectionImpl.h" #include "FlipperConnectionManager.h" #include "FlipperInitConfig.h" @@ -85,6 +86,10 @@ class FlipperClient : public FlipperConnectionManager::Callbacks { void setStateListener( std::shared_ptr stateListener); + void setCertificateProvider( + const std::shared_ptr provider); + std::shared_ptr getCertificateProvider(); + std::shared_ptr getPlugin(const std::string& identifier); std::string getState(); diff --git a/xplat/Flipper/FlipperConnectionManager.h b/xplat/Flipper/FlipperConnectionManager.h index 6c41cc943..0c5c86c09 100644 --- a/xplat/Flipper/FlipperConnectionManager.h +++ b/xplat/Flipper/FlipperConnectionManager.h @@ -8,6 +8,7 @@ #pragma once #include +#include "FlipperCertificateProvider.h" #include "FlipperResponder.h" namespace facebook { @@ -30,6 +31,18 @@ class FlipperConnectionManager { */ virtual void stop() = 0; + /** + Sets the Auth token to be used for hitting an Intern end point + */ + virtual void setCertificateProvider( + const std::shared_ptr provider) = 0; + + /** + Gets the certificate provider + */ + virtual std::shared_ptr + getCertificateProvider() = 0; + /** True if there's an open connection. This method may block if the connection is busy. diff --git a/xplat/Flipper/FlipperConnectionManagerImpl.cpp b/xplat/Flipper/FlipperConnectionManagerImpl.cpp index e79f85721..f0049d30f 100644 --- a/xplat/Flipper/FlipperConnectionManagerImpl.cpp +++ b/xplat/Flipper/FlipperConnectionManagerImpl.cpp @@ -90,6 +90,16 @@ FlipperConnectionManagerImpl::~FlipperConnectionManagerImpl() { stop(); } +void FlipperConnectionManagerImpl::setCertificateProvider( + const std::shared_ptr provider) { + certProvider_ = provider; +}; + +std::shared_ptr +FlipperConnectionManagerImpl::getCertificateProvider() { + return certProvider_; +}; + void FlipperConnectionManagerImpl::start() { if (isStarted_) { log("Already started"); @@ -169,10 +179,13 @@ void FlipperConnectionManagerImpl::startSync() { bool FlipperConnectionManagerImpl::doCertificateExchange() { rsocket::SetupParameters parameters; folly::SocketAddress address; + int medium = certProvider_ != nullptr + ? certProvider_->getCertificateExchangeMedium() + : FlipperCertificateExchangeMedium::FS_ACCESS; parameters.payload = rsocket::Payload(folly::toJson(folly::dynamic::object( "os", deviceData_.os)("device", deviceData_.device)( - "app", deviceData_.app)("sdk_version", sdkVersion))); + "app", deviceData_.app)("sdk_version", sdkVersion)("medium", medium))); address.setFromHostPort(deviceData_.host, insecurePort); auto connectingInsecurely = flipperState_->start("Connect insecurely"); @@ -358,6 +371,9 @@ void FlipperConnectionManagerImpl::requestSignedCertFromFlipper() { } gettingCert->complete(); log("Certificate exchange complete."); + // TODO: Use Certificate provider get Certificates + // `certProvider_->getCertificates("path", "device");` + // Disconnect after message sending is complete. // This will trigger a reconnect which should use the secure // channel. diff --git a/xplat/Flipper/FlipperConnectionManagerImpl.h b/xplat/Flipper/FlipperConnectionManagerImpl.h index 004c5be31..04a885359 100644 --- a/xplat/Flipper/FlipperConnectionManagerImpl.h +++ b/xplat/Flipper/FlipperConnectionManagerImpl.h @@ -50,10 +50,14 @@ class FlipperConnectionManagerImpl : public FlipperConnectionManager { std::unique_ptr responder) override; void reconnect(); + void setCertificateProvider( + const std::shared_ptr provider) override; + std::shared_ptr getCertificateProvider() override; private: bool isOpen_ = false; bool isStarted_ = false; + std::shared_ptr certProvider_ = nullptr; Callbacks* callbacks_; DeviceData deviceData_; std::shared_ptr flipperState_; diff --git a/xplat/FlipperTestLib/FlipperConnectionManagerMock.h b/xplat/FlipperTestLib/FlipperConnectionManagerMock.h index 3403acc47..b918f57be 100644 --- a/xplat/FlipperTestLib/FlipperConnectionManagerMock.h +++ b/xplat/FlipperTestLib/FlipperConnectionManagerMock.h @@ -40,6 +40,14 @@ class FlipperConnectionManagerMock : public FlipperConnectionManager { messages.push_back(message); } + void setCertificateProvider( + const std::shared_ptr provider) override{}; + + std::shared_ptr getCertificateProvider() + override { + return nullptr; + }; + void onMessageReceived( const folly::dynamic& message, std::unique_ptr responder) override {