Several of the crypto projects I've been building (and now in the advanced stage of testing, yay!), use a unique cross-chain authentication system that validates and consolidates your identities over any number of crypto and non-crypto platforms. All that is required is a locally-stored copy of either your posting or memo key for signing transactions. I began describing how I initially accomplished this in a previous post.
That method worked beautifully at first, until EOS...
After EOS went live, I wanted to support it as well. Since EOS accounts initially only have owner and active keys, a "safer" posting or memo key wasn't available for login and account verification. As such, I decided the better route would be to incorporate the signing functionality of the EOS Scatter Wallet (the EOS equivalent of Ethereum's MetaMask).
However, I quickly realized that the EOS dev team updated the key and signing format. And while it's definitely a better choice in the long run, especially as more chains opt for 4 letter token codes (ie. EOS versus TLOS / Telos), it did force me to revisit my original signing and key management process. For reference, this is the new key and signing format used by EOS (while signatures now use the new format, the standard public/private key format is still supported).
new key formats for eos: ( https://github.com/EOSIO/eos/issues/2146 )
- Public keys will be represented as:
- EOS_<key_type_id>_<base_58_encoding_of_public_key_data>
- Private keys will be represented as:
- PRI_<key_type_id>_<base_58_encoding_of_private_key_data>
- Signatures will be represented as:
- SIG_<key_type_id>_<base_58_encoding_of_signature_data>
On my first go around, although I realized that graphene public and private keys were base58 encoded, it had somehow eluded me that the graphene signatures were base58 encoded as well. So instead, I was base64 encoding the signatures, as described from my previous post on this subject. Given my use case of authenticating messages signed in javascript and verified on a java-based server, it really didn't matter so long as both sides understood the format being used.
However, this all changed when I had to verify the base58 signatures returned by Scatter Wallet. After identifying the various format differences, I reworked the signing methods, and ever since I've been seamlessly signing and verifying EOS-style signatures over all graphene-based chains between javascript and java.
To save others the trouble of working this out on their own, here's the updated java / javascript for use in your own projects:
javascript:
// ============================= EOS signing =============================
/**
@arg {Buffer} keyBuffer data
@arg {string} keyType = sha256x2, K1, etc
@return {string} checksum encoded base58 string
*/
function checkEncode(keyBuffer, keyType = null) {
assert(Buffer.isBuffer(keyBuffer), 'expecting keyBuffer<Buffer>')
if(keyType === 'sha256x2') { // legacy
const checksum = hash.sha256(hash.sha256(keyBuffer)).slice(0, 4)
return base58.encode(Buffer.concat([keyBuffer, checksum]))
} else {
const check = [keyBuffer]
if(keyType) {
check.push(Buffer.from(keyType))
}
const checksum = hash.ripemd160(Buffer.concat(check)).slice(0, 4)
return base58.encode(Buffer.concat([keyBuffer, checksum]))
}
}
/**
@arg {Buffer} keyString data
@arg {string} keyType = sha256x2, K1, etc
@return {string} checksum encoded base58 string
*/
function checkDecode(keyString, keyType = null) {
assert(keyString != null, 'private key expected')
const buffer = new Buffer(base58.decode(keyString))
const checksum = buffer.slice(-4)
const key = buffer.slice(0, -4)
let newCheck
if(keyType === 'sha256x2') { // legacy
newCheck = hash.sha256(hash.sha256(key)).slice(0, 4) // WIF (legacy)
} else {
const check = [key]
if(keyType) {
check.push(Buffer.from(keyType))
}
newCheck = hash.ripemd160(Buffer.concat(check)).slice(0, 4) //PVT
}
if (checksum.toString() !== newCheck.toString()) {
throw new Error('Invalid checksum, ' +
`${checksum.toString('hex')} != ${newCheck.toString('hex')}`
)
}
return key
}
function signEosMessage(msg, privateWif) {
const sigObj = Signature.sign(msg, privateWif);
const sigBuf = sigObj.toBuffer();
return 'SIG_K1_' + checkEncode(sigBuf, 'K1');
}
function verifyEosMessage(msg, signature, publicKey) {
try {
const pubKeyObj = PublicKey.fromString(publicKey);
assert(typeof signature, 'string', 'signature');
const match = signature.match(/^SIG_([A-Za-z0-9]+)_([A-Za-z0-9]+)$/);
assert(match != null && match.length === 3, 'Expecting signature like: SIG_K1_bas58signature..');
const [, sigType, sigString] = match;
const sigObj = Signature.fromBuffer(checkDecode(sigString, sigType));
return sigObj.verifyBuffer(msg, pubKeyObj);
} catch (e) { return false; }
}
java:
public static String SignEosMessage(String message, eu.bittrade.crypto.core.ECKey privKey) {
Sha256Hash messageAsHash = Sha256Hash.of(message.getBytes());
ECKey.ECDSASignature sigObj = privKey.sign(messageAsHash);
byte[] sigData = new byte[69];
// first byte is header, defined as "int headerByte = recId + 27 + (isCompressed() ? 4 : 0);"
sigData[0] = (byte)31;
System.arraycopy(CryptoUtils.bigIntegerToBytes(sigObj.r, 32), 0, sigData, 1, 32);
System.arraycopy(CryptoUtils.bigIntegerToBytes(sigObj.s, 32), 0, sigData, 33, 32);
//append ripemd160 checksum
byte[] checksum = calculateChecksum(Bytes.concat(Arrays.copyOfRange(sigData, 0, 65),"K1".getBytes()));
System.arraycopy(checksum, 0, sigData, 65, 4);
return new String("SIG_K1_"+Base58.encode(sigData));
}
public static boolean VerifyEosMessage(String message, String sigBase58, PublicKey pubKey) {
try {
byte[] encodedSig;
String[] sig_arr = sigBase58.split("_");
encodedSig = Base58.decode(sig_arr[2]);
byte header = encodedSig[0];
BigInteger r = new BigInteger(1, Arrays.copyOfRange(encodedSig, 1, 33));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(encodedSig, 33, 65));
byte[] checksum = Arrays.copyOfRange(encodedSig, 65, 69);
//ripemd160 checksum
byte[] new_checksum = calculateChecksum(Bytes.concat(Arrays.copyOfRange(encodedSig, 0, 65),"K1".getBytes()));
if (!Arrays.equals(Arrays.copyOfRange(new_checksum,0, 4),checksum)) return false;
ECKey.ECDSASignature sigObj = new ECKey.ECDSASignature(r, s);
Sha256Hash messageAsHash = Sha256Hash.of(message.getBytes());
return ECKey.verify(messageAsHash.getBytes(), sigObj.encodeToDER(), pubKey.toByteArray());
} catch (Exception e) { return false; }
}
Scatter Wallet Arbitrary Signatures
This code demonstrates how to request an arbitrary signature from Scatter Wallet without requiring the user to directly provide their EOS private key:
try {
scatter.getArbitrarySignature(
chainPublicKey, // Scatter will match the pubkey to the linked EOS account
the_message_to_sign,
whatfor = 'Authentication or Whatever',
isHash = false
).then(function(result) {
scatter_signature = result; // got it! :D
});
} catch (e) { // signature authorization denied by user?! }
And for a quick example of how it looks in practice...
And for the final result........?
granted, this is still an EOS testnet, but nonetheless... successfully verified EOS identity! :D
I also added this code to my github crypto-playpen repo:
- GrapheneUtils.java contains all the Java classes
- steem-lib.js contains the javascript code, and is extracted from my fork of @ripplerm's terrific steem-lib library, which I also plan to release on github in the near future as well.
I really like what you are doing here mate. Good on ya!
thanks for the kind words Ryan! :)
I appreciate you and your work.
Congratulations! This post has been upvoted from the communal account, @minnowsupport, by alexpmorris from the Minnow Support Project. It's a witness project run by aggroed, ausbitbank, teamsteem, theprophet0, someguy123, neoxian, followbtcnews, and netuoso. The goal is to help Steemit grow by supporting Minnows. Please find us at the Peace, Abundance, and Liberty Network (PALnet) Discord Channel. It's a completely public and open space to all members of the Steemit community who voluntarily choose to be there.
If you would like to delegate to the Minnow Support Project you can do so by clicking on the following links: 50SP, 100SP, 250SP, 500SP, 1000SP, 5000SP.
Be sure to leave at least 50SP undelegated on your account.