Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

net: Sanity check private keys received from SAM proxy #28695

Merged
merged 2 commits into from Oct 30, 2023

Conversation

dergoegge
Copy link
Member

@dergoegge dergoegge commented Oct 20, 2023

Not sanity checking can lead to crashes or worse:

==1715589==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6140000055c2 at pc 0x5622ed66e7ad bp 0x7ffee547a2c0 sp 0x7ffee547a2b8
READ of size 2 at 0x6140000055c2 thread T0 (b-test)
    #0 0x5622ed66e7ac in memcpy include/bits/string_fortified.h:29:10
    #1 0x5622ed66e7ac in i2p::sam::Session::MyDestination() const src/i2p.cpp:362:5
    #2 0x5622ed662e46 in i2p::sam::Session::CreateIfNotCreatedAlready() src/i2p.cpp:414:40
    #3 0x5622ed6619f2 in i2p::sam::Session::Listen(i2p::Connection&) src/i2p.cpp:143:9

@DrahtBot
Copy link
Contributor

DrahtBot commented Oct 20, 2023

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage

For detailed information about the code coverage, see the test coverage report.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK maflcko, stickies-v, vasild
Concept ACK mzumsande, kristapsk

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

@DrahtBot DrahtBot added the P2P label Oct 20, 2023
@dergoegge
Copy link
Member Author

I would greatly appreciate it if someone could write unit tests for this, @vasild @jonatack maybe?

Copy link
Contributor

@mzumsande mzumsande left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK

I think that keys that are loaded from disk (ReadBinaryFile()) also should be sanitized. That's what made the fuzzer crash in #28665.

src/i2p.cpp Outdated Show resolved Hide resolved
@dergoegge
Copy link
Member Author

This should probably be in the 26.0 milestone?

@fanquake fanquake added this to the 26.0 milestone Oct 23, 2023
@maflcko
Copy link
Member

maflcko commented Oct 23, 2023

To what extent is the proxy trusted, and to what extent is it required to validate inputs from it? I think it would be good to first document that and then design unit and fuzz tests based on that design assumption?

@maflcko
Copy link
Member

maflcko commented Oct 23, 2023

typo-nit: received

@fanquake fanquake changed the title net: Sanitize private keys recevied from SAM proxy net: Sanitize private keys received from SAM proxy Oct 23, 2023
@fanquake
Copy link
Member

To what extent is the proxy trusted, and to what extent is it required to validate inputs from it?

@jonatack @vasild @zzzi2p

@zzzi2p
Copy link

zzzi2p commented Oct 23, 2023

The i2pd crash in OP should be reported to that project and/or @orignal

Java I2P probably won't crash on bad input and we do some validation of public and private keys but we can't promise to catch everything.

The format is a little elaborate, I don't know how far down you want to go. Would you validate the individual key fields? Would you check for pubkey/privkey mismatches? Just basic length checks?

From the SAM spec:

The $privkey is the base 64 of the concatenation of the [Destination](https://geti2p.net/en/docs/spec/common-structures#type_Destination) 
followed by the [Private Key](https://geti2p.net/en/docs/spec/common-structures#type_PrivateKey) 
followed by the [Signing Private Key](https://geti2p.net/en/docs/spec/common-structures#type_SigningPrivateKey),
 optionally followed by the [Offline Signature](https://geti2p.net/en/docs/spec/common-structures#struct_OfflineSignature), 
which is 663 or more bytes in binary and 884 or more bytes in base 64, depending on signature type. 
The binary format is specified in [Private Key File](https://github.com/i2p/i2p.i2p/blob/master/core/java/src/net/i2p/data/PrivateKeyFile.java). 
See additional notes about the [Private Key](https://geti2p.net/en/docs/spec/common-structures#type_PrivateKey)
 in the Destination Key Generation section below.

If the signing private key is all zeros, 
the [Offline Signature](https://geti2p.net/en/docs/spec/common-structures#struct_OfflineSignature) 
section follows....

And there's more near the bottom in the 'Destination Key Generation' section of https://geti2p.net/en/docs/api/samv3

You're not using offline signatures so you can skip that part.

I can't really answer your question about "trust" in SAM. It probably won't send you bad input, and in most cases you can treat the private key as an opaque string without "validation". So as long as you don't corrupt it when you store and retrieve it, you should be fine. But it's up to you.

@zzzi2p
Copy link

zzzi2p commented Oct 23, 2023

Ah, correction, OP is a bitcoin crash not a i2pd crash, apologies to @orignal

@mzumsande
Copy link
Contributor

To what extent is the proxy trusted, and to what extent is it required to validate inputs from it? I think it would be good to first document that and then design unit and fuzz tests based on that design assumption?

Wouldn't it be best to write error handling code / tests on the assumption that no external input source should be able to cause memory corruption within bitcoind - including trusted ones, like the datadir?

Copy link
Contributor

@stickies-v stickies-v left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK

I think I would prefer the constructor of Binary to do size sanity checks so we don't need to call a helper function any time we use a Binary, but it seems this doesn't pair very elegantly with the lazy construction approach of sam::Session. Below a diff of one such example implementation that passes unit tests by making m_private_key a std::optional<Binary> (using not-super-safe dereferencing on m_private_key for now just to PoC it) edit: I think vasild's approach is better

git diff on 9ff405e
diff --git a/src/i2p.cpp b/src/i2p.cpp
index 1b8345059e..fc5d23e8fb 100644
--- a/src/i2p.cpp
+++ b/src/i2p.cpp
@@ -77,19 +77,6 @@ static Binary DecodeI2PBase64(const std::string& i2p_b64)
     return std::move(*decoded);
 }
 
-static Binary SanitizePrivKey(Binary&& binary)
-{
-    if (binary.size() < 387) {
-        throw std::runtime_error("Private key too small (size < 387 bytes)");
-    }
-
-    if (binary.size() > (387 + std::numeric_limits<uint16_t>::max())) {
-        throw std::runtime_error("Private key too large");
-    }
-
-    return binary;
-}
-
 /**
  * Derive the .b32.i2p address of an I2P destination (binary).
  * @param[in] dest I2P destination.
@@ -340,7 +327,7 @@ void Session::DestGenerate(const Sock& sock)
     // If SIGNATURE_TYPE is not specified, then the default one is DSA_SHA1.
     const Reply& reply = SendRequestAndGetReply(sock, "DEST GENERATE SIGNATURE_TYPE=7", false);
 
-    m_private_key = SanitizePrivKey(DecodeI2PBase64(reply.Get("PRIV")));
+    m_private_key = DecodeI2PBase64(reply.Get("PRIV"));
 }
 
 void Session::GenerateAndSavePrivateKey(const Sock& sock)
@@ -349,7 +336,7 @@ void Session::GenerateAndSavePrivateKey(const Sock& sock)
 
     // umask is set to 0077 in common/system.cpp, which is ok.
     if (!WriteBinaryFile(m_private_key_file,
-                         std::string(m_private_key.begin(), m_private_key.end()))) {
+                         std::string(m_private_key->begin(), m_private_key->end()))) {
         throw std::runtime_error(
             strprintf("Cannot save I2P private key to %s", fs::quoted(fs::PathToString(m_private_key_file))));
     }
@@ -364,16 +351,16 @@ Binary Session::MyDestination() const
     static constexpr size_t CERT_LEN_POS = 385;
 
     uint16_t cert_len;
-    memcpy(&cert_len, &m_private_key.at(CERT_LEN_POS), sizeof(cert_len));
+    memcpy(&cert_len, &m_private_key->at(CERT_LEN_POS), sizeof(cert_len));
     cert_len = be16toh(cert_len);
 
     const size_t dest_len = DEST_LEN_BASE + cert_len;
 
-    if (dest_len > m_private_key.size()) {
+    if (dest_len > m_private_key->size()) {
         throw std::runtime_error("Certificate length did not match the actual private key length");
     }
 
-    return Binary{m_private_key.begin(), m_private_key.begin() + dest_len};
+    return Binary{m_private_key->begin(), m_private_key->begin() + dest_len};
 }
 
 void Session::CreateIfNotCreatedAlready()
@@ -399,18 +386,18 @@ void Session::CreateIfNotCreatedAlready()
                       "inbound.quantity=1 outbound.quantity=1",
                       session_id));
 
-        m_private_key = SanitizePrivKey(DecodeI2PBase64(reply.Get("DESTINATION")));
+        m_private_key = DecodeI2PBase64(reply.Get("DESTINATION"));
     } else {
         // Read our persistent destination (private key) from disk or generate
         // one and save it to disk. Then use it when creating the session.
         const auto& [read_ok, data] = ReadBinaryFile(m_private_key_file);
         if (read_ok) {
-            m_private_key = SanitizePrivKey(Binary{data.begin(), data.end()});
+            m_private_key = Binary{data.begin(), data.end()};
         } else {
             GenerateAndSavePrivateKey(*sock);
         }
 
-        const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key));
+        const std::string& private_key_b64 = SwapBase64(EncodeBase64(m_private_key.value()));
 
         SendRequestAndGetReply(*sock,
                                strprintf("SESSION CREATE STYLE=STREAM ID=%s DESTINATION=%s "
diff --git a/src/i2p.h b/src/i2p.h
index cb9da64816..e5bcef53e5 100644
--- a/src/i2p.h
+++ b/src/i2p.h
@@ -23,7 +23,31 @@ namespace i2p {
 /**
  * Binary data.
  */
-using Binary = std::vector<uint8_t>;
+class Binary {
+private:
+    std::vector<uint8_t> m_data;
+    void Validate() const {
+        if (m_data.size() < 387) {
+            throw std::runtime_error("Private key too small (size < 387 bytes)");
+        } else if (m_data.size() > (387 + std::numeric_limits<uint16_t>::max())) {
+            throw std::runtime_error("Private key too large");
+        }
+    }
+
+public:
+    template<typename... Args>
+    Binary(Args&&... args) : m_data(std::forward<Args>(args)...) { Validate(); }
+
+    operator Span<const uint8_t>() const { return m_data; }
+
+    // forward to std::vector methods
+    template<typename... Args>
+    auto& at(Args&&... args) const { return m_data.at(std::forward<Args>(args)...); }
+    auto begin() const { return m_data.begin(); }
+    auto data() const { return m_data.data(); }
+    auto end() const { return m_data.end(); }
+    auto size() const { return m_data.size(); }
+};
 
 /**
  * An established connection with another peer.
@@ -253,7 +277,7 @@ private:
      * The private key of this peer.
      * @see The reply to the "DEST GENERATE" command in https://geti2p.net/en/docs/api/samv3
      */
-    Binary m_private_key GUARDED_BY(m_mutex);
+    std::optional<Binary> m_private_key GUARDED_BY(m_mutex);
 
     /**
      * SAM control socket.

src/i2p.cpp Outdated Show resolved Hide resolved
src/i2p.cpp Outdated Show resolved Hide resolved
src/i2p.cpp Outdated Show resolved Hide resolved
@maflcko
Copy link
Member

maflcko commented Oct 24, 2023

In any case, I don't think this is a blocker for rc1. It is not a regression and this diff can be added to rc2 trivially.

Copy link
Contributor

@vasild vasild left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK

src/i2p.cpp Outdated Show resolved Hide resolved
Co-authored-by: Vasil Dimov <vd@FreeBSD.org>
@dergoegge dergoegge changed the title net: Sanitize private keys received from SAM proxy net: Sanity check private keys received from SAM proxy Oct 26, 2023
Copy link
Contributor

@stickies-v stickies-v left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK adb5d6b

src/test/i2p_tests.cpp Outdated Show resolved Hide resolved
Copy link
Contributor

@vasild vasild left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK adb5d6b

@kristapsk
Copy link
Contributor

Concept ACK

luke-jr pushed a commit to bitcoinknots/bitcoin that referenced this pull request Oct 28, 2023
Co-authored-by: Vasil Dimov <vd@FreeBSD.org>

Github-Pull: bitcoin#28695
Rebased-From: cf70a8d
luke-jr added a commit to bitcoinknots/bitcoin that referenced this pull request Oct 28, 2023
@maflcko
Copy link
Member

maflcko commented Oct 30, 2023

code lgtm ACK 5cf4d26

Copy link
Contributor

@stickies-v stickies-v left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-ACK 5cf4d26

Copy link
Contributor

@vasild vasild left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 5cf4d26

@fanquake fanquake merged commit ec5116a into bitcoin:master Oct 30, 2023
16 checks passed
fanquake pushed a commit to fanquake/bitcoin that referenced this pull request Oct 30, 2023
Co-authored-by: Vasil Dimov <vd@FreeBSD.org>

GitHub-Pull: bitcoin#28695
Rebased-From: cf70a8d
fanquake pushed a commit to fanquake/bitcoin that referenced this pull request Oct 30, 2023
@fanquake
Copy link
Member

Backported to 26.x in #28754.

fanquake pushed a commit to fanquake/bitcoin that referenced this pull request Oct 31, 2023
Co-authored-by: Vasil Dimov <vd@FreeBSD.org>

GitHub-Pull: bitcoin#28695
Rebased-From: cf70a8d
fanquake pushed a commit to fanquake/bitcoin that referenced this pull request Oct 31, 2023
fanquake added a commit that referenced this pull request Nov 1, 2023
e4e8479 doc: update manual pages for v26.0rc2 (fanquake)
0b189a9 build: bump version to v26.0rc2 (fanquake)
e097d4c gui: fix crash on selecting "Mask values" in transaction view (Sebastian Falbesoner)
05e8874 guix: update signapple (fanquake)
deccc50 guix: Zip needs to include all files with time as SOURCE_DATE_EPOCH (Andrew Chow)
fe57abd test: add coverage for snapshot chainstate not matching AssumeUTXO parameters (pablomartin4btc)
b761a58 assumeutxo, blockstorage: prevent core dump on invalid hash (pablomartin4btc)
d3ebf6e [test] Test i2p private key constraints (Vasil Dimov)
1f11784 [net] Check i2p private key constraints (dergoegge)
6544ffa bugfix: Mark CNoDestination and PubKeyDestination constructor explicit (MarcoFalke)

Pull request description:

  Backports for v26.0rc2:
  * #28695
  * #28698
  * #28728
  * #28757
  * #28759
  * bitcoin-core/gui#774

ACKs for top commit:
  josibake:
    ACK e4e8479
  hebasto:
    re-ACK e4e8479, only a backport of bitcoin-core/gui#774 added since my [recent](#28754 (review)) review.
  TheCharlatan:
    Re-ACK e4e8479

Tree-SHA512: 4b95afd26b8bf91250cb883423de8b274cefa48dc474734f5900aeb756eee3a6c656116efcfa2caff3c250678c16b70cc6b7a5d840018dc7e2c1e8161622cd61
luke-jr pushed a commit to bitcoinknots/bitcoin that referenced this pull request Nov 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet