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 {