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

Add `sign_message` command to `cli_wallet` #1878

Merged
merged 8 commits into from Aug 7, 2019
@@ -267,6 +267,22 @@ struct vesting_balance_object_with_info : public vesting_balance_object
fc::time_point_sec allowed_withdraw_time;
};

struct signed_message_meta {
string account;
public_key_type memo_key;
uint32_t block;
int64_t time;
};

class signed_message {
public:
string message;
signed_message_meta meta;
fc::optional<fc::ecc::compact_signature> signature;

fc::sha256 digest()const;
};

namespace detail {
class wallet_api_impl;
}
@@ -1055,6 +1071,40 @@ class wallet_api
string read_memo(const memo_data& memo);


/** Sign a message using an account's memo key. The signature is generated as in
* in https://github.com/xeroc/python-graphenelib/blob/d9634d74273ebacc92555499eca7c444217ecba0/graphenecommon/message.py#L64 .
*
* @param signer the name or id of signing account
* @param message text to sign
* @return the signed message in an abstract format
*/
signed_message sign_message(string signer, string message);

/** Verify a message signed with sign_message using the given account's memo key.
*
* @param message the message text
* @param account the account name of the message
* @param block the block number of the message
* @param time the timestamp of the message
* @param sig the message signature
* @return true if signature matches
*/
bool verify_message( string message, string account, int block, int time, compact_signature sig );

/** Verify a message signed with sign_message
*
* @param message the signed_message structure containing message, meta data and signature
* @return true if signature matches
*/
bool verify_signed_message( signed_message message );

/** Verify a message signed with sign_message, in its encapsulated form.
*
* @param message the complete encapsulated message string including separators and line feeds
* @return true if signature matches
*/
bool verify_encapsulated_message( string message );

/** These methods are used for stealth transfers */
///@{
/**
@@ -2034,6 +2084,9 @@ FC_REFLECT(graphene::wallet::operation_detail_ex,
FC_REFLECT( graphene::wallet::account_history_operation_detail,
(total_count)(result_count)(details))

FC_REFLECT( graphene::wallet::signed_message_meta, (account)(memo_key)(block)(time) )
FC_REFLECT( graphene::wallet::signed_message, (message)(meta)(signature) )

FC_API( graphene::wallet::wallet_api,
(help)
(gethelp)
@@ -2151,6 +2204,10 @@ FC_API( graphene::wallet::wallet_api,
(network_get_connected_peers)
(sign_memo)
(read_memo)
(sign_message)
(verify_message)
(verify_signed_message)
(verify_encapsulated_message)
(set_key_label)
(get_key_label)
(get_public_key)
@@ -107,6 +107,11 @@ using std::endl;

namespace detail {

static const string ENC_HEADER( "-----BEGIN BITSHARES SIGNED MESSAGE-----\n" );
static const string ENC_META( "-----BEGIN META-----\n" );
static const string ENC_SIG( "-----BEGIN SIGNATURE-----\n" );
static const string ENC_FOOTER( "-----END BITSHARES SIGNED MESSAGE-----" );

struct operation_result_printer
{
public:
@@ -297,6 +302,26 @@ class htlc_hash_to_string_visitor
}
};

/* meta contains lines of the form "key=value".
* Returns the value for the corresponding key, throws if key is not present. */
static string meta_extract( const string& meta, const string& key )
{
FC_ASSERT( meta.size() > key.size(), "Key '${k}' not found!", ("k",key) );
size_t start;
if( meta.substr( 0, key.size() ) == key && meta[key.size()] == '=' )
start = 0;
else
{
start = meta.find( "\n" + key + "=" );
FC_ASSERT( start != string::npos, "Key '${k}' not found!", ("k",key) );
++start;
}
start += key.size() + 1;
size_t lf = meta.find( "\n", start );
if( lf == string::npos ) lf = meta.size();
return meta.substr( start, lf - start );
}

class wallet_api_impl
{
public:
@@ -2244,6 +2269,84 @@ class wallet_api_impl
return clear_text;
}

signed_message sign_message(string signer, string message)
{
FC_ASSERT( !self.is_locked() );

const account_object from_account = get_account(signer);

signed_message msg;
msg.message = message;
msg.meta.account = from_account.name;
msg.meta.memo_key = from_account.options.memo_key;
msg.meta.block = 0;
msg.meta.time = 0;
This conversation was marked as resolved by abitmore

This comment has been minimized.

Copy link
@abitmore

abitmore Aug 3, 2019

Member

Why not use head_block_num and the timestamp?

This comment has been minimized.

Copy link
@pmconrad

pmconrad Aug 5, 2019

Author Contributor

Thanks. Had planned to add it after getting the proof-of-concept to work, then forgot. :-/

msg.signature = get_private_key( from_account.options.memo_key ).sign_compact( msg.digest() );
return msg;
}

bool verify_message( const string& message, const string& account, int block, int time, const compact_signature& sig )
{
const account_object from_account = get_account( account );

signed_message msg;
msg.message = message;
msg.meta.account = from_account.name;
msg.meta.memo_key = from_account.options.memo_key;
msg.meta.block = block;
msg.meta.time = time;
msg.signature = sig;

return verify_signed_message( msg );
}

bool verify_signed_message( const signed_message& message )
{
if( !message.signature.valid() ) return false;

const account_object from_account = get_account( message.meta.account );

const public_key signer( *message.signature, message.digest() );
if( !( message.meta.memo_key == signer ) ) return false;
FC_ASSERT( from_account.options.memo_key == signer,
"Message was signed by contained key, but it doesn't belong to the contained account!" );

return true;
}

