Transcription / Sketch of Bitcoin 2FA TOTP Authenticated Transactions

This idea does not involve a token, requires no funding / fundraising and should not mandate a change in the versioning of the protocol or transactions at all.


11 min read
Transcription / Sketch of Bitcoin 2FA TOTP Authenticated Transactions

The 'Abstract', motivations, breakdown & 'laymen's term' description of what this is, is in another document that is still a work-in-progress.

Please bear with me as I continue to craft it.

If you have any suggestions, please shoot me an e-mail (you can find it on our Telegram channel ; "@librehash").

Brief Explainer / Foreword (so you know what it is that's being sketched here)

[note: This document uses the pronoun, 'we', although I am the only author working on this solution at this current moment. This is purposeful as I want the greater blockchain community to see this directive is not a self-motivated one or a 'closed' / 'private' & personal endeavor where individuals can't contribute ]

Intro

This idea does not involve a token, requires no funding / fundraising and should not mandate a change in the versioning of the protocol or transactions at all.

Methodology Used to Formulate This Solution

To be frank - it took a pen and paper, several tabs open to various portions of the Bitcoin documentation, a Bitcoin IDE, and some trial and error.

Specifically, we used P2SH addresses  because of the flexibility that they have (and because the second-factor will require another signature).

Challenges

Bitcoin is stateless and TOTP is anything but. TOTP (think - Google Authenticator) relies on a ticking nonce that increases by intervals from the time that the 'secret' (QR Code) is entered into whatever application is being used.

How that secret is hashed is meaningful for the server side. The root formula for deriving the 6-digit code, however, is fairly straightforward.

The challenge with Bitcoin though is in creating a reflexive (adaptable) transaction without changing its hash at all.

We can't predict the future and there is no telling when someone will decide to spend their unspent outputs. In addition, with a P2SH script, the sender establishes the conditions that must be fulfilled via a 'redeem script'. That script, in addition to the transaction hash, contains op_codes that perform fixed operations on the input given by anyone attempting to spend said transaction.

If those operations do not resolve to a value that the stack processes as 'true', then the transaction (in 99% of cases) will be considered invalid and thus, null (not spent ; i.e., the funds don't move).

Normally, this is a background process when users are sending funds because the condition is almost always a provided signature & ECDSA public key pair that, when hashed, provides the requisite output (proving that attempted 'spender' can spend said funds).

New Bitcoin Innovation: 2FA-enabled (TOTP Transactions) [part 2]

Brief Breakdown

In order to implement this solution properly, we needed to start with P2SH transactions.

This transaction also needs to be specially crafted.

Some of the rarely used op codes that we created for our redeem script include:

-CHECKLOCKTIMEVERIFY
-OP_SUB1
-OP_HASH160
-(if, then conditions)

Among others. The first two (checklocktimeverify + op_sub1) are ran on the submitted expiry time value (which is measured against the block height in this case, not the UNIX time).

If the expiry time is at least at the height of the chain at the time that it is submitted, then the expiry time is valid.

However, if this 2FA TOTP authentication is to work, then we need to ensure that an arbitrary time is not being selected either.

That's where the 'OP_IF', 'OP_ELSE' condition statements come in.

By lining up these conditions on the stack to generate a condition that declares that our submitted block height value must pass the CHECKLOCKTIMEVERY (with a 'true'), yet render a 'false' when its value is subtracted by '1' (with the 'OP_SUB1' operation), we can essentially validate that whatever number was submitted must be accurate to the block height at the time that the operations were performed.

New Bitcoin Innovation: 2FA-enabled (TOTP Transactions) [part 3]

Where it Gets Quirky

Using literal UNIX time would be the best case scenario - as this is what the TOTP uses (generally a 'nonce' factor for your TOTP secret code increases by '1' every time a 30 second period transpires).

This might sound confusing, because end users of TOTP apps (like Google Authenticator), do not see anything related to a 'nonce'.

How Authenticator Apps Work

Have you ever wondered how different web clients can authenticate your 2FA code despite the fact that its time based and derived from a hash based algorithm designed to generate a virtually random 'secret key'?

After all - if the numbers are always 'turning over' every 30 seconds - how do they get in sync with you?

Answer = Nonce Values

See the GIF below first:

Notice how repeated clicks on the 'get new code' button increases the nonce counter.

If we hadn't pressed that button, the nonce counter would increase every 30 seconds.

How Servers Sync

Most (all virtually) apps / login solutions integrated with 2FA (TOTP specifically) - use UNIX time as their international, unilaterally agreed upon means of tracking time (as most computing systems do).

Since this is a default agreed upon standard (and a specified one via RFC) - web servers simply mark the time at which they send you your 2FA code (when you request it).

From that point, they track these 6-digit values that have been produced.

When you eventually put the code in your app (or scan the QR image), you'll start receiving these numbers as well.

In order to enable it on your account - you will be required to tell the service what your 6 digit (or 8) code is at the time of you pressing the enter button.

To verify, servers will either:

A) Quickly calculate the 30-second intervals that have transpired since your initial request & calculate the correspondng 6-digit auth code at that nonce 'height' (hmm... "height"? ; see where we're going here). If your value matches what they compute you should have - you're good to go.

OR

B) Servers will entrust that you aren't screwing around and will simply place a limit on how much time you have to produce the code. Since there are 6 numbers (or 8) produced virtually at random, the chances of two identical codes being produced within a given time frame because infintisemal. So the server will simply calculate the expected values X nonce periods ahead of time when they send you the code. When you submit a code back, they will (a) first check you've provided one that matches one of their computed values, then (b) if it does match, they'll see what nonce counter your submitted value was at and adjust their 2FA to match that nonce so that it remains 'in-sync' from that point onward.

Both solutions above work. Obviously, the aformentioned one is exponentially more secure - but both can be useful in certain unique situations.

Bitcoin's Combination of UNIX-time Tracking + Targeted Block Heights That Increase Incrementally Provide a VERY Similar Framework Here

However, since transactions are stateless as we mentioned before, there is no 'server' or central time tracker that has accounted for this 2FA setup that we have.

No matter though - there's a work around to this.

After Validating the Block Height

At this point in our transaction operations, we should have removed the op conditions + CHECKLOCKTIMEVERIFY from the stack, duplicated the submitted block time (after the conditions are met successfully), then remove the top submitted block time from our step.

At this point, we should be left with an input.

What Do We Do With the Block Height Value? And How to Handle the Input

In order to really make this work, we made this a multi-signature address (m-of-n). However, there will be multiple signatures involved and only two are needed.

One signature is obviously the signature (and accompanying public key) associated with the wallet.

However, another one is the 'input' (which will be our 2FA TOTP secret), which will hash out to one of the provided signatures.

Since cryptography is strong (this is what makes a "normal" transaction secure), we don't weaken our security by affixing pre-generated signatures to the transaction to select from (guessing these should be no easier than guessing someone's private / public key pairing).

From this point, we're going to run the 'op_hash160' operation on our input secret (which will be 32 bytes in length) [operation hashes the input twice with SHA256, then peforms a ripemd160 hash operation on the result of those two sha256 hashes).

Lucky Break

According to the IETF, the recommended length of the 2FA TOTP secret = 160-bits (link = https://tools.ietf.org/html/rfc6238) - thus that result can also be the 'secret' for our 2FA authenticated code (more on this later before you object).

New Bitcoin Innovation: 2FA-enabled (TOTP Transactions) [part 4]

On the Backend

Blake2bs is hashed from a true secret (to give us our variable input). Hashed with RIPEMD160 and placed in as our secret.

From there, we generate our first 6-digit code.

It is that code that is then inputted back into our Blake2bs function (as the IV key) to generate our set of 20 signatures.

This is a bit complex to explain in a Telegram channel, but we were able to work it out to where this will validate on a blockchain, must be required in order to fulfill the condition to spend the transaction, is pegged to time in a pretty direct manner (by polling the block height in a fairly concrete manner), is attached to the ECDSA-generated priv+pubkey pair so that we don't impose a vendor restriction (in other words, you don't need a 'special wallet' or software that leverages capabilities that other general wallet software does not have).

Super Brief Explainer of How Password Storage Works in Many Cases

You sign up for some service

Create a password

They hash your password and store it somewhere

—-

^ Next time you log in, you enter that password & they run the same hash operations on it. If it matches, you're in. If not, you aren't.

Drawback Here

Figuring out our 'secrets' in this scenario would be no less difficult than cracking your private/public key pairing altogether.

A would-be attacker needs to figure out:

A) Some input (which is already based on our elliptic curve generated public key as the root of this entire operation)

