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
Conversation
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers. Code CoverageFor detailed information about the code coverage, see the test coverage report. ReviewsSee the guideline for information on the review process.
If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update. |
There was a problem hiding this 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.
1e6d1db
to
9ff405e
Compare
This should probably be in the 26.0 milestone? |
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? |
typo-nit: |
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:
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. |
Ah, correction, OP is a bitcoin crash not a i2pd crash, apologies to @orignal |
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 |
There was a problem hiding this 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 edit: I think vasild's approach is betterBinary
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)
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.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Concept ACK
Co-authored-by: Vasil Dimov <vd@FreeBSD.org>
9ff405e
to
adb5d6b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK adb5d6b
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK adb5d6b
Concept ACK |
Co-authored-by: Vasil Dimov <vd@FreeBSD.org> Github-Pull: bitcoin#28695 Rebased-From: cf70a8d
Github-Pull: bitcoin#28695 Rebased-From: adb5d6b
adb5d6b
to
5cf4d26
Compare
code lgtm ACK 5cf4d26 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
re-ACK 5cf4d26
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ACK 5cf4d26
Co-authored-by: Vasil Dimov <vd@FreeBSD.org> GitHub-Pull: bitcoin#28695 Rebased-From: cf70a8d
Github-Pull: bitcoin#28695 Rebased-From: 5cf4d26
Backported to 26.x in #28754. |
Co-authored-by: Vasil Dimov <vd@FreeBSD.org> GitHub-Pull: bitcoin#28695 Rebased-From: cf70a8d
Github-Pull: bitcoin#28695 Rebased-From: 5cf4d26
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
Github-Pull: bitcoin#28695 Rebased-From: 5cf4d26
Not sanity checking can lead to crashes or worse: