diff options
| author | xiphon <xiphon@protonmail.com> | 2020-04-06 16:57:32 +0000 |
|---|---|---|
| committer | xiphon <xiphon@protonmail.com> | 2020-04-12 21:34:22 +0000 |
| commit | 5f27a45910fcba954e652f3214bc489f03262346 (patch) | |
| tree | 151d35b69a466881628096f7130eefdd02758596 /src/openpgp | |
| parent | 042400b83fdea6c18ea8a7970cb0a8af2070c03a (diff) | |
| download | monzero-gui-5f27a45910fcba954e652f3214bc489f03262346.tar.gz monzero-gui-5f27a45910fcba954e652f3214bc489f03262346.tar.xz monzero-gui-5f27a45910fcba954e652f3214bc489f03262346.zip | |
'--verify-update', shasum support, OpenPGP signatures verification
Diffstat (limited to 'src/openpgp')
| -rw-r--r-- | src/openpgp/CMakeLists.txt | 18 | ||||
| -rw-r--r-- | src/openpgp/hash.h | 107 | ||||
| -rw-r--r-- | src/openpgp/mpi.h | 78 | ||||
| -rw-r--r-- | src/openpgp/openpgp.cpp | 380 | ||||
| -rw-r--r-- | src/openpgp/openpgp.h | 122 | ||||
| -rw-r--r-- | src/openpgp/packet_stream.h | 76 | ||||
| -rw-r--r-- | src/openpgp/s_expression.h | 78 | ||||
| -rw-r--r-- | src/openpgp/serialization.h | 171 |
8 files changed, 1030 insertions, 0 deletions
diff --git a/src/openpgp/CMakeLists.txt b/src/openpgp/CMakeLists.txt new file mode 100644 index 00000000..a5a36ee7 --- /dev/null +++ b/src/openpgp/CMakeLists.txt @@ -0,0 +1,18 @@ +file(GLOB_RECURSE SOURCES *.cpp) +file(GLOB_RECURSE HEADERS *.h) + +find_library(GCRYPT_LIBRARY gcrypt) +find_library(GPG_ERROR_LIBRARY gpg-error) + +add_library(openpgp + ${SOURCES} + ${HEADERS}) + +target_include_directories(openpgp + PUBLIC + ${CMAKE_SOURCE_DIR}/monero/contrib/epee/include) + +target_link_libraries(openpgp + PUBLIC + ${GCRYPT_LIBRARY} + ${GPG_ERROR_LIBRARY}) diff --git a/src/openpgp/hash.h b/src/openpgp/hash.h new file mode 100644 index 00000000..0952c602 --- /dev/null +++ b/src/openpgp/hash.h @@ -0,0 +1,107 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include <vector> + +#include <gcrypt.h> +#include <span.h> + +namespace openpgp +{ + +class hash +{ +public: + enum algorithm : uint8_t + { + sha256 = 8, + }; + + hash(const hash &) = delete; + hash &operator=(const hash &) = delete; + + hash(uint8_t algorithm) + : algorithm(algorithm) + , consumed(0) + { + if (gcry_md_open(&md, algorithm, 0) != GPG_ERR_NO_ERROR) + { + throw std::runtime_error("failed to create message digest object"); + } + } + + ~hash() + { + gcry_md_close(md); + } + + hash &operator<<(uint8_t byte) + { + gcry_md_putc(md, byte); + ++consumed; + return *this; + } + + hash &operator<<(const epee::span<const uint8_t> &bytes) + { + gcry_md_write(md, &bytes[0], bytes.size()); + consumed += bytes.size(); + return *this; + } + + hash &operator<<(const std::vector<uint8_t> &bytes) + { + return *this << epee::to_span(bytes); + } + + std::vector<uint8_t> finish() const + { + std::vector<uint8_t> result(gcry_md_get_algo_dlen(algorithm)); + const void *digest = gcry_md_read(md, algorithm); + if (digest == nullptr) + { + throw std::runtime_error("failed to read the digest"); + } + memcpy(&result[0], digest, result.size()); + return result; + } + + size_t consumed_bytes() const + { + return consumed; + } + +private: + const uint8_t algorithm; + gcry_md_hd_t md; + size_t consumed; +}; + +} diff --git a/src/openpgp/mpi.h b/src/openpgp/mpi.h new file mode 100644 index 00000000..800bcac5 --- /dev/null +++ b/src/openpgp/mpi.h @@ -0,0 +1,78 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include <gcrypt.h> + +namespace openpgp +{ + +class mpi +{ +public: + mpi(const mpi &) = delete; + mpi &operator=(const mpi &) = delete; + + mpi(mpi &&other) + : data(other.data) + { + other.data = nullptr; + } + + template < + typename byte_container, + typename = typename std::enable_if<(sizeof(typename byte_container::value_type) == 1)>::type> + mpi(const byte_container &buffer, gcry_mpi_format format = GCRYMPI_FMT_USG) + : mpi(&buffer[0], buffer.size(), format) + { + } + + mpi(const void *buffer, size_t size, gcry_mpi_format format = GCRYMPI_FMT_USG) + { + if (gcry_mpi_scan(&data, format, buffer, size, nullptr) != GPG_ERR_NO_ERROR) + { + throw std::runtime_error("failed to read mpi from buffer"); + } + } + + ~mpi() + { + gcry_mpi_release(data); + } + + const gcry_mpi_t &get() const + { + return data; + } + +private: + gcry_mpi_t data; +}; + +} // namespace openpgp diff --git a/src/openpgp/openpgp.cpp b/src/openpgp/openpgp.cpp new file mode 100644 index 00000000..108990fb --- /dev/null +++ b/src/openpgp/openpgp.cpp @@ -0,0 +1,380 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "openpgp.h" + +#include <algorithm> +#include <locale> +#include <vector> + +#include <string_coding.h> + +#include "hash.h" +#include "mpi.h" +#include "packet_stream.h" +#include "s_expression.h" +#include "serialization.h" + +namespace openpgp +{ +namespace +{ + +std::string::const_iterator find_next_line(std::string::const_iterator begin, const std::string::const_iterator &end) +{ + begin = std::find(begin, end, '\n'); + return begin != end ? ++begin : end; +} + +std::string::const_iterator find_line_starting_with( + std::string::const_iterator it, + const std::string::const_iterator &end, + const std::string &starts_with) +{ + for (std::string::const_iterator next_line; it != end; it = next_line) + { + next_line = find_next_line(it, end); + const size_t line_length = static_cast<size_t>(std::distance(it, next_line)); + if (line_length >= starts_with.size() && std::equal(starts_with.begin(), starts_with.end(), it)) + { + return it; + } + } + return end; +} + +std::string::const_iterator find_empty_line(std::string::const_iterator it, const std::string::const_iterator &end) +{ + for (; it != end && *it != '\r' && *it != '\n'; it = find_next_line(it, end)) + { + } + return it; +} + +std::string get_armored_block_contents(const std::string &text, const std::string &block_name) +{ + static constexpr const char dashes[] = "-----"; + const std::string armor_header = dashes + block_name + dashes; + auto block_start = find_line_starting_with(text.begin(), text.end(), armor_header); + auto block_headers = find_next_line(block_start, text.end()); + auto block_end = find_line_starting_with(block_headers, text.end(), dashes); + auto contents_begin = find_next_line(find_empty_line(block_headers, block_end), block_end); + if (contents_begin == block_end) + { + throw std::runtime_error("armored block not found"); + } + return std::string(contents_begin, block_end); +} + +} // namespace + +public_key_rsa::public_key_rsa(const std::string &armored) + : public_key_rsa(decode(armored)) +{ +} + +public_key_rsa::public_key_rsa(std::tuple<std::string, s_expression, size_t> params) + : m_expression(std::move(std::get<1>(params))) + , m_bits(std::get<2>(params)) + , m_user_id(std::move(std::get<0>(params))) +{ +} + +const gcry_sexp_t &public_key_rsa::get() const +{ + return m_expression.get(); +} + +size_t public_key_rsa::bits() const +{ + return m_bits; +} + +std::string public_key_rsa::user_id() const +{ + return m_user_id; +} + +std::tuple<std::string, s_expression, size_t> public_key_rsa::decode(const std::string &armored) +{ + const std::string buffer = epee::string_encoding::base64_decode( + strip_line_breaks(get_armored_block_contents(armored, "BEGIN PGP PUBLIC KEY BLOCK"))); + return decode(epee::to_byte_span(epee::to_span(buffer))); +} + +std::tuple<std::string, s_expression, size_t> public_key_rsa::decode(const epee::span<const uint8_t> buffer) +{ + packet_stream packets(buffer); + + const std::vector<uint8_t> *data = packets.find_first(packet_tag::type::user_id); + if (data == nullptr) + { + throw std::runtime_error("user id is missing"); + } + std::string user_id(data->begin(), data->end()); + + data = packets.find_first(packet_tag::type::public_key); + if (data == nullptr) + { + throw std::runtime_error("public key is missing"); + } + + deserializer<std::vector<uint8_t>> serialized(*data); + + const auto version = serialized.read_big_endian<uint8_t>(); + if (version != 4) + { + throw std::runtime_error("unsupported public key version"); + } + + /* const auto timestamp = */ serialized.read_big_endian<uint32_t>(); + + const auto algorithm = serialized.read_big_endian<uint8_t>(); + if (algorithm != algorithm::rsa) + { + throw std::runtime_error("unsupported public key algorithm"); + } + + const mpi public_key_n = serialized.read_mpi(); + const mpi public_key_e = serialized.read_mpi(); + + s_expression expression("(public-key (rsa (n %m) (e %m)))", public_key_n.get(), public_key_e.get()); + + return {std::move(user_id), std::move(expression), gcry_mpi_get_nbits(public_key_n.get())}; +} + +signature_rsa::signature_rsa( + uint8_t algorithm, + std::pair<uint8_t, uint8_t> hash_leftmost_bytes, + uint8_t hash_algorithm, + const std::vector<uint8_t> &hashed_data, + type type, + s_expression signature, + uint8_t version) + : m_hash_algorithm(hash_algorithm) + , m_hash_leftmost_bytes(hash_leftmost_bytes) + , m_hashed_appendix(format_hashed_appendix(algorithm, hash_algorithm, hashed_data, type, version)) + , m_signature(std::move(signature)) + , m_type(type) +{ +} + +signature_rsa signature_rsa::from_armored(const std::string &armored_signed_message) +{ + return from_base64(get_armored_block_contents(armored_signed_message, "BEGIN PGP SIGNATURE")); +} + +signature_rsa signature_rsa::from_base64(const std::string &base64) +{ + std::string decoded = epee::string_encoding::base64_decode(strip_line_breaks(base64)); + epee::span<const uint8_t> buffer(reinterpret_cast<const uint8_t *>(&decoded[0]), decoded.size()); + return from_buffer(buffer); +} + +signature_rsa signature_rsa::from_buffer(const epee::span<const uint8_t> input) +{ + packet_stream packets(input); + + const std::vector<uint8_t> *data = packets.find_first(packet_tag::type::signature); + if (data == nullptr) + { + throw std::runtime_error("signature is missing"); + } + + deserializer<std::vector<uint8_t>> buffer(*data); + + const auto version = buffer.read_big_endian<uint8_t>(); + if (version != 4) + { + throw std::runtime_error("unsupported signature version"); + } + + const auto signature_type = static_cast<type>(buffer.read_big_endian<uint8_t>()); + + const auto algorithm = buffer.read_big_endian<uint8_t>(); + if (algorithm != algorithm::rsa) + { + throw std::runtime_error("unsupported signature algorithm"); + } + + const auto hash_algorithm = buffer.read_big_endian<uint8_t>(); + + const auto hashed_data_length = buffer.read_big_endian<uint16_t>(); + std::vector<uint8_t> hashed_data = buffer.read(hashed_data_length); + + const auto unhashed_data_length = buffer.read_big_endian<uint16_t>(); + buffer.read_span(unhashed_data_length); + + std::pair<uint8_t, uint8_t> hash_leftmost_bytes{buffer.read_big_endian<uint8_t>(), buffer.read_big_endian<uint8_t>()}; + + const mpi signature = buffer.read_mpi(); + + return signature_rsa( + algorithm, + std::move(hash_leftmost_bytes), + hash_algorithm, + hashed_data, + signature_type, + s_expression("(sig-val (rsa (s %m)))", signature.get()), + version); +} + +bool signature_rsa::verify(const epee::span<const uint8_t> message, const public_key_rsa &public_key) const +{ + const s_expression signed_data = hash_message(message, public_key.bits()); + return gcry_pk_verify(m_signature.get(), signed_data.get(), public_key.get()) == 0; +} + +s_expression signature_rsa::hash_message(const epee::span<const uint8_t> message, size_t public_key_bits) const +{ + switch (m_type) + { + case type::binary_document: + return hash_bytes(message, public_key_bits); + case type::canonical_text_document: + { + std::vector<uint8_t> crlf_formatted; + crlf_formatted.reserve(message.size()); + const size_t message_size = message.size(); + for (size_t offset = 0; offset < message_size; ++offset) + { + const auto &character = message[offset]; + if (character == '\r') + { + continue; + } + if (character == '\n') + { + const bool skip_last_crlf = offset + 1 == message_size; + if (skip_last_crlf) + { + break; + } + crlf_formatted.push_back('\r'); + } + crlf_formatted.push_back(character); + } + return hash_bytes(epee::to_span(crlf_formatted), public_key_bits); + } + default: + throw std::runtime_error("unsupported signature type"); + } +} + +std::vector<uint8_t> signature_rsa::hash_asn_object_id() const +{ + size_t size; + if (gcry_md_algo_info(m_hash_algorithm, GCRYCTL_GET_ASNOID, nullptr, &size) != GPG_ERR_NO_ERROR) + { + throw std::runtime_error("failed to get ASN.1 Object Identifier (OID) size"); + } + + std::vector<uint8_t> asn_object_id(size); + if (gcry_md_algo_info(m_hash_algorithm, GCRYCTL_GET_ASNOID, &asn_object_id[0], &size) != GPG_ERR_NO_ERROR) + { + throw std::runtime_error("failed to get ASN.1 Object Identifier (OID)"); + } + + return asn_object_id; +} + +s_expression signature_rsa::hash_bytes(const epee::span<const uint8_t> message, size_t public_key_bits) const +{ + const std::vector<uint8_t> plain_hash = (hash(m_hash_algorithm) << message << m_hashed_appendix).finish(); + if (plain_hash.size() < 2) + { + throw std::runtime_error("insufficient message hash size"); + } + if (plain_hash[0] != m_hash_leftmost_bytes.first || plain_hash[1] != m_hash_leftmost_bytes.second) + { + throw std::runtime_error("signature checksum doesn't match the expected value"); + } + + std::vector<uint8_t> asn_object_id = hash_asn_object_id(); + + const size_t public_key_bytes = bits_to_bytes(public_key_bits); + if (public_key_bytes < plain_hash.size() + asn_object_id.size() + 11) + { + throw std::runtime_error("insufficient public key bit length"); + } + + std::vector<uint8_t> emsa_pkcs1_v1_5_encoded; + emsa_pkcs1_v1_5_encoded.reserve(public_key_bytes); + emsa_pkcs1_v1_5_encoded.push_back(0); + emsa_pkcs1_v1_5_encoded.push_back(1); + const size_t ps_size = public_key_bytes - plain_hash.size() - asn_object_id.size() - 3; + emsa_pkcs1_v1_5_encoded.insert(emsa_pkcs1_v1_5_encoded.end(), ps_size, 0xff); + emsa_pkcs1_v1_5_encoded.push_back(0); + emsa_pkcs1_v1_5_encoded.insert(emsa_pkcs1_v1_5_encoded.end(), asn_object_id.begin(), asn_object_id.end()); + emsa_pkcs1_v1_5_encoded.insert(emsa_pkcs1_v1_5_encoded.end(), plain_hash.begin(), plain_hash.end()); + + mpi value(emsa_pkcs1_v1_5_encoded); + return s_expression("(data (flags raw) (value %m))", value.get()); +} + +std::vector<uint8_t> signature_rsa::format_hashed_appendix( + uint8_t algorithm, + uint8_t hash_algorithm, + const std::vector<uint8_t> &hashed_data, + uint8_t type, + uint8_t version) +{ + const uint16_t hashed_data_size = static_cast<uint16_t>(hashed_data.size()); + const uint32_t hashed_pefix_size = sizeof(version) + sizeof(type) + sizeof(algorithm) + sizeof(hash_algorithm) + + sizeof(hashed_data_size) + hashed_data.size(); + + std::vector<uint8_t> appendix; + appendix.reserve(hashed_pefix_size + sizeof(version) + sizeof(uint8_t) + sizeof(hashed_pefix_size)); + appendix.push_back(version); + appendix.push_back(type); + appendix.push_back(algorithm); + appendix.push_back(hash_algorithm); + appendix.push_back(static_cast<uint8_t>(hashed_data_size >> 8)); + appendix.push_back(static_cast<uint8_t>(hashed_data_size)); + appendix.insert(appendix.end(), hashed_data.begin(), hashed_data.end()); + appendix.push_back(version); + appendix.push_back(0xff); + appendix.push_back(static_cast<uint8_t>(hashed_pefix_size >> 24)); + appendix.push_back(static_cast<uint8_t>(hashed_pefix_size >> 16)); + appendix.push_back(static_cast<uint8_t>(hashed_pefix_size >> 8)); + appendix.push_back(static_cast<uint8_t>(hashed_pefix_size)); + + return appendix; +} + +message_armored::message_armored(const std::string &message_armored) + : m_message(get_armored_block_contents(message_armored, "BEGIN PGP SIGNED MESSAGE")) +{ +} + +message_armored::operator epee::span<const uint8_t>() const +{ + return epee::to_byte_span(epee::to_span(m_message)); +} + +} // namespace openpgp diff --git a/src/openpgp/openpgp.h b/src/openpgp/openpgp.h new file mode 100644 index 00000000..cef003ed --- /dev/null +++ b/src/openpgp/openpgp.h @@ -0,0 +1,122 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include <vector> + +#include <gcrypt.h> + +#include <span.h> + +#include "s_expression.h" + +namespace openpgp +{ + +enum algorithm : uint8_t +{ + rsa = 1, +}; + +class public_key_rsa +{ +public: + public_key_rsa(const std::string &armored); + public_key_rsa(std::tuple<std::string, s_expression, size_t> params); + + size_t bits() const; + const gcry_sexp_t &get() const; + std::string user_id() const; + +private: + static std::tuple<std::string, s_expression, size_t> decode(const std::string &armored); + static std::tuple<std::string, s_expression, size_t> decode(const epee::span<const uint8_t> buffer); + +private: + s_expression m_expression; + size_t m_bits; + std::string m_user_id; +}; + +class signature_rsa +{ +public: + enum type : uint8_t + { + binary_document = 0, + canonical_text_document = 1, + }; + + signature_rsa( + uint8_t algorithm, + std::pair<uint8_t, uint8_t> hash_leftmost_bytes, + uint8_t hash_algorithm, + const std::vector<uint8_t> &hashed_data, + type type, + s_expression signature, + uint8_t version); + + static signature_rsa from_armored(const std::string &armored_signed_message); + static signature_rsa from_base64(const std::string &base64); + static signature_rsa from_buffer(const epee::span<const uint8_t> input); + + bool verify(const epee::span<const uint8_t> message, const public_key_rsa &public_key) const; + +private: + s_expression hash_message(const epee::span<const uint8_t> message, size_t public_key_bits) const; + std::vector<uint8_t> hash_asn_object_id() const; + s_expression hash_bytes(const epee::span<const uint8_t> message, size_t public_key_bits) const; + + static std::vector<uint8_t> format_hashed_appendix( + uint8_t algorithm, + uint8_t hash_algorithm, + const std::vector<uint8_t> &hashed_data, + uint8_t type, + uint8_t version); + +private: + uint8_t m_hash_algorithm; + std::pair<uint8_t, uint8_t> m_hash_leftmost_bytes; + std::vector<uint8_t> m_hashed_appendix; + s_expression m_signature; + type m_type; +}; + +class message_armored +{ +public: + message_armored(const std::string &message_armored); + + operator epee::span<const uint8_t>() const; + +private: + std::string m_message; +}; + +} // namespace openpgp diff --git a/src/openpgp/packet_stream.h b/src/openpgp/packet_stream.h new file mode 100644 index 00000000..b93b8747 --- /dev/null +++ b/src/openpgp/packet_stream.h @@ -0,0 +1,76 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include <vector> + +#include <span.h> + +#include "serialization.h" + +namespace openpgp +{ + +class packet_stream +{ +public: + packet_stream(const epee::span<const uint8_t> buffer) + : packet_stream(deserializer<epee::span<const uint8_t>>(buffer)) + { + } + + template < + typename byte_container, + typename = typename std::enable_if<(sizeof(typename byte_container::value_type) == 1)>::type> + packet_stream(deserializer<byte_container> buffer) + { + while (!buffer.empty()) + { + packet_tag tag = buffer.read_packet_tag(); + packets.push_back({std::move(tag), buffer.read(tag.length)}); + } + } + + const std::vector<uint8_t> *find_first(packet_tag::type type) const + { + for (const auto &packet : packets) + { + if (packet.first.packet_type == type) + { + return &packet.second; + } + } + return nullptr; + } + +private: + std::vector<std::pair<packet_tag, std::vector<uint8_t>>> packets; +}; + +} // namespace openpgp diff --git a/src/openpgp/s_expression.h b/src/openpgp/s_expression.h new file mode 100644 index 00000000..b46bf216 --- /dev/null +++ b/src/openpgp/s_expression.h @@ -0,0 +1,78 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include <algorithm> +#include <stdexcept> + +#include <gcrypt.h> + +namespace openpgp +{ + +class s_expression +{ +public: + s_expression(const s_expression &) = delete; + s_expression &operator=(const s_expression &) = delete; + + template <typename... Args> + s_expression(Args... args) + { + if (gcry_sexp_build(&data, nullptr, args...) != GPG_ERR_NO_ERROR) + { + throw std::runtime_error("failed to build S-expression"); + } + } + + s_expression(s_expression &&other) + { + std::swap(data, other.data); + } + + s_expression(gcry_sexp_t data) + : data(data) + { + } + + ~s_expression() + { + gcry_sexp_release(data); + } + + const gcry_sexp_t &get() const + { + return data; + } + +private: + gcry_sexp_t data = nullptr; +}; + +} // namespace openpgp diff --git a/src/openpgp/serialization.h b/src/openpgp/serialization.h new file mode 100644 index 00000000..33de9216 --- /dev/null +++ b/src/openpgp/serialization.h @@ -0,0 +1,171 @@ +// Copyright (c) 2020, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#pragma once + +#include "mpi.h" + +namespace openpgp +{ + +size_t bits_to_bytes(size_t bits) +{ + constexpr const uint16_t bits_in_byte = 8; + return (bits + bits_in_byte - 1) / bits_in_byte; +} + +std::string strip_line_breaks(const std::string &string) +{ + std::string result; + result.reserve(string.size()); + for (const auto &character : string) + { + if (character != '\r' && character != '\n') + { + result.push_back(character); + } + } + return result; +} + +struct packet_tag +{ + enum type : uint8_t + { + signature = 2, + public_key = 6, + user_id = 13, + }; + + const type packet_type; + const size_t length; +}; + +template < + typename byte_container, + typename = typename std::enable_if<(sizeof(typename byte_container::value_type) == 1)>::type> +class deserializer +{ +public: + deserializer(byte_container buffer) + : buffer(std::move(buffer)) + , cursor(0) + { + } + + bool empty() const + { + return buffer.size() - cursor == 0; + } + + packet_tag read_packet_tag() + { + const auto tag = read_big_endian<uint8_t>(); + + constexpr const uint8_t format_mask = 0b11000000; + constexpr const uint8_t format_old_tag = 0b10000000; + if ((tag & format_mask) != format_old_tag) + { + throw std::runtime_error("invalid packet tag"); + } + + const packet_tag::type packet_type = static_cast<packet_tag::type>((tag & 0b00111100) >> 2); + const uint8_t length_type = tag & 0b00000011; + + size_t length; + switch (length_type) + { + case 0: + length = read_big_endian<uint8_t>(); + break; + case 1: + length = read_big_endian<uint16_t>(); + break; + case 2: + length = read_big_endian<uint32_t>(); + break; + default: + throw std::runtime_error("unsupported packet length type"); + } + + return {packet_type, length}; + } + + mpi read_mpi() + { + const size_t bit_length = read_big_endian<uint16_t>(); + return mpi(read_span(bits_to_bytes(bit_length))); + } + + std::vector<uint8_t> read(size_t size) + { + if (buffer.size() - cursor < size) + { + throw std::runtime_error("insufficient buffer size"); + } + + const size_t offset = cursor; + cursor += size; + + return {&buffer[offset], &buffer[cursor]}; + } + + template <typename T, typename = typename std::enable_if<std::is_integral<T>::value>::type> + T read_big_endian() + { + if (buffer.size() - cursor < sizeof(T)) + { + throw std::runtime_error("insufficient buffer size"); + } + T result = 0; + for (size_t read = 0; read < sizeof(T); ++read) + { + result = (result << 8) | static_cast<uint8_t>(buffer[cursor++]); + } + return result; + } + + epee::span<const uint8_t> read_span(size_t size) + { + if (buffer.size() - cursor < size) + { + throw std::runtime_error("insufficient buffer size"); + } + + const size_t offset = cursor; + cursor += size; + + return {reinterpret_cast<const uint8_t *>(&buffer[offset]), size}; + } + +private: + byte_container buffer; + size_t cursor; +}; + +} // namespace openpgp |
