Signing messages in Javascript and Verifying in Python

in steemdev •  6 years ago 

developing

Moving away from SteemConnect

Via SteemConnect a website can give users access to the Steem blockchain without having to handle private keys. With both SteemConnect and Steemit the keys stay in the browser, but there are levels of power keys have. The active key can drain all of your funds out of your Steem wallet. Even if SteemConnect and Steemit are super trusted sites, it seems conceivable that there could be an exploit that could be used to steal the keys from these trusted websites. Certainly there is the cross-site scripting attack which is one vector that has been used on Steemit.com itself in the past.

So, after SteemConnect v1 was retired, I was faced with the choice of using the v2 SteemConnect that always requires the active key of the user or have Steemfiles handle some lower-valued key itself.

The posting key cannot be used to steal funds. It would just not be honoured by the protocol if a site tried to steal with that key. The memo key really has no power at all. It can be used to encrypt and decrypting memos in transfers but these are normally not encrypted anyway.

Sending Private keys over HTTPS

Probably a bad practice, but in the list of functions in the APIs there are no routines for just signing and verifying messages. If I need to verify whether a user has a private key, it seemed I needed to convert the private key to a public key and then check the account information if this public key is in the published list for posting by that user.

In Javascript, I can do that, but the problem is, the server must not trust the browser that the private key is legitimate. If Steemfiles did, then anyone would be able to pretend to be you and upload something distasteful in your name. So, this has to be done on the server. You can, send the private key over HTTPS you can verify the private keys as belonging to the owner keys and discard them.

Without boring you this is how it is working right now:

    # Routine returns 'owner'   if passed the master password or private owner key
    #                 'active'  if passed the private active key
    #                 'posting' if passed the private posting key
    #                 'memo'    if passed the private memo key
    #              or None otherwise
    
    def get_login(connection, username, password):
        pk = None
        try:
            Pk = PrivateKey(password)      
            pk = Pk.pubkey
        except Exception as e:
            # Perhaps not a private key
            Pk = PasswordKey(username, password, role="owner")
            pk = Pk.get_public()
        if testnet:
            spk = format(pk, "STX")
        else:
            spk = format(pk, "STM")
        s = steem.steem.Steem(nodes=get_node_list(connection))
        account_information = s.get_account(username)
        for role in ['owner', 'active', 'posting', 'memo']:
            try:
                for key_power_pair in account_information[role]['key_auths']:
                    if spk == key_power_pair[0]:
                        return role
            except:
                pass
        return None

Theoretically, this works fine but the user cannot see what source code is being run on the server and even if it were open source they couldn't verify the open source code was actually being used. Why should they trust me? I could be storing the posting key and the user could change the said key. So, it is better practice to not send keys to the server.

Implementing Signing messages in Javascript and Verifying in Python

As said there is no "sign message" call in the API. The custom-json operation allows us to create custom strings for signing. So, this is what I have worked out for now.

    let possible_keys = steem.auth.getPrivateKeys($scope.username, $scope.private_key, ["posting"]);
    let posting_key = possible_keys['posting'];
    console.log(posting_key);
    steem.api.getDynamicGlobalProperties(function(err, globals) {
            var http_data;
            let head_block_number = globals["head_block_number"] & 0xFFFF;
            let ref_block_prefix  = parseInt(globals["head_block_id"].substring(8,16), 16);
            let now               = new Date();
            let expiration        = new Date(now.getTime() + 10000);
            let custom_account_association_transaction = {
            'ref_block_num': head_block_number, 
            'ref_block_prefix': ref_block_prefix, 
            'expiration': expiration.toISOString().split('.')[0],
            'operations': [
                ['custom_json', 
                        {   
                            'required_auths': [], 
                            'required_posting_auths': [$scope.username], 
                            'id': 'authenticate',
                            'json': JSON.stringify({
                                    session_id: getCookie('session'),
                                    site: 'www.steemfiles.com',
                                    username: $scope.username                      
                            })
                        }
                    ]
            ],
            'extensions': [], 
            'signatures': []
            };                        
            
            steem.auth.signTransaction(custom_account_association_transaction,  [posting_key]);
            var signatures = custom_account_association_transaction['signatures'];
            // Convert signature into a format that pysteem uses.
            for (i = 0; i < signatures.length; ++i) {
                var signature = signatures[i];
                var s = '';
                var h;
                for (j = 0; j < signature.length; ++j) {
                    h = signature[j].toString(16);
                    if (h.length < 2) {
                        h = '0' + h;
                    }
                    s += h;
                }
                signature = s;
                signatures[i] = s;
            }
            custom_account_association_transaction['signatures'] = signatures;                        
            http_data = 'username=' + encodeURIComponent($scope.username) + '&' +  'session=' + encodeURIComponent(getCookie('session')) + '&association_transaction=' + encodeURIComponent(JSON.stringify(custom_account_association_transaction));           
            
    });

Now, from Javascript, you only need to send http_data to the server (via $http in angular for example) and you have a proof, of possession of the private key for that user. So, with that the server can mark the user as logged in.

The server gets a CGI call and needs to verify the transaction as correctly signed. The transaction never goes to the blockchain, it doesn't need to. We are just borrowing the blockchain's authentication method without actually publishing anything to the blockchain. One needs to verify the expiration date has not expired on the server, and verify the username is right, and it is signed by the correct key.

On the server, the transaction sent doesn't include the private key but the signed message proves the browser has a private key and we can be confident that username is really the one uploading files to Steemfiles.

Credit goes to @xeroc for publishing how to do this originally in python-steem:

    steem_ = steem.steem.Steem(nodes=get_node_list(connection))
    account_information = steem_.get_account(username)
    if association_transaction != None and association_transaction != "":
        # Pulled from this article by @xeroc : https://steemit.com/steem/@xeroc/steem-transaction-signing-in-a-nutshell
        from steembase import transactions
        from copy import copy
        tx1 = transactions.SignedTransaction(association_transaction)
        tx1.deriveDigest("STEEM")
        
        for role in ['owner', 'active', 'posting']:
            for key_power_pair in account_information[role]['key_auths']:
                tx2 = copy(tx1)
                pubkeys = []

                if testnet:
                    pubkeys.append(PublicKey(key_power_pair[0], prefix='STX'))
                else:
                    pubkeys.append(PublicKey(key_power_pair[0], prefix='STM'))
                        
                try:
                    tx2.verify(pubkeys, "STEEM")
                    answer_json = {"isAuth": True, "isSteemConnect": None, "reason": "authenticated with signature", "username": username, "session": session.id}
                    session.steemit_id = username
                    session.logged_in  = 1
                    session.update()
                except Exception as e:
                    answer_json = {"isAuth": False, "isSteemConnect": None, "reason": ' '.join(e.args), "session": session.id}
                else:
                    break
            if answer_json['isAuth']:
                break

Dash XjzjT4mr4f7T3E8G9jQQzozTgA2J1ehMkV
LTC LLXj1ZPQPaBA1LFtoU1Gkvu5ZrxYzeLGKt
BitcoinCash 1KVqnW7wZwn2cWbrXmSxsrzqYVC5Wj836u
Bitcoin 1Q1WX5gVPKxJKoQXF6pNNZmstWLR87ityw (too expensive to use for tips)

See also

Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!