From 43179a7ef40ca20f01b5c0741024710770e36baa Mon Sep 17 00:00:00 2001 From: Lorenzo Blasa Date: Wed, 4 Aug 2021 06:33:27 -0700 Subject: [PATCH] ConnectionContext to expose client certificate in PKCS #12 format Summary: RSocket plays nicely with Folly and OpenSSL. Flipper WebSocket-client uses SocketRocket which instead relies on Apple's NSInputStream and NSOutputStream types. SSL options can be set to secure the communication in both. Unfortunately, Apple APIs are a bit limited on the supported cryptographic formats it can accept as arguments. SSL options require the client certificate to be set in PKCS #12 format, contrary to the existing PEM format used by RSocket. This change adds a method to the ConnectionContext which converts and saves the client certificate in PKCS #12 format. The method is always expected to succeed as it will only be called once a valid client certificate is available. An unlikely failure will raise an exception. Reviewed By: fabiomassimo Differential Revision: D30074334 fbshipit-source-id: 91a475d080569cc339b649c7302b1f28793c7de7 --- xplat/Flipper/CertificateUtils.cpp | 202 ++++++++++++++++-- xplat/Flipper/CertificateUtils.h | 8 + xplat/Flipper/ConnectionContextStore.cpp | 38 +++- xplat/Flipper/ConnectionContextStore.h | 11 +- .../Flipper/FlipperConnectionManagerImpl.cpp | 2 + .../ConnectionContextStoreMock.h | 4 +- 6 files changed, 237 insertions(+), 28 deletions(-) diff --git a/xplat/Flipper/CertificateUtils.cpp b/xplat/Flipper/CertificateUtils.cpp index 60db2aba7..2dd81a4bc 100644 --- a/xplat/Flipper/CertificateUtils.cpp +++ b/xplat/Flipper/CertificateUtils.cpp @@ -10,20 +10,31 @@ #include #include #include +#include #include +#include #include +#include #include +#include namespace facebook { namespace flipper { -void free( +void generateCertSigningRequest_free( EVP_PKEY* pKey, X509_REQ* x509_req, BIGNUM* bne, BIO* privateKey, BIO* csrBio); +void generateCertPKCS12_free( + X509* cacert, + X509* cert, + EVP_PKEY* pKey, + STACK_OF(X509) * cacertstack, + PKCS12* pkcs12bundle); + bool generateCertSigningRequest( const char* appId, const char* csrFile, @@ -58,13 +69,13 @@ bool generateCertSigningRequest( BN_set_flags(bne, BN_FLG_CONSTTIME); ret = BN_set_word(bne, e); if (ret != 1) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } ret = RSA_generate_key_ex(rsa, bits, bne, NULL); if (ret != 1) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } @@ -73,19 +84,19 @@ bool generateCertSigningRequest( int privateKeyFd = open(privateKeyFile, O_CREAT | O_WRONLY, S_IWUSR | S_IRUSR); if (privateKeyFd < 0) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return -1; } FILE* privateKeyFp = fdopen(privateKeyFd, "w"); if (privateKeyFp == NULL) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return -1; } privateKey = BIO_new_fp(privateKeyFp, BIO_CLOSE); ret = PEM_write_bio_RSAPrivateKey(privateKey, rsa, NULL, NULL, 0, NULL, NULL); if (ret != 1) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } } @@ -94,13 +105,13 @@ bool generateCertSigningRequest( ret = BIO_flush(privateKey); if (ret != 1) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } ret = X509_REQ_set_version(x509_req, nVersion); if (ret != 1) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } @@ -115,7 +126,7 @@ bool generateCertSigningRequest( -1, 0); if (ret != 1) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } @@ -128,7 +139,7 @@ bool generateCertSigningRequest( -1, 0); if (ret != 1) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } @@ -141,7 +152,7 @@ bool generateCertSigningRequest( -1, 0); if (ret != 1) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } @@ -154,7 +165,7 @@ bool generateCertSigningRequest( -1, 0); if (ret != 1) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } @@ -167,20 +178,20 @@ bool generateCertSigningRequest( -1, 0); if (ret != 1) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } ret = X509_REQ_set_pubkey(x509_req, pKey); if (ret != 1) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } ret = X509_REQ_sign( x509_req, pKey, EVP_sha256()); // returns x509_req->signature->length if (ret <= 0) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } @@ -188,29 +199,29 @@ bool generateCertSigningRequest( // Write CSR to a file int csrFd = open(csrFile, O_CREAT | O_WRONLY, S_IWUSR | S_IRUSR); if (csrFd < 0) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return -1; } FILE* csrFp = fdopen(csrFd, "w"); if (csrFp == NULL) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return -1; } csrBio = BIO_new_fp(csrFp, BIO_CLOSE); ret = PEM_write_bio_X509_REQ(csrBio, x509_req); if (ret != 1) { - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return ret; } } ret = BIO_flush(csrBio); - free(pKey, x509_req, bne, privateKey, csrBio); + generateCertSigningRequest_free(pKey, x509_req, bne, privateKey, csrBio); return (ret == 1); } -void free( +void generateCertSigningRequest_free( EVP_PKEY* pKey, X509_REQ* x509_req, BIGNUM* bne, @@ -223,5 +234,156 @@ void free( BIO_free_all(csrBio); } +bool generateCertPKCS12( + const char* cacertFilepath, + const char* certFilepath, + const char* keyFilepath, + const char* pkcs12Filepath, + const char* pkcs12Name, + const char* pkcs12Password) { + X509 *cert = NULL, *cacert = NULL; + STACK_OF(X509)* cacertstack = NULL; + PKCS12* pkcs12bundle = NULL; + EVP_PKEY* cert_privkey = NULL; + FILE *cacertfile = NULL, *certfile = NULL, *keyfile = NULL, + *pkcs12file = NULL; + int bytes = 0; + + /* 1) These function calls are essential to make PEM_read and + * other openssl functions work. + */ + OpenSSL_add_all_algorithms(); + ERR_load_crypto_strings(); + + /* 2) load the certificate's private key + */ + if ((cert_privkey = EVP_PKEY_new()) == NULL) { + return false; + } + + if (!(keyfile = fopen(keyFilepath, "r"))) { + generateCertPKCS12_free( + cacert, cert, cert_privkey, cacertstack, pkcs12bundle); + return false; + } + + if (!(cert_privkey = PEM_read_PrivateKey(keyfile, NULL, NULL, NULL))) { + fclose(keyfile); + generateCertPKCS12_free( + cacert, cert, cert_privkey, cacertstack, pkcs12bundle); + return false; + } + + fclose(keyfile); + + /* 3) Load the corresponding certificate + */ + if (!(certfile = fopen(certFilepath, "r"))) { + generateCertPKCS12_free( + cacert, cert, cert_privkey, cacertstack, pkcs12bundle); + return false; + } + + if (!(cert = PEM_read_X509(certfile, NULL, NULL, NULL))) { + fclose(certfile); + generateCertPKCS12_free( + cacert, cert, cert_privkey, cacertstack, pkcs12bundle); + return false; + } + + fclose(certfile); + + /* 4) Load the CA certificate who signed it + */ + if (!(cacertfile = fopen(cacertFilepath, "r"))) { + generateCertPKCS12_free( + cacert, cert, cert_privkey, cacertstack, pkcs12bundle); + return false; + } + + if (!(cacert = PEM_read_X509(cacertfile, NULL, NULL, NULL))) { + fclose(cacertfile); + generateCertPKCS12_free( + cacert, cert, cert_privkey, cacertstack, pkcs12bundle); + return false; + } + + fclose(cacertfile); + + /* 5) Load the CA certificate on the stack + */ + if ((cacertstack = sk_X509_new_null()) == NULL) { + generateCertPKCS12_free( + cacert, cert, cert_privkey, cacertstack, pkcs12bundle); + return false; + } + + sk_X509_push(cacertstack, cacert); + + /* 6) we create the PKCS12 structure and fill it with our data + */ + if ((pkcs12bundle = PKCS12_new()) == NULL) { + generateCertPKCS12_free( + cacert, cert, cert_privkey, cacertstack, pkcs12bundle); + return false; + } + + /* values of zero use the openssl default values */ + pkcs12bundle = PKCS12_create( + const_cast(pkcs12Password), // certbundle access password + const_cast(pkcs12Name), // friendly certificate name + cert_privkey, // the certificate private key + cert, // the main certificate + cacertstack, // stack of CA cert chain + 0, // int nid_key (default 3DES) + 0, // int nid_cert (40bitRC2) + 0, // int iter (default 2048) + 0, // int mac_iter (default 1) + 0 // int keytype (default no flag) + ); + + if (pkcs12bundle == nullptr) { + generateCertPKCS12_free( + cacert, cert, cert_privkey, cacertstack, pkcs12bundle); + return false; + } + + /* 7) Write the PKCS12 structure out to file + */ + if (!(pkcs12file = fopen(pkcs12Filepath, "w"))) { + generateCertPKCS12_free( + cacert, cert, cert_privkey, cacertstack, pkcs12bundle); + return false; + } + + bytes = i2d_PKCS12_fp(pkcs12file, pkcs12bundle); + if (bytes <= 0) { + generateCertPKCS12_free( + cacert, cert, cert_privkey, cacertstack, pkcs12bundle); + return false; + } + + /* 8) Done, free resources. + */ + fclose(pkcs12file); + generateCertPKCS12_free( + cacert, cert, cert_privkey, cacertstack, pkcs12bundle); + + return true; +} + +void generateCertPKCS12_free( + X509* cacert, + X509* cert, + EVP_PKEY* pKey, + STACK_OF(X509) * cacertstack, + PKCS12* pkcs12bundle) { + X509_free(cacert); + X509_free(cert); + EVP_PKEY_free(pKey); + sk_X509_free(cacertstack); + PKCS12_free(pkcs12bundle); +} + } // namespace flipper } // namespace facebook diff --git a/xplat/Flipper/CertificateUtils.h b/xplat/Flipper/CertificateUtils.h index 180a78407..9cf01258b 100644 --- a/xplat/Flipper/CertificateUtils.h +++ b/xplat/Flipper/CertificateUtils.h @@ -20,6 +20,14 @@ bool generateCertSigningRequest( const char* csrFile, const char* privateKeyFile); +bool generateCertPKCS12( + const char* caCertificateFile, + const char* certificateFile, + const char* keyFile, + const char* pkcs12File, + const char* pkcs12Name, + const char* pkcs12Password); + } // namespace flipper } // namespace facebook diff --git a/xplat/Flipper/ConnectionContextStore.cpp b/xplat/Flipper/ConnectionContextStore.cpp index a91f80a0f..b8b2d2a70 100644 --- a/xplat/Flipper/ConnectionContextStore.cpp +++ b/xplat/Flipper/ConnectionContextStore.cpp @@ -8,12 +8,17 @@ #include "ConnectionContextStore.h" #include #include +#include +#include +#include +#include #include #include #include +#include +#include #include "CertificateUtils.h" #include "Log.h" - namespace facebook { namespace flipper { @@ -21,6 +26,8 @@ static constexpr auto CSR_FILE_NAME = "app.csr"; static constexpr auto FLIPPER_CA_FILE_NAME = "sonarCA.crt"; static constexpr auto CLIENT_CERT_FILE_NAME = "device.crt"; static constexpr auto PRIVATE_KEY_FILE = "privateKey.pem"; +static constexpr auto CERTIFICATE_FILE_NAME = "device.p12"; +static constexpr auto CERTIFICATE_PASSWORD = "fl1pp3r"; static constexpr auto CONNECTION_CONFIG_FILE = "connection_config.json"; bool fileExists(std::string fileName); @@ -72,7 +79,7 @@ std::string ConnectionContextStore::getCertificateSigningRequest() { return csr; } -std::shared_ptr ConnectionContextStore::getSSLContext() { +std::shared_ptr ConnectionContextStore::getSSLContext() { std::shared_ptr sslContext = std::make_shared(); sslContext->loadTrustedCertificates( @@ -132,7 +139,8 @@ bool ConnectionContextStore::resetState() { FLIPPER_CA_FILE_NAME, CLIENT_CERT_FILE_NAME, PRIVATE_KEY_FILE, - CONNECTION_CONFIG_FILE}) { + CONNECTION_CONFIG_FILE, + CERTIFICATE_FILE_NAME}) { std::remove(absoluteFilePath(file).c_str()); } return true; @@ -142,6 +150,30 @@ bool ConnectionContextStore::resetState() { } } +std::pair ConnectionContextStore::getCertificate() { + auto cacertFilepath = absoluteFilePath(FLIPPER_CA_FILE_NAME); + auto certFilepath = absoluteFilePath(CLIENT_CERT_FILE_NAME); + auto keyFilepath = absoluteFilePath(PRIVATE_KEY_FILE); + auto certificate_path = absoluteFilePath(CERTIFICATE_FILE_NAME); + + if (fileExists(certificate_path.c_str())) { + std::remove(certificate_path.c_str()); + } + + if (!facebook::flipper::generateCertPKCS12( + cacertFilepath.c_str(), + certFilepath.c_str(), + keyFilepath.c_str(), + certificate_path.c_str(), + CERTIFICATE_FILE_NAME, + CERTIFICATE_PASSWORD)) { + log("ERROR: Unable to genereate certificate pkcs#12"); + return std::make_pair("", ""); + } + + return std::make_pair(certificate_path, std::string(CERTIFICATE_PASSWORD)); +} + std::string loadStringFromFile(std::string fileName) { if (!fileExists(fileName)) { return ""; diff --git a/xplat/Flipper/ConnectionContextStore.h b/xplat/Flipper/ConnectionContextStore.h index bad971481..6a7214108 100644 --- a/xplat/Flipper/ConnectionContextStore.h +++ b/xplat/Flipper/ConnectionContextStore.h @@ -12,8 +12,6 @@ #include #include "FlipperInitConfig.h" -using namespace folly; - namespace facebook { namespace flipper { @@ -22,12 +20,19 @@ class ConnectionContextStore { ConnectionContextStore(DeviceData deviceData); bool hasRequiredFiles(); std::string getCertificateSigningRequest(); - std::shared_ptr getSSLContext(); + std::shared_ptr getSSLContext(); std::string getCertificateDirectoryPath(); std::string getDeviceId(); void storeConnectionConfig(folly::dynamic& config); bool resetState(); + /** Convert and save to disk the existing certificate to PKCS #12 format. + * @return Returns a pair where `first` contains the certificate file path and + * `second` contains the certificate export password. If there's an error, the + * pair will contain both empty strings. + */ + std::pair getCertificate(); + private: DeviceData deviceData_; std::string csr = ""; diff --git a/xplat/Flipper/FlipperConnectionManagerImpl.cpp b/xplat/Flipper/FlipperConnectionManagerImpl.cpp index 4e470615e..4c942ed50 100644 --- a/xplat/Flipper/FlipperConnectionManagerImpl.cpp +++ b/xplat/Flipper/FlipperConnectionManagerImpl.cpp @@ -37,6 +37,8 @@ static constexpr int maxPayloadSize = 0xFFFFFF; // To be bumped for every core platform interface change. static constexpr int sdkVersion = 4; +using namespace folly; + namespace facebook { namespace flipper { diff --git a/xplat/FlipperTestLib/ConnectionContextStoreMock.h b/xplat/FlipperTestLib/ConnectionContextStoreMock.h index 8917b2b3a..2fbfbd75a 100644 --- a/xplat/FlipperTestLib/ConnectionContextStoreMock.h +++ b/xplat/FlipperTestLib/ConnectionContextStoreMock.h @@ -20,10 +20,10 @@ class ConnectionContextStoreMock : public ConnectionContextStore { std::string createCertificateSigningRequest() { return "thisIsACsr"; } - std::shared_ptr getSSLContext() { + std::shared_ptr getSSLContext() { return nullptr; } - dynamic getConnectionConfig() { + folly::dynamic getConnectionConfig() { return nullptr; } std::string getCertificateDirectoryPath() {