/** Verify a message signed with sign_message, in its encapsulated form.
*
* @param message the complete encapsulated message string including separators and line feeds
* @return true if signature matches
*/
bool verify_encapsulated_message( const string& message )
{
signed_message msg;
size_t begin_p = message.find( ENC_HEADER );
FC_ASSERT( begin_p != string::npos, "BEGIN MESSAGE line not found!" );
size_t meta_p = message.find( ENC_META, begin_p );
FC_ASSERT( meta_p != string::npos, "BEGIN META line not found!" );
FC_ASSERT( meta_p >= begin_p + ENC_HEADER.size() + 1, "Missing message!?" );
size_t sig_p = message.find( ENC_SIG, meta_p );
FC_ASSERT( sig_p != string::npos, "BEGIN SIGNATURE line not found!" );
FC_ASSERT( sig_p >= meta_p + ENC_META.size(), "Missing metadata?!" );
size_t end_p = message.find( ENC_FOOTER, meta_p );
FC_ASSERT( end_p != string::npos, "END MESSAGE line not found!" );
FC_ASSERT( end_p >= sig_p + ENC_SIG.size() + 1, "Missing signature?!" );

msg.message = message.substr( begin_p + ENC_HEADER.size(), meta_p - begin_p - ENC_HEADER.size() - 1 );
const string meta = message.substr( meta_p + ENC_META.size(), sig_p - meta_p - ENC_META.size() );
const string sig = message.substr( sig_p + ENC_SIG.size(), end_p - sig_p - ENC_SIG.size() - 1 );

msg.meta.account = meta_extract( meta, "account" );
msg.meta.memo_key = public_key_type( meta_extract( meta, "memokey" ) );
msg.meta.block = boost::lexical_cast<uint32_t>( meta_extract( meta, "block" ) );
msg.meta.time = boost::lexical_cast<uint64_t>( meta_extract( meta, "timestamp" ) );
This conversation was marked as resolved by abitmore

This comment has been minimized.

Copy link
@abitmore

abitmore Aug 3, 2019

Member

This can fail, because specification (precision and format) of timestamp is not well-defined.

This comment has been minimized.

Copy link
@pmconrad

pmconrad Aug 5, 2019

Author Contributor

Switched "time" field to string.

msg.signature = variant(sig).as< fc::ecc::compact_signature >( 5 );

return verify_signed_message( msg );
}

signed_transaction sell_asset(string seller_account,
string amount_to_sell,
string symbol_to_sell,
@@ -2637,6 +2740,25 @@ class wallet_api_impl
return ss.str();
};

m["sign_message"] = [this](variant result, const fc::variants& a)
{
auto r = result.as<signed_message>( GRAPHENE_MAX_NESTED_OBJECTS );

fc::stringstream encapsulated;
encapsulated << ENC_HEADER;
encapsulated << r.message << '\n';
encapsulated << ENC_META;
encapsulated << "account=" << r.meta.account << '\n';
encapsulated << "memokey=" << std::string( r.meta.memo_key ) << '\n';
encapsulated << "block=" << r.meta.block << '\n';
encapsulated << "timestamp=" << r.meta.time << '\n';
encapsulated << ENC_SIG;
encapsulated << fc::to_hex( (const char*)r.signature->data, r.signature->size() ) << '\n';
encapsulated << ENC_FOOTER;

return encapsulated.str();
};

return m;
}

@@ -3136,6 +3258,17 @@ std::string operation_result_printer::operator()(const asset& a)
}}}

namespace graphene { namespace wallet {
fc::sha256 signed_message::digest()const
{
fc::stringstream to_sign;
to_sign << message << '\n';
to_sign << "account=" << meta.account << '\n';
to_sign << "memokey=" << std::string( meta.memo_key ) << '\n';
to_sign << "block=" << meta.block << '\n';
to_sign << "timestamp=" << meta.time;

return fc::sha256::hash( to_sign.str() );
}
vector<brain_key_info> utility::derive_owner_keys_from_brain_key(string brain_key, int number_of_desired_keys)
{
// Safety-check
@@ -4480,6 +4613,38 @@ string wallet_api::read_memo(const memo_data& memo)
return my->read_memo(memo);
}

signed_message wallet_api::sign_message(string signer, string message)
{
FC_ASSERT(!is_locked());
return my->sign_message(signer, message);
}

bool wallet_api::verify_message( string message, string account, int block, int time, compact_signature sig )
{
return my->verify_message( message, account, block, time, sig );
}

/** Verify a message signed with sign_message
*
* @param message the signed_message structure containing message, meta data and signature
* @return true if signature matches
*/
This conversation was marked as resolved by abitmore

This comment has been minimized.

Copy link
@abitmore

abitmore Aug 3, 2019

Member

Duplicating docs may lead to inconsistent docs.

This comment has been minimized.

Copy link
@pmconrad

pmconrad Aug 5, 2019

Author Contributor

Removed.

bool wallet_api::verify_signed_message( signed_message message )
{
return my->verify_signed_message( message );
}

/** Verify a message signed with sign_message, in its encapsulated form.
*
* @param message the complete encapsulated message string including separators and line feeds
* @return true if signature matches
*/
bool wallet_api::verify_encapsulated_message( string message )
{
return my->verify_encapsulated_message( message );
}


string wallet_api::get_key_label( public_key_type key )const
{
auto key_itr = my->_wallet.labeled_keys.get<by_key>().find(key);
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.