User protocol upgrade and js client lib. (#191)

* Unified js client lib for browser and nodejs.
* Client lib multiple connections support.
* Implemented server challenge response.
* Contract guid and version validation.
* Server key validation.
* User json message encoding improvements.
This commit is contained in:
Ravin Perera
2020-12-11 11:02:58 +05:30
committed by GitHub
parent b77a3fc924
commit f2ed9040c0
34 changed files with 1202 additions and 905 deletions

View File

@@ -29,41 +29,84 @@ namespace msg::usrmsg::json
* @param msg String reference to copy the generated json message string into.
* Message format:
* {
* "type": "handshake_challenge",
* "hp_version": "<hp protocol version>",
* "type": "user_challenge",
* "contract_id": "<contract id>",
* "challenge": "<hex challenge string>"
* "contract_version": "<contract version string>",
* "challenge": "<challenge string>"
* }
* @param challengehex String reference to copy the generated hex challenge string into.
* @param challenge_bytes String reference to copy the generated challenge bytes into.
*/
void create_user_challenge(std::vector<uint8_t> &msg, std::string &challengehex)
void create_user_challenge(std::vector<uint8_t> &msg, std::string &challenge)
{
// Use libsodium to generate the random challenge bytes.
unsigned char challenge_bytes[msg::usrmsg::CHALLENGE_LEN];
randombytes_buf(challenge_bytes, msg::usrmsg::CHALLENGE_LEN);
// We pass the hex challenge string separately to the caller even though
// we also include it in the challenge msg as well.
util::bin2hex(challengehex, challenge_bytes, msg::usrmsg::CHALLENGE_LEN);
std::string challenge_bytes;
crypto::random_bytes(challenge_bytes, msg::usrmsg::CHALLENGE_LEN);
util::bin2hex(challenge,
reinterpret_cast<const unsigned char *>(challenge_bytes.data()),
msg::usrmsg::CHALLENGE_LEN);
// Construct the challenge msg json.
// We do not use jasoncons library here in favour of performance because this is a simple json message.
// We do not use jsoncons library here in favour of performance because this is a simple json message.
// Since we know the rough size of the challenge message we reserve adequate amount for the holder.
// Only Hot Pocket version number is variable length.
msg.reserve(256);
msg += "{\"";
msg += msg::usrmsg::FLD_HP_VERSION;
msg += SEP_COLON;
msg += msg::usrmsg::USER_PROTOCOL_VERSION;
msg += SEP_COMMA;
msg += msg::usrmsg::FLD_TYPE;
msg += SEP_COLON;
msg += msg::usrmsg::MSGTYPE_HANDSHAKE_CHALLENGE;
msg += msg::usrmsg::MSGTYPE_USER_CHALLENGE;
msg += SEP_COMMA;
msg += msg::usrmsg::FLD_CONTRACT_ID;
msg += SEP_COLON;
msg += conf::cfg.contractid;
msg += SEP_COMMA;
msg += msg::usrmsg::FLD_CONTRACT_VERSION;
msg += SEP_COLON;
msg += conf::cfg.contractversion;
msg += SEP_COMMA;
msg += msg::usrmsg::FLD_CHALLENGE;
msg += SEP_COLON;
msg += challengehex;
msg += challenge;
msg += "\"}";
}
/**
* Constructs server challenge response message json. This gets sent when we receive
* a challenge from the user.
*
* @param msg String reference to copy the generated json message string into.
* Message format:
* {
* "type": "server_challenge_response",
* "sig": "<hex encoded signature of the [challenge + contract_id]>",
* "pubkey": "<our public key in hex>"
* }
* @param original_challenge Original challenge issued by the user.
*/
void create_server_challenge_response(std::vector<uint8_t> &msg, const std::string &original_challenge)
{
// Generate signature of challenge + contract id + contract version.
const std::string content = original_challenge + conf::cfg.contractid + conf::cfg.contractversion;
const std::string sig_hex = crypto::sign_hex(content, conf::cfg.seckeyhex);
// Since we know the rough size of the challenge message we reserve adequate amount for the holder.
msg.reserve(256);
msg += "{\"";
msg += msg::usrmsg::FLD_TYPE;
msg += SEP_COLON;
msg += msg::usrmsg::MSGTYPE_SERVER_CHALLENGE_RESPONSE;
msg += SEP_COMMA;
msg += msg::usrmsg::FLD_SIG;
msg += SEP_COLON;
msg += sig_hex;
msg += SEP_COMMA;
msg += msg::usrmsg::FLD_PUBKEY;
msg += SEP_COLON;
msg += conf::cfg.pubkeyhex;
msg += "\"}";
}
@@ -146,16 +189,33 @@ namespace msg::usrmsg::json
*/
void create_contract_read_response_container(std::vector<uint8_t> &msg, std::string_view content)
{
msg.reserve(256);
msg.reserve(content.size() + 256);
msg += "{\"";
msg += msg::usrmsg::FLD_TYPE;
msg += SEP_COLON;
msg += msg::usrmsg::MSGTYPE_CONTRACT_READ_RESPONSE;
msg += SEP_COMMA;
msg += msg::usrmsg::FLD_CONTENT;
msg += SEP_COLON;
msg += content;
msg += "\"}";
msg += SEP_COLON_NOQUOTE;
if (is_json_string(content))
{
// Process the final string using jsoncons.
jsoncons::json jstring = content;
jsoncons::json_options options;
options.escape_all_non_ascii(true);
std::string escaped_content;
jstring.dump(escaped_content);
msg += escaped_content;
}
else
{
msg += content;
}
msg += "}";
}
/**
@@ -172,7 +232,9 @@ namespace msg::usrmsg::json
*/
void create_contract_output_container(std::vector<uint8_t> &msg, std::string_view content, const uint64_t lcl_seq_no, std::string_view lcl)
{
msg.reserve(256);
const bool is_string = is_json_string(content);
msg.reserve(content.size() + 256);
msg += "{\"";
msg += msg::usrmsg::FLD_TYPE;
msg += SEP_COLON;
@@ -187,9 +249,26 @@ namespace msg::usrmsg::json
msg += std::to_string(lcl_seq_no);
msg += SEP_COMMA_NOQUOTE;
msg += msg::usrmsg::FLD_CONTENT;
msg += SEP_COLON;
msg += content;
msg += "\"}";
msg += SEP_COLON_NOQUOTE;
if (is_json_string(content))
{
// Process the final string using jsoncons.
jsoncons::json jstring = content;
jsoncons::json_options options;
options.escape_all_non_ascii(true);
std::string escaped_content;
jstring.dump(escaped_content);
msg += escaped_content;
}
else
{
msg += content;
}
msg += "}";
}
/**
@@ -198,80 +277,107 @@ namespace msg::usrmsg::json
*
* @param extracted_pubkeyhex The hex public key extracted from the response.
* @param extracted_protocol The protocol code extracted from the response.
* @param extracted_server_challenge Any server challenge issued by user.
* @param response The response bytes to verify. This will be parsed as json.
* Accepted response format:
* {
* "type": "handshake_response",
* "challenge": "<original hex challenge the user received>",
* "type": "user_challenge_response",
* "sig": "<hex signature of the challenge>",
* "pubkey": "<hex public key of the user>",
* "server_challenge": "<hex encoded challenge issued to server>", (max 16 bytes/32 chars)
* "protocol": "<json | bson>"
* }
* @param original_challenge The original hex challenge string issued to the user.
* @param original_challenge The original challenge string we issued to the user.
* @return 0 if challenge response is verified. -1 if challenge not met or an error occurs.
*/
int verify_user_handshake_response(std::string &extracted_pubkeyhex, std::string &extracted_protocol,
std::string_view response, std::string_view original_challenge)
int verify_user_challenge(std::string &extracted_pubkeyhex, std::string &extracted_protocol, std::string &extracted_server_challenge,
std::string_view response, std::string_view original_challenge)
{
jsoncons::json d;
if (parse_user_message(d, response) != 0)
return -1;
// Validate msg type.
if (d[msg::usrmsg::FLD_TYPE] != msg::usrmsg::MSGTYPE_HANDSHAKE_RESPONSE)
if (d[msg::usrmsg::FLD_TYPE] != msg::usrmsg::MSGTYPE_USER_CHALLENGE_RESPONSE)
{
LOG_DEBUG << "User handshake response type invalid. 'handshake_response' expected.";
return -1;
}
// Compare the response handshake string with the original issued challenge.
if (!d.contains(msg::usrmsg::FLD_CHALLENGE) || d[msg::usrmsg::FLD_CHALLENGE] != original_challenge.data())
{
LOG_DEBUG << "User handshake response 'challenge' invalid.";
LOG_DEBUG << "User challenge response type invalid. 'handshake_response' expected.";
return -1;
}
// Check for the 'sig' field existence.
if (!d.contains(msg::usrmsg::FLD_SIG) || !d[msg::usrmsg::FLD_SIG].is<std::string>())
{
LOG_DEBUG << "User handshake response 'challenge signature' invalid.";
LOG_DEBUG << "User challenge response 'challenge signature' invalid.";
return -1;
}
// Check for the 'pubkey' field existence.
if (!d.contains(msg::usrmsg::FLD_PUBKEY) || !d[msg::usrmsg::FLD_PUBKEY].is<std::string>())
{
LOG_DEBUG << "User handshake response 'public key' invalid.";
LOG_DEBUG << "User challenge response 'public key' invalid.";
return -1;
}
// Check for optional server challenge field existence and valid value.
if (d.contains(msg::usrmsg::FLD_SERVER_CHALLENGE))
{
bool server_challenge_valid = false;
if (d[msg::usrmsg::FLD_SERVER_CHALLENGE].is<std::string>())
{
std::string_view challenge = d[msg::usrmsg::FLD_SERVER_CHALLENGE].as<std::string_view>();
if (!challenge.empty() && challenge.size() <= 32)
{
server_challenge_valid = true;
extracted_server_challenge = challenge;
}
}
if (!server_challenge_valid)
{
LOG_DEBUG << "User challenge response 'server_challenge' invalid.";
return -1;
}
}
// Check for protocol field existence and valid value.
if (!d.contains(msg::usrmsg::FLD_PROTOCOL) || !d[msg::usrmsg::FLD_PROTOCOL].is<std::string>())
{
LOG_DEBUG << "User handshake response 'protocol' invalid.";
LOG_DEBUG << "User challenge response 'protocol' invalid.";
return -1;
}
std::string_view protocolsv = d[msg::usrmsg::FLD_PROTOCOL].as<std::string_view>();
if (protocolsv != "json" && protocolsv != "bson")
{
LOG_DEBUG << "User handshake response 'protocol' type invalid.";
LOG_DEBUG << "User challenge response 'protocol' type invalid.";
return -1;
}
// Verify the challenge signature. We do this last due to signature verification cost.
std::string_view pubkeysv = d[msg::usrmsg::FLD_PUBKEY].as<std::string_view>();
if (crypto::verify_hex(
original_challenge,
d[msg::usrmsg::FLD_SIG].as<std::string_view>(),
pubkeysv) != 0)
std::string_view pubkey_hex = d[msg::usrmsg::FLD_PUBKEY].as<std::string_view>();
std::string pubkey_bytes;
pubkey_bytes.resize(crypto::PFXD_PUBKEY_BYTES);
util::hex2bin(reinterpret_cast<unsigned char *>(pubkey_bytes.data()),
pubkey_bytes.size(),
pubkey_hex);
std::string_view sig_hex = d[msg::usrmsg::FLD_SIG].as<std::string_view>();
std::string sig_bytes;
sig_bytes.resize(sig_hex.size() / 2);
util::hex2bin(reinterpret_cast<unsigned char *>(sig_bytes.data()),
sig_bytes.size(),
sig_hex);
if (crypto::verify(original_challenge, sig_bytes, pubkey_bytes) != 0)
{
LOG_DEBUG << "User challenge response signature verification failed.";
return -1;
}
extracted_pubkeyhex = pubkeysv;
extracted_pubkeyhex = pubkey_hex;
extracted_protocol = protocolsv;
return 0;
@@ -436,4 +542,31 @@ namespace msg::usrmsg::json
return 0;
}
bool is_json_string(std::string_view content)
{
if (content.empty())
return true;
const char first = content[0];
const char last = content[content.size() - 1];
if ((first == '\"' && last == '\"') ||
(first == '{' && last == '}') ||
(first == '[' && last == ']') ||
content == "true" || content == "false")
return false;
// Check whether all characters are digits.
bool decimal_found = false;
for (const char c : content)
{
if ((c != '.' && (c < '0' || c > '9')) || (c == '.' && decimal_found)) // Not a number.
return true;
else if (c == '.') // There can only be one decimal in a proper number.
decimal_found = true;
}
return false; // Is a number.
}
} // namespace msg::usrmsg::json