Verifying a Steem user without leaking the private keysteemCreated with Sketch.

How to verify a Steem user without their private key leaving the browser

Gemini-Security_Public_and_Private_keys_What_are_Public_and_Private_keys.png

Image source

To help out my fellow devs, I just wanted to share a technique we're using in the soon-to-be-launched Pennsif.witness multisig tool.

One of our requirements is that we need to be able to tell whether a Steem user is who they claim to be (ie, whether they can prove that they have a private key for the Steem account they are identifying as).

An absolute constraint is that we must never allow a private key to leave the server.

Our solution is to create a dummy zero-value transaction that we won't bother broadcasting to the block chain, and have the user sign it with their Private Active Key.

This signed transaction is then sent to a verification server. The server does not need the user's private key; instead it can easily use the verify_authority API call.

This call checks whether a transaction is correctly signed (ie, signed with the sender's key).

Example code for this is shown below.

Example Clientside Code

const authAccount = "rexthetech"; // Whoever we're authenticating

// Check tip for proposal ref block
const props = await client.database.getDynamicGlobalProperties();
const ref_block_num = props.head_block_number & 0xFFFF;
const ref_block_prefix = Buffer.from(props.head_block_id, 'hex').readUInt32LE(4);

// Calc expiry time
const expireTime = 1000 * 3590; // (ms) max is 1 hour, but allow for chain timing variance and go 10 secs under 
//const expiration = new Date(Date.now() + expireTime).toISOString().slice(0, -5);

const now = new Date();
const future = new Date(now.getTime() + expireTime);
const year = future.getUTCFullYear();
const month = ('0' + (future.getUTCMonth() + 1)).slice(-2);
const day = ('0' + future.getUTCDate()).slice(-2);
const hours = ('0' + future.getUTCHours()).slice(-2);
const minutes = ('0' + future.getUTCMinutes()).slice(-2);
const seconds = ('0' + future.getUTCSeconds()).slice(-2);
const expiration = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;

// Build auth tx: Just get them to sign some random bytes
const randomBytes = Math.random().toString(36).substring(2);
const authOperations = [[
  'custom_json', 
  {   
    'required_auths': [], 
    'required_posting_auths': [authAccount], 
    'id': 'authenticate',
    'json': JSON.stringify({'random_bytes': randomBytes})
  }
]];

const authTx = {
  expiration,
  extensions,
  'operations': authOperations,
  ref_block_num,
  ref_block_prefix
};

// Sign auth TX with user's PK (this example uses Active Key)
const signedAuthTx = client.broadcast.sign(authTx, PrivateKey.from(USER_ACTIVE_KEY));
const serialisedTx = {
  'authTx': signedAuthTx,
  'randomBytes': randomBytes,
  'signedBy': authAccount
};

// JSONify
const jsonTx = JSON.stringify(serialisedTx);

// (Now post jsonTx to your server)

In this case, we get the user to sign some random bytes (to help prevent replay attacks). Note that authTx, randomBytes and signedBy are all passed to the server.

Example Serverside Code

On the server, we can verify that the transaction is correctly signed, and by who it claims to be signed by.

// Grab explicit fields
const authTx = req.body.authTx;
const randomBytes = req.body.randomBytes;
const signedBy = req.body.signedBy;

const expiration = authTx.expiration;

// Check auth TX: Signer is really the same account as originator?
if (signedBy != authTx.operations[0][1].required_posting_auths[0]) {
    console.error('!! Bad authority in auth TX');
}
  
// Check auth TX: Signed message body is correct?
if (randomBytes != JSON.parse(authTx.operations[0][1].json).random_bytes) {
    console.error('!! Body mismatch in auth TX');
}

// Check signature
try {
    const authOK = await client.database.verifyAuthority(authTx);
} catch (err) {
    console.error('!! Auth transaction is incorrectly signed');
}

Hope this helps!

Sort:  

Figuring out how to verify user identity is a roadblock that contributed to me losing momentum on one of the projects I was working on. I haven't done any security or encryption stuff before so it took me a little bit to wrap my head around it. The last part to click into place for me was that you want freshly-signed transactions based on some information from the server so you can confirm the user is trying to do the thing right now, like with your random bytes. Since people don't need to keep their old signed transactions private seeing an old signature or a signature on some unrelated transaction doesn't prove the person you're talking to has the key, only a freshly signed transaction with contents specific to the present moment does that. Unfortunately my motivation for the project got too low before I figured that part out so my progress stalled.

Thanks for sharing! !thumbup

Thank you, friend!
I'm @steem.history, who is steem witness.
Thank you for witnessvoting for me.
image.png
please click it!
image.png
(Go to https://steemit.com/~witnesses and type fbslo at the bottom of the page)

The weight is reduced because of the lack of Voting Power. If you vote for me as a witness, you can get my little vote.

Hello @rexthetech! You are Terrific!


command: !thumbup is powered by witness @justyy and his contributions are: https://steemyy.com
More commands are coming!

Thanks for the valuable blog