B) That, when hashed by SHA256 (twice) and then hashed with a RIPEMD160 operation

Produces one of our defined outputs.

And the acceptable signature is contingent on the block height at which the transaction was initiated, and stems from a nonce value that is already pre-determined, based on the block height the transaction was sent.

How This Works:

Your public key (not wallet address), is hashed with SHA3-512
(this is acceptable so long as the 'modulus' on platforms that perform this operation are the same and yielding the same output - we can be sure that's the case because this is a standard operation for OpenSSL and in all modern cryptography libraries)

That result is then hashed with Blake2b - the reason why is because Blake2b gives us a 32-bit output ; due to the limitations of Bitcoin, we can't have an excessively large output - so that's our truncating function.

We then perform (SHA256(SHA256(ripemd160))) on the 32-bit output that we received from our Blake2b operation.

^^^ This gives us our "secret" ; it doesn't change at any point ; also the recommended bit-size for secrets by the IETF = 160 bits

In addition to the secret, we need a "nonce" value. For the 'nonce' value, we're going to use the block height at the time of sending.
(to be clear, in order to establish TOTP, we're sending whatever Bitcoins we have to a different wallet that has the appropriate redeem script on it)

When we input that nonce value, its going to give us a certain output - which is a 6-digit value (what you would use to 'authenticate' when signing into websites with Authenticator TOTP).

We use that authenticator value as our new 'key' for the Blake2b hash operation - that yields a new hash output (a unique one). [note: we didn't have a "key" before when we first ran this function]

We repeat step '#4' by incrementally increasing our nonce value by '1' on the counter for 19 more integers. Each 6-digit output produced with that gives us another 6-digit code that will then be used as the 'key' in our hash operation (remember, this is from our public key - hashed with SHA3-512, then input in here) - which will give us 19 more unique INPUTS.

From here, our work is easy. We take those 19 inputs & we hash them using:

  1. SHA256
  2. SHA256 (again)
  3. ripemd160

^^^ That's how we get the valid signatures for our Bitcoin transaction.

—-

TOTP and Transaction Authentication

When it comes time for us to actually 'redeem' our transaction, we need to first validate the block height we're at (because its time based).

We do that by providing an additional input that has a proposed block time.

That input is then matched again the 'CHECKLOCKTIMEVERIFY' op code (first part in our OP_IF / OP_ELSE) conditional statements ;  so... IF checklocktimeverify = true, THEN (no_op will run automatically) ; OP_SUB1 (subtracts one from the block height integer we submitted); THEN another CHECKLOCKTIMEVERY op_code on the stack is ran <—- that one should run false | if this works out (how it should), then the operation will continue (because we've already set those conditions)

From here, we run an op_code that pushes the last four bytes of that block height integer on to the stack (which will be the final two digits of the blockheight)
admittedly, the only part I'm struggling with is calculating the addition operations in case the last two digits of the block height are >20 ; but if that's not the case, we're good to go

The reason why we pushed that integer to the top of the stack (which should already be an unsigned 32-bit integer) is because we have the op_drop command running directly after it. Op_drop allows us to pick a certain element in the stack (based on that variable) and move it to the front. This is how we ensure that someone is not validating with just any of the sig outputs for the script

From there, the top of our stack should only have our input remaining (assuming we've made it this far)

After the operation in '5' is ran, another opcode is ran (op_over), which moves our second-to-top stack items (main stack) to the top.

**Amendment to What Was Written Above in Those Notes!**

~~The commands in '7', going forward are:~~ 

~~op_roll (second to top to the front~~) 

~~then~~ 

~~'op_2over' ; the item 2 spaces back to the very front~~

(changed my mind again; scratch out what was above) 

Scratch that again - sorry. i wrote the above thinking that we'd need to run 'op_dup' here, but we're not going to be verifying a signature for the 'TOTP' authenticated signature. 

Just 'op_roll' for #7 


Scratch that again - sorry. i wrote the above thinking that we'd need to run 'op_dup' here, but we're not going to be verifying a signature for the 'TOTP' authenticated signature.

Just 'op_roll' for #7

  1. The top operation now should be op_hash160, which gives us the sha256(sha256(ripemd160))), which should be the equivalent of our transaction hash (remember the signature is verifying against that top signature)

(8a - Contrary to other multi-signature addresses, we are not going to run 'checksig' or 'checksigverify' here)

Instead we're going w 'op_equalverify' because once the op_hash160 command is ran on the top of our stack (which should have our input from the Blake2b function - predetermined by the TOTP result that we got giving us an integer 1-20 which then corresponds to a particular block height that's used as the 'nonce' value)

  1. If this is the case, then the stack will push "true" ; we can move on

9a. For good measure, I'm including a ' OP_CODESEPARATOR' upon successful resolution of the steps above

wsolution given what we're doing. This should not be an issue though as Bitcoin addresses are simple to generate)

The implementation of this needs to be standardized as well so that, if widespread, wallet providers are not running different algorithms which would result in different outputs - thus, rendering all of what I described above useless.

GO TOP