Makefile.am | 3 wsd/LOOLWSD.cpp | 11 ++ wsd/ProofKey.cpp | 220 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ wsd/ProofKey.hpp | 32 ++++++++ wsd/Storage.cpp | 38 +++++---- 5 files changed, 286 insertions(+), 18 deletions(-)
New commits: commit a986aabeb1d899b123fa3486008e4ecb2b981642 Author: Mike Kaganski <mike.kagan...@collabora.com> AuthorDate: Thu Nov 7 18:24:37 2019 +0300 Commit: Mike Kaganski <mike.kagan...@collabora.com> CommitDate: Thu Nov 21 12:56:53 2019 +0100 Initial implementation of proof-key Change-Id: I7ab79218ca2af268dd4573cb64c6353dc71b5f03 Reviewed-on: https://gerrit.libreoffice.org/82232 Reviewed-by: Mike Kaganski <mike.kagan...@collabora.com> Tested-by: Mike Kaganski <mike.kagan...@collabora.com> diff --git a/Makefile.am b/Makefile.am index 9bed60626..1035a22e2 100644 --- a/Makefile.am +++ b/Makefile.am @@ -109,7 +109,8 @@ loolwsd_sources = common/Crypto.cpp \ wsd/ClientSession.cpp \ wsd/FileServer.cpp \ wsd/Storage.cpp \ - wsd/TileCache.cpp + wsd/TileCache.cpp \ + wsd/ProofKey.cpp loolwsd_SOURCES = $(loolwsd_sources) \ $(shared_sources) diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp index e7b4f7439..cfa03034a 100644 --- a/wsd/LOOLWSD.cpp +++ b/wsd/LOOLWSD.cpp @@ -10,6 +10,7 @@ #include <config.h> #include "LOOLWSD.hpp" +#include "ProofKey.hpp" /* Default host used in the start test URI */ #define LOOLWSD_TEST_HOST "localhost" @@ -3002,6 +3003,16 @@ private: LOOLWSD::EditFileExtensions.insert(elem->getAttribute("ext")); } + const auto& proofAttribs = GetProofKeyAttributes(); + if (!proofAttribs.empty()) + { + // Add proof-key element to wopi-discovery root + AutoPtr<Element> keyElem = docXML->createElement("proof-key"); + for (const auto& attrib : proofAttribs) + keyElem->setAttribute(attrib.first, attrib.second); + docXML->documentElement()->appendChild(keyElem); + } + std::ostringstream ostrXML; DOMWriter writer; writer.writeNode(ostrXML, docXML); diff --git a/wsd/ProofKey.cpp b/wsd/ProofKey.cpp new file mode 100644 index 000000000..d4587dc70 --- /dev/null +++ b/wsd/ProofKey.cpp @@ -0,0 +1,220 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <config.h> + +#include "ProofKey.hpp" +#include "LOOLWSD.hpp" + +#include <cassert> +#include <chrono> +#include <cstdlib> +#include <memory> + +#include <Poco/Base64Decoder.h> +#include <Poco/Base64Encoder.h> +#include <Poco/BinaryWriter.h> +#include <Poco/Crypto/RSADigestEngine.h> +#include <Poco/Crypto/RSAKey.h> +#include <Poco/Dynamic/Var.h> +#include <Poco/JSON/Object.h> +#include <Poco/JSON/Parser.h> +#include <Poco/LineEndingConverter.h> +#include <Poco/Net/HTTPClientSession.h> +#include <Poco/Net/HTTPRequest.h> +#include <Poco/Net/HTTPResponse.h> +#include <Poco/Net/NetException.h> +#include <Poco/StringTokenizer.h> +#include <Poco/Timestamp.h> +#include <Poco/URI.h> +#include <Poco/Util/Application.h> + +#include <Log.hpp> +#include <Util.hpp> + +namespace{ + +class Proof { +public: + Proof(); + VecOfStringPairs GetProofHeaders(const std::string& access_token, const std::string& uri) const; + const VecOfStringPairs& GetProofKeyAttributes() const { return m_aAttribs; } +private: + static std::string ProofKeyPath(); + + // Returns .Net tick (=100ns) count since 0001-01-01 00:00:00 Z + // See https://docs.microsoft.com/en-us/dotnet/api/system.datetime.ticks + static int64_t DotNetTicks(const std::chrono::system_clock::time_point& utc); + // Returns string of bytes to sign and base64-encode + // See http://www.wictorwilen.se/sharepoint-2013-building-your-own-wopi-client-part-2 + static std::string GetProof(const std::string& access_token, const std::string& uri, int64_t ticks); + // Signs string of bytes and returns base64-encoded string + std::string SignProof(const std::string& proof) const; + + const std::unique_ptr<const Poco::Crypto::RSAKey> m_pKey; + VecOfStringPairs m_aAttribs; +}; + +Proof::Proof() + : m_pKey([]() -> Poco::Crypto::RSAKey* { + try + { + return new Poco::Crypto::RSAKey("", ProofKeyPath()); + } + catch (const Poco::Exception& e) + { + LOG_ERR("Could not open proof RSA key: " << e.displayText()); + } + catch (const std::exception& e) + { + LOG_ERR("Could not open proof RSA key: " << e.what()); + } + catch (...) + { + LOG_ERR("Could not open proof RSA key: unknown exception"); + } + return nullptr; + }()) +{ + if (m_pKey) + { + { + // TODO: This is definitely not correct at the moment. The proof key must be + // base64-encoded blob in "unmanaged Microsoft Cryptographic API (CAPI)" format + // (as .Net's RSACryptoServiceProvider::ExportScpBlob returns). + std::ostringstream oss; + Poco::OutputLineEndingConverter lineEndingConv(oss, ""); + m_pKey->save(&lineEndingConv); + std::string sKey = oss.str(); + const std::string sBegin = "-----BEGIN RSA PUBLIC KEY-----"; + const std::string sEnd = "-----END RSA PUBLIC KEY-----"; + auto pos = sKey.find(sBegin); + if (pos != std::string::npos) + sKey = sKey.substr(pos + sBegin.length()); + pos = sKey.find(sEnd); + if (pos != std::string::npos) + sKey = sKey.substr(0, pos); + m_aAttribs.emplace_back("value", sKey); + } + { + std::ostringstream oss; + // The signature generated contains CRLF line endings. + // Use a line ending converter to remove these CRLF + Poco::OutputLineEndingConverter lineEndingConv(oss, ""); + Poco::Base64Encoder encoder(lineEndingConv); + const auto m = m_pKey->modulus(); + encoder << std::string(m.begin(), m.end()); + encoder.close(); + m_aAttribs.emplace_back("modulus", oss.str()); + } + { + std::ostringstream oss; + // The signature generated contains CRLF line endings. + // Use a line ending converter to remove these CRLF + Poco::OutputLineEndingConverter lineEndingConv(oss, ""); + Poco::Base64Encoder encoder(lineEndingConv); + const auto e = m_pKey->encryptionExponent(); + encoder << std::string(e.begin(), e.end()); + encoder.close(); + m_aAttribs.emplace_back("exponent", oss.str()); + } + } +} + +std::string Proof::ProofKeyPath() +{ + const std::string keyPath = LOOLWSD_CONFIGDIR "/proof_key"; + if (!Poco::File(keyPath).exists()) + { + std::string msg = "Could not find " + keyPath + + "\nNo proof-key will be present in discovery." + "\nGenerate an RSA key using this command line:" + "\n ssh-keygen -t rsa -N \"\" -f \"" + keyPath + "\""; + fprintf(stderr, "%s\n", msg.c_str()); + LOG_WRN(msg); + } + + return keyPath; +} + +int64_t Proof::DotNetTicks(const std::chrono::system_clock::time_point& utc) +{ + // Get time point for Unix epoch; unfortunately from_time_t isn't constexpr + const auto aUnxEpoch(std::chrono::system_clock::from_time_t(0)); + const auto duration_ns = std::chrono::duration_cast<std::chrono::nanoseconds>(utc - aUnxEpoch); + return duration_ns.count() / 100 + 621355968000000000; +} + +std::string Proof::GetProof(const std::string& access_token, const std::string& uri, int64_t ticks) +{ + std::string decoded_access_token; + Poco::URI::decode(access_token, decoded_access_token); + assert(decoded_access_token.size() <= std::numeric_limits<int32_t>::max() + && uri.size() <= std::numeric_limits<int32_t>::max()); + const size_t size = 4 + decoded_access_token.size() + 4 + uri.size() + 4 + 8; + Poco::Buffer<char> buffer(size); // allocate enough size + buffer.resize(0); // start from empty buffer + Poco::MemoryBinaryWriter writer(buffer, Poco::BinaryWriter::NETWORK_BYTE_ORDER); + writer << static_cast<int32_t>(decoded_access_token.size()) + << decoded_access_token + << static_cast<int32_t>(uri.size()) + << uri + << int32_t(8) + << ticks; + assert(buffer.size() == size); + return std::string(buffer.begin(), buffer.end()); +} + +std::string Proof::SignProof(const std::string& proof) const +{ + assert(m_pKey); + std::ostringstream ostr; + static Poco::Crypto::RSADigestEngine digestEngine(*m_pKey, "SHA256"); + digestEngine.update(proof.c_str(), proof.length()); + Poco::Crypto::DigestEngine::Digest digest = digestEngine.signature(); + // The signature generated contains CRLF line endings. + // Use a line ending converter to remove these CRLF + Poco::OutputLineEndingConverter lineEndingConv(ostr, ""); + Poco::Base64Encoder encoder(lineEndingConv); + encoder << std::string(digest.begin(), digest.end()); + encoder.close(); + return ostr.str(); +} + +VecOfStringPairs Proof::GetProofHeaders(const std::string& access_token, const std::string& uri) const +{ + VecOfStringPairs vec; + if (m_pKey) + { + int64_t ticks = DotNetTicks(std::chrono::system_clock::now()); + vec.emplace_back("X-WOPI-TimeStamp", std::to_string(ticks)); + vec.emplace_back("X-WOPI-Proof", SignProof(GetProof(access_token, uri, ticks))); + } + return vec; +} + +const Proof& GetProof() +{ + static const Proof proof; + return proof; +} + +} + +VecOfStringPairs GetProofHeaders(const std::string& access_token, const std::string& uri) +{ + return GetProof().GetProofHeaders(access_token, uri); +} + +const VecOfStringPairs& GetProofKeyAttributes() +{ + return GetProof().GetProofKeyAttributes(); +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/wsd/ProofKey.hpp b/wsd/ProofKey.hpp new file mode 100644 index 000000000..e1d4b63a3 --- /dev/null +++ b/wsd/ProofKey.hpp @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ +/* + * This file is part of the LibreOffice project. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +// WOPI proof management +#ifndef INCLUDED_PROOFKEY_HPP +#define INCLUDED_PROOFKEY_HPP + +#include <string> +#include <utility> +#include <vector> + +typedef std::vector<std::pair<std::string, std::string>> VecOfStringPairs; + +// Returns pairs <header_name, header_value> to add to request +// The headers returned are X-WOPI-TimeStamp, X-WOPI-Proof +// If no proof key, returns empty vector +// Both parameters are utf-8-encoded strings +VecOfStringPairs GetProofHeaders(const std::string& access_token, const std::string& uri); + +// Returns pairs <attribute, value> to set in proof-key element in discovery xml. +// If no proof key, returns empty vector +const VecOfStringPairs& GetProofKeyAttributes(); + +#endif // INCLUDED_PROOFKEY_HPP + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/wsd/Storage.cpp b/wsd/Storage.cpp index 5569763ee..6b5b87daf 100644 --- a/wsd/Storage.cpp +++ b/wsd/Storage.cpp @@ -47,6 +47,7 @@ #include <Log.hpp> #include <Unit.hpp> #include <Util.hpp> +#include "ProofKey.hpp" #include <common/FileUtil.hpp> #include <common/JsonUtil.hpp> @@ -445,18 +446,18 @@ static void addStorageReuseCookie(Poco::Net::HTTPRequest& request, const std::st } } -std::string getReuseCookies(const Poco::URI &uriObject) +void addWopiProof(Poco::Net::HTTPRequest& request, const std::string& access_token) { - std::string reuseStorageCookies; - for (const auto& param : uriObject.getQueryParameters()) - { - if (param.first == "reuse_cookies") - { - reuseStorageCookies = param.second; - break; - } - } - return reuseStorageCookies; + for (const auto header : GetProofHeaders(access_token, request.getURI())) + request.set(header.first, header.second); +} + +std::map<std::string, std::string> GetQueryParams(const Poco::URI& uri) +{ + std::map<std::string, std::string> result; + for (const auto& param : uri.getQueryParameters()) + result.emplace(param); + return result; } } // anonymous namespace @@ -467,7 +468,7 @@ std::unique_ptr<WopiStorage::WOPIFileInfo> WopiStorage::getWOPIFileInfo(const Au Poco::URI uriObject(getUri()); auth.authorizeURI(uriObject); const std::string uriAnonym = LOOLWSD::anonymizeUrl(uriObject.toString()); - std::string reuseStorageCookies = getReuseCookies(uriObject); + std::map<std::string, std::string> params = GetQueryParams(uriObject); LOG_DBG("Getting info for wopi uri [" << uriAnonym << "]."); @@ -480,7 +481,8 @@ std::unique_ptr<WopiStorage::WOPIFileInfo> WopiStorage::getWOPIFileInfo(const Au auth.authorizeRequest(request); addStorageDebugCookie(request); if (_reuseCookies) - addStorageReuseCookie(request, reuseStorageCookies); + addStorageReuseCookie(request, params["reuse_cookies"]); + addWopiProof(request, params["access_token"]); const auto startTime = std::chrono::steady_clock::now(); std::unique_ptr<Poco::Net::HTTPClientSession> psession(getHTTPClientSession(uriObject)); @@ -676,7 +678,7 @@ std::string WopiStorage::loadStorageFileToLocal(const Authorization& auth, const uriObject.setPath(uriObject.getPath() + "/contents"); auth.authorizeURI(uriObject); - std::string reuseStorageCookies = getReuseCookies(uriObject); + std::map<std::string, std::string> params = GetQueryParams(uriObject); Poco::URI uriObjectAnonym(getUri()); uriObjectAnonym.setPath(LOOLWSD::anonymizeUrl(uriObjectAnonym.getPath()) + "/contents"); @@ -705,7 +707,8 @@ std::string WopiStorage::loadStorageFileToLocal(const Authorization& auth, const auth.authorizeRequest(request); addStorageDebugCookie(request); if (_reuseCookies) - addStorageReuseCookie(request, reuseStorageCookies); + addStorageReuseCookie(request, params["reuse_cookies"]); + addWopiProof(request, params["access_token"]); psession->sendRequest(request); Poco::Net::HTTPResponse response; @@ -772,7 +775,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization& uriObject.setPath(isSaveAs || isRename? uriObject.getPath(): uriObject.getPath() + "/contents"); auth.authorizeURI(uriObject); - std::string reuseStorageCookies = getReuseCookies(uriObject); + std::map<std::string, std::string> params = GetQueryParams(uriObject); const std::string uriAnonym = LOOLWSD::anonymizeUrl(uriObject.toString()); @@ -854,7 +857,8 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization& request.setContentLength(size); addStorageDebugCookie(request); if (_reuseCookies) - addStorageReuseCookie(request, reuseStorageCookies); + addStorageReuseCookie(request, params["reuse_cookies"]); + addWopiProof(request, params["access_token"]); std::ostream& os = psession->sendRequest(request); std::ifstream ifs(filePath); _______________________________________________ Libreoffice-commits mailing list libreoffice-comm...@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits