diff --git a/bin/python/ripple/util/Sign.py b/bin/python/ripple/util/Sign.py index ab4adf023..10b324e7e 100644 --- a/bin/python/ripple/util/Sign.py +++ b/bin/python/ripple/util/Sign.py @@ -22,6 +22,9 @@ Usage: sign Create a new signed manifest with the given sequence number, validator public key, and master secret key. + + verify + Verify hex-encoded manifest signature with master public key. """ def prepend_length_byte(b): @@ -113,6 +116,16 @@ def get_signature(seq, validator_public_key_human, private_key_human): m1 = sign_manifest(m, private_key, pk) return base64.b64encode(m1) +def verify_signature(seq, validator_public_key_human, public_key_human, signature): + v, validator_public_key = Base58.decode_version(validator_public_key_human) + check_validator_public(v, validator_public_key) + + v, public_key = Base58.decode_version(public_key_human) + + m = make_manifest(public_key, validator_public_key, seq) + public_key = public_key[1:] # Remove ED25519_BYTE + sig = signature.decode('hex') + ed25519.checkvalid(sig, 'MAN\0' + m, public_key) # Testable versions of functions. def perform_create(urandom=os.urandom, print=print): @@ -131,6 +144,12 @@ def perform_sign( print(wrap(get_signature( int(seq), validator_public_key_human, private_key_human))) +def perform_verify( + seq, validator_public_key_human, public_key_human, signature, print=print): + verify_signature( + int(seq), validator_public_key_human, public_key_human, signature) + print('Signature valid for', public_key_human) + # Externally visible versions of functions. def create(): perform_create() @@ -141,6 +160,8 @@ def check(s): def sign(seq, validator_public_key_human, private_key_human): perform_sign(seq, validator_public_key_human, private_key_human) +def verify(seq, validator_public_key_human, public_key_human, signature): + perform_verify(seq, validator_public_key_human, public_key_human, signature) def usage(*errors): if errors: @@ -148,7 +169,7 @@ def usage(*errors): print(USAGE) return not errors -_COMMANDS = dict((f.__name__, f) for f in (create, check, sign)) +_COMMANDS = dict((f.__name__, f) for f in (create, check, sign, verify)) def run_command(args): if not args: diff --git a/bin/python/ripple/util/test_Sign.py b/bin/python/ripple/util/test_Sign.py index e44618b75..6708e761c 100644 --- a/bin/python/ripple/util/test_Sign.py +++ b/bin/python/ripple/util/test_Sign.py @@ -14,6 +14,10 @@ class test_Sign(TestCase): 'JAAAABdxIe2DIKUZd9jDjKikknxnDfWCHkSXYZReFenvsmoVCdIw6nMhAnZ2dnZ2' 'dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dkDOjlWtQSvRTjuwe+4iNusg0sJM' 'zqkBJwDz30b2SkxZ7Fte/Vx4htM/kkfUfJCaxmxE5N4dHSKuiO9iDHsktqIA') + VALIDATOR_KEY_HUMAN = 'n9JijuoCv8ubEy5ag3LiX3hyq27GaLJsitZPbQ6APkwx2MkUXq8E' + SIGNATURE_HEX = ( + '0a1546caa29c887f9fcb5e6143ea101b31fb5895a5cdfa24939301c66ff51794' + 'a0b729e0ebbf576f2cc7cdb9f68c2366324a53b8e1ecf16f3c17bebbdb8d7102') def setUp(self): self.results = [] @@ -96,6 +100,11 @@ class test_Sign(TestCase): 'dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dnZ2dkDOjlWtQSvRTjuwe+4iNusg0sJM' 'zqkBJwDz30b2SkxZ7Fte/Vx4htM/kkfUfJCaxmxE5N4dHSKuiO9iDHsktqIA') + def test_verify_signature(self): + Sign.verify_signature(self.SEQUENCE, self.VALIDATOR_KEY_HUMAN, + 'nHUUaKHpxyRP4TZZ79tTpXuTpoM8pRNs5crZpGVA5jdrjib5easY', + self.SIGNATURE_HEX) + def test_check(self): public = Base58.encode_version(Base58.VER_NODE_PRIVATE, 32 * 'k') Sign.perform_check(public, self.print) @@ -125,3 +134,8 @@ class test_Sign(TestCase): 'Z2dnZ2dkDOjlWtQSvRTjuwe+4iNusg0sJMzqkBJwDz30b2S\n' 'kxZ7Fte/Vx4htM/kkfUfJCaxmxE5N4dHSKuiO9iDHsktqIA'], {}]]) + + def test_verify(self): + Sign.perform_verify(self.SEQUENCE, self.VALIDATOR_KEY_HUMAN, + 'nHUUaKHpxyRP4TZZ79tTpXuTpoM8pRNs5crZpGVA5jdrjib5easY', + self.SIGNATURE_HEX, print=self.print) diff --git a/src/ripple/app/misc/NetworkOPs.cpp b/src/ripple/app/misc/NetworkOPs.cpp index c588ef222..53cae3b1b 100644 --- a/src/ripple/app/misc/NetworkOPs.cpp +++ b/src/ripple/app/misc/NetworkOPs.cpp @@ -436,6 +436,10 @@ public: bool subBook (InfoSub::ref ispListener, Book const&) override; bool unsubBook (std::uint64_t uListener, Book const&) override; + bool subManifests (InfoSub::ref ispListener) override; + bool unsubManifests (std::uint64_t uListener) override; + void pubManifest (Manifest const&) override; + bool subTransactions (InfoSub::ref ispListener) override; bool unsubTransactions (std::uint64_t uListener) override; @@ -526,6 +530,7 @@ private: subRpcMapType mRpcSubMap; SubMapType mSubLedger; // Accepted ledgers. + SubMapType mSubManifests; // Received validator manifests. SubMapType mSubServer; // When server changes connectivity state. SubMapType mSubTransactions; // All accepted transactions. SubMapType mSubRTTransactions; // All proposed and accepted transactions. @@ -1507,6 +1512,36 @@ void NetworkOPsImp::consensusViewChange () setMode (omCONNECTED); } +void NetworkOPsImp::pubManifest (Manifest const& mo) +{ + // VFALCO consider std::shared_mutex + ScopedLockType sl (mSubLock); + + if (!mSubManifests.empty ()) + { + Json::Value jvObj (Json::objectValue); + + jvObj [jss::type] = "manifestReceived"; + jvObj [jss::master_key] = toBase58(TokenType::TOKEN_NODE_PUBLIC, mo.masterKey); + jvObj [jss::signing_key] = toBase58(TokenType::TOKEN_NODE_PUBLIC, mo.signingKey); + jvObj [jss::seq] = Json::UInt (mo.sequence); + jvObj [jss::signature] = strHex (mo.getSignature ()); + + for (auto i = mSubManifests.begin (); i != mSubManifests.end (); ) + { + if (auto p = i->second.lock()) + { + p->send (jvObj, true); + ++i; + } + else + { + i = mSubManifests.erase (i); + } + } + } +} + void NetworkOPsImp::pubServer () { // VFALCO TODO Don't hold the lock across calls to send...make a copy of the @@ -1568,9 +1603,7 @@ void NetworkOPsImp::pubValidation (STValidation::ref val) for (auto i = mSubValidations.begin (); i != mSubValidations.end (); ) { - InfoSub::pointer p = i->second.lock (); - - if (p) + if (auto p = i->second.lock()) { p->send (jvObj, true); ++i; @@ -2538,6 +2571,20 @@ bool NetworkOPsImp::unsubLedger (std::uint64_t uSeq) return mSubLedger.erase (uSeq); } +// <-- bool: true=added, false=already there +bool NetworkOPsImp::subManifests (InfoSub::ref isrListener) +{ + ScopedLockType sl (mSubLock); + return mSubManifests.emplace (isrListener->getSeq (), isrListener).second; +} + +// <-- bool: true=erased, false=was not there +bool NetworkOPsImp::unsubManifests (std::uint64_t uSeq) +{ + ScopedLockType sl (mSubLock); + return mSubManifests.erase (uSeq); +} + // <-- bool: true=added, false=already there bool NetworkOPsImp::subServer (InfoSub::ref isrListener, Json::Value& jvResult, bool admin) diff --git a/src/ripple/net/InfoSub.h b/src/ripple/net/InfoSub.h index d661defe1..ee4a543bf 100644 --- a/src/ripple/net/InfoSub.h +++ b/src/ripple/net/InfoSub.h @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -84,6 +85,10 @@ public: virtual bool subLedger (ref ispListener, Json::Value& jvResult) = 0; virtual bool unsubLedger (std::uint64_t uListener) = 0; + virtual bool subManifests (ref ispListener) = 0; + virtual bool unsubManifests (std::uint64_t uListener) = 0; + virtual void pubManifest (Manifest const&) = 0; + virtual bool subServer (ref ispListener, Json::Value& jvResult, bool admin) = 0; virtual bool unsubServer (std::uint64_t uListener) = 0; diff --git a/src/ripple/net/impl/InfoSub.cpp b/src/ripple/net/impl/InfoSub.cpp index 73627a348..7da7666bd 100644 --- a/src/ripple/net/impl/InfoSub.cpp +++ b/src/ripple/net/impl/InfoSub.cpp @@ -56,6 +56,7 @@ InfoSub::~InfoSub () m_source.unsubTransactions (mSeq); m_source.unsubRTTransactions (mSeq); m_source.unsubLedger (mSeq); + m_source.unsubManifests (mSeq); m_source.unsubServer (mSeq); m_source.unsubValidations (mSeq); m_source.unsubPeerStatus (mSeq); diff --git a/src/ripple/overlay/impl/Manifest.cpp b/src/ripple/overlay/impl/Manifest.cpp index 05e833947..3d22bf73a 100644 --- a/src/ripple/overlay/impl/Manifest.cpp +++ b/src/ripple/overlay/impl/Manifest.cpp @@ -119,6 +119,14 @@ bool Manifest::revoked () const return sequence == std::numeric_limits::max (); } +Blob Manifest::getSignature () const +{ + STObject st (sfGeneric); + SerialIter sit (serialized.data (), serialized.size ()); + st.set (sit); + return st.getFieldVL (sfSignature); +} + void ManifestCache::configValidatorKey( std::string const& line, beast::Journal journal) diff --git a/src/ripple/overlay/impl/Manifest.h b/src/ripple/overlay/impl/Manifest.h index 2e5a39660..6bf17bcdd 100644 --- a/src/ripple/overlay/impl/Manifest.h +++ b/src/ripple/overlay/impl/Manifest.h @@ -98,6 +98,7 @@ struct Manifest bool verify () const; uint256 hash () const; bool revoked () const; + Blob getSignature () const; }; boost::optional make_Manifest(std::string s); diff --git a/src/ripple/overlay/impl/OverlayImpl.cpp b/src/ripple/overlay/impl/OverlayImpl.cpp index 3f86c272b..c26de0b55 100644 --- a/src/ripple/overlay/impl/OverlayImpl.cpp +++ b/src/ripple/overlay/impl/OverlayImpl.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -695,6 +696,10 @@ OverlayImpl::onManifests ( app_.validators(), journal); + if (result == ManifestDisposition::accepted || + result == ManifestDisposition::untrusted) + app_.getOPs().pubManifest (*make_Manifest(serialized)); + if (result == ManifestDisposition::accepted) { auto db = app_.getWalletDB ().checkoutDb (); diff --git a/src/ripple/overlay/tests/manifest_test.cpp b/src/ripple/overlay/tests/manifest_test.cpp index 188debb44..bbd429987 100644 --- a/src/ripple/overlay/tests/manifest_test.cpp +++ b/src/ripple/overlay/tests/manifest_test.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -93,9 +94,9 @@ public: auto const pk = derivePublicKey(type, sk); STObject st(sfGeneric); - set(st, sfSequence, seq); - set(st, sfPublicKey, pk); - set(st, sfSigningPubKey, spk); + st[sfSequence] = seq; + st[sfPublicKey] = pk; + st[sfSigningPubKey] = spk; sign(st, HashPrefix::manifest, type, sk); expect(verify(st, HashPrefix::manifest, pk, true)); @@ -179,6 +180,26 @@ public: boost::filesystem::path (dbName)); } + void testGetSignature() + { + testcase ("getSignature"); + auto const sk = randomSecretKey(); + auto const pk = derivePublicKey(KeyType::ed25519, sk); + auto const kp = randomKeyPair(KeyType::secp256k1); + auto const m = make_Manifest (KeyType::ed25519, sk, kp.first, 0); + + STObject st(sfGeneric); + st[sfSequence] = 0; + st[sfPublicKey] = pk; + st[sfSigningPubKey] = kp.first; + Serializer ss; + ss.add32(HashPrefix::manifest); + st.addWithoutSigningFields(ss); + auto const sig = sign(KeyType::ed25519, sk, ss.slice()); + + expect (strHex(sig) == strHex(m.getSignature())); + } + void run() override { @@ -227,6 +248,7 @@ public: expect (cache.applyManifest (clone (s_b2), *unl, journal) == invalid); } testLoadStore (cache, *unl); + testGetSignature (); } }; diff --git a/src/ripple/protocol/JsonFields.h b/src/ripple/protocol/JsonFields.h index 0e9ff6506..d090de833 100644 --- a/src/ripple/protocol/JsonFields.h +++ b/src/ripple/protocol/JsonFields.h @@ -341,6 +341,7 @@ JSS ( server_state ); // out: NetworkOPs JSS ( server_status ); // out: NetworkOPs JSS ( severity ); // in: LogLevel JSS ( signature ); // out: NetworkOPs +JSS ( signing_key ); // out: NetworkOPs JSS ( signer_list ); // in: AccountObjects JSS ( snapshot ); // in: Subscribe JSS ( source_account ); // in: PathRequest, RipplePathFind diff --git a/src/ripple/rpc/handlers/Subscribe.cpp b/src/ripple/rpc/handlers/Subscribe.cpp index 60dcfa735..887834836 100644 --- a/src/ripple/rpc/handlers/Subscribe.cpp +++ b/src/ripple/rpc/handlers/Subscribe.cpp @@ -124,6 +124,10 @@ Json::Value doSubscribe (RPC::Context& context) { context.netOps.subLedger (ispSub, jvResult); } + else if (streamName == "manifests") + { + context.netOps.subManifests (ispSub); + } else if (streamName == "transactions") { context.netOps.subTransactions (ispSub); diff --git a/src/ripple/rpc/handlers/Unsubscribe.cpp b/src/ripple/rpc/handlers/Unsubscribe.cpp index 2e3f894b3..b9b7543d3 100644 --- a/src/ripple/rpc/handlers/Unsubscribe.cpp +++ b/src/ripple/rpc/handlers/Unsubscribe.cpp @@ -77,6 +77,10 @@ Json::Value doUnsubscribe (RPC::Context& context) { context.netOps.unsubLedger (ispSub->getSeq ()); } + else if (streamName == "manifests") + { + context.netOps.unsubManifests (ispSub->getSeq ()); + } else if (streamName == "transactions") { context.netOps.unsubTransactions (ispSub->getSeq ()); diff --git a/test/jsonrpc-test.js b/test/jsonrpc-test.js index 4d7d40eb4..9c3a247c9 100644 --- a/test/jsonrpc-test.js +++ b/test/jsonrpc-test.js @@ -170,6 +170,21 @@ suite('JSON-RPC', function() { }); }); + test('subscribe manifests', function(done) { + var rippled_config = testutils.get_server_config(config); + var client = jsonrpc.client("http://" + rippled_config.rpc_ip + ":" + rippled_config.rpc_port); + var http_config = config.http_servers["zed"]; + + client.call('subscribe', [{ + 'url' : "http://" + http_config.ip + ":" + http_config.port, + 'streams' : [ 'manifests' ], + }], function (result) { + assert(typeof result === 'object'); + assert(result.status === 'success'); + done(); + }); + }); + test('subscribe validations', function(done) { var rippled_config = testutils.get_server_config(config); var client = jsonrpc.client("http://" + rippled_config.rpc_ip + ":" + rippled_config.rpc_port); @@ -179,7 +194,6 @@ suite('JSON-RPC', function() { 'url' : "http://" + http_config.ip + ":" + http_config.port, 'streams' : [ 'validations' ], }], function (result) { - // console.log(JSON.stringify(result, undefined, 2)); assert(typeof result === 'object'); assert(result.status === 'success'); done();