On-Chain 2FA Whitepaper
Summary of Points
- The incentive for a supply chain attack is ginormous and dwarfs all other attack vectors
- Nothing can be trusted — open source or otherwise
- We must stop trusting hardware wallets & supply chains
- Those that control the entropy, control the funds
- On-chain 2FA is the solution
- How does 2FA work on chain?
- Using on-chain 2FA plus multisig
At the time of this writing there are about 18 million Bitcoins in circulation subtracting an estimated 4 million of lost coins, we come to 14 million. At the time of this writting that is $102 billion USD, the equivilent of the fourth richest person in the world.
If we assume half those Bitcoins are stored in hardware wallets then the current honeypot is $51 billion USD. Someone who pulled off this attack in its entirety would become the 18th richest person in the world. If Bitcoin doubles or triples in value as it has in the past, this attack could easily create the richest person in the world.
The incentive to perform this heist is so large, budgeting $1B USD would be conservative. How many low wage factory workers can you buy and trick with that kind of budget? How about just buying factories outright?
You can execute this attack from anywhere along the supply chain including the wafer die, the wafer printing machine itself, putting the wafer into chips, putting the chip onto the circuit board, making the circuit board, assembling the product enclosure, and ANY of the shipping companies between these various factories.
This attack could be going on right now and we have absolutely no way of knowing. Our current Bitcoin security model is essentially 'hope no one notices we are storing wealth here’ -- but security by obscurity on this scale cannot and will not stand the test of time.
Because these chips are used to program themselves, they could be running any code at all. It is almost trivial for an attacker to preprogram chips with malicious code that then “pretends” to be programmed in the factory. When the factory programmer asks the chip “give me a hash of your code” for “verification" it is trivial for the malicious code to send over whatever hash the programmer wants. You fundamentally have to trust the device you are programming to actually be programmed.
If I am a chip manufacturer my attack vector is even easier. Instead of modifying the hardware wallet’s code, I simply reverse engineer how it is generating entropy. Now I redesign the hardware of my chip such that it creates entropy in a manner I can predict. One simple method is to hash an incrementing number plus some chosen secret. The result of this hash will be indistinguishable from randomness to the code but the attacker will be able to recreate every piece of entropy generated.
These attacks are two easy ones but the attack vectors are limitless and with a budget over $1B USD you could accomplish vastly more complex varieties.
These changes can be done at ANY stage of the supply chain. It doesn’t matter if the board design calls for a secure element or a regular CPU. It doesn’t matter if the hardware is open source or closed source. It doesn’t matter if the code is open source or closed source. When a supply chain attack is present all these methods are nothing but security theater. They fundamentally don’t change the amount of trust in the device.
How does entropy control wealth?
Once an attacker has the ability to generate the same entropy as hardware devices, they can generate all the private keys generated by all those devices. After they infiltrate the supply chain during manufacture they then wait. The more devices they do this to and the longer they wait, the more users will deposit funds into them.
With the ability to generate the private keys at will, they could build a simple dashboard that adds up all the deposits users of all the devices they’ve infiltrated. They watch as this number goes up until it reaches a value they’re excited about. Maybe it’s just $10B USD, maybe it’s $50B USD, maybe more.
Then, using the regenerated private keys, they steal all the funds at once. Everyone using the wallets wakes up to zero balances. Trust in hardware wallets collapses, but it doesn’t matter for the attacker — they’ve completed the heist.
There is, however, a solution. Hardware must be designed and treated as fundamentally untrustworthy. It cannot be solely responsible and able to manage funds. It needs a check that comes from entirely outside. There must be a some entropy that is never generated on device.
However a system that incorporates entropy from outside the device is only half the solution. After you’re incorporated the entropy you need to prove it was actually used by the device. Without this critical second step you’re back to trusting the system again.
Luckily there is an automated machine for verifying the use of entropy — the Bitcoin blockchain. By putting the remotely generated entropy right there in the deposit hash (and later input script) itself, the blockchain will enforce that it is used for you. This is how we get so called “on-chain 2FA”.
So what does a transaction like this look like? Let’s take a real world example:
Txid: 48b32b810af56d87c89614b7e909452e9af280ecc7975838209a0faecc38d583 Ver: 2, locktime: 0, value: 7345, fee: 2655 [Input 1] Previous Transaction Hash: [4a297df190ad97d24696cbc8f8cb467cc76e519a6cb09d8329797eebc1b81506] Output Index: 0 Witness Stack: , [30440220250040f46b22f082330e3150ed3d57e8d769147a805707b65363738ea49c0cd9022052c6d5e77104679a4135f4055a102c2165e4e3e89c414566c686950f3cef198601], [304402203dea427c98818990c270a78f80b9f44f2ba8401b75d7b4c4662d39b989f8368002202cfa707bad4e7f911c741ae89d407b656e86c4089e9991bef148c8bc08d34fa901] Witness Script: PUSH(33) [03259448d19ec38dfa86f6935097d3430ff2deb17c8f65b612cf28483a17190bd9] OP_CHECKSIGVERIFY OP_1 PUSH(33) [0276cb99a3c054afca40c36b6c3d68747d7d0291117c3604a0ba515f56cd9f7e10] OP_1 OP_CHECKMULTISIG Sequence: 00000000 [Output 1] Value: 0.00007345 Script: OP_FALSE PUSH(20) [f83f56531009b5a74c11e07bbc6c9055b20cd653]
The important bit here is the witness script, let’s break that into two pieces. Here's the first.
PUSH(33) [03259448d19ec38dfa86f6935097d3430ff2deb17c8f65b612cf28483a17190bd9] OP_CHECKSIGVERIFY
The first section is the pub key of entropy generated off of the hardware device. As we know from the Bitcoin script spec OP_CHECKSIGVERIFY will take the two topmost items, assume the bottom one is a signature and the top one a public key. The pub key is pushed just before OP_CHECKSIGVERIFY and is 03259448d19ec38dfa86f6935097d3430ff2deb17c8f65b612cf28483a17190bd9. The other item is found at the top of the witness stack and is 304402203dea427c98818990c270a78f80b9f44f2ba8401b75d7b4c4662d39b989f8368002202cfa707bad4e7f911c741ae89d407b656e86c4089e9991bef148c8bc08d34fa901fa901. Since this signature validates we continue onward. Importantly, if they did not this script would fail. This is the mechanism that ensures our “2FA” entropy generated off device is used.
Onto the second portion.
OP_1 PUSH(33) [0276cb99a3c054afca40c36b6c3d68747d7d0291117c3604a0ba515f56cd9f7e10] OP_1 OP_CHECKMULTISIG
This second section is where whatever your standard hardware wallet controlled script is put. As we know from the Bitcoin script spec, OP_CHECKMULTISIG expects the stack to some number of signatures, the count of signatures, some number of public keys, and the count of public keys.
Since OPCHECKSIGVERIFY consumed its two items, the witness stack has been reduced to:
The OP_1, PUSH(33), and OP_1 grow the stack and put it in this state:
 [30440220250040f46b22f082330e3150ed3d57e8d769147a805707b65363738ea49c0cd9022052c6d5e77104679a4135f4055a102c2165e4e3e89c414566c686950f3cef198601] OP_1 PUSH(33) [0276cb99a3c054afca40c36b6c3d68747d7d0291117c3604a0ba515f56cd9f7e10] OP_1
This stack is then consumed by OP_CHECKSIGVERIFY which puts either true or false on the stack. Since this one signature is valid, the stacks now in this state.
Since it’s true, the blockchain validates the transaction as valid and it can be published.
The important thing to remember here is that BOTH the CHECKSIGVERIFY and the CHECKMULTISIG had to be executed. In this way we can combine the 2FA with any script we like. In this case we only did a multisig 1 of 1 — but it could be anything we’d like.
For our next example we’ll demonstrate using this 2FA technique with a multisig 2 of 3.
Txid: 35ceaca8eed20e6c950f05e2ed108357f3915b28463104eb20fb70ec8e57fd7f Ver: 2, locktime: 0, value: 962062, fee: 470 [Input 1] Previous Transaction Hash: [20d2dc624afeca0cff1ca38664a1c3e5077bf9ce0591f27629c6e89a06d337bb] Output Index: 0 Witness Stack: , [304402203710a6f4b8a52b5bcccb9e64337561385db10ea6a9eb52cec16d7a30491e009c022024f547e4fc0d376274de32b84bb12de02103ae92b0224a96546921fad1a164a801], [304502210099f55e1edcb8585a3622cca3716385735cd47efac864926795f4a6f0de77e620022061aed6fef15ea49fe539005661d6850155ef3afa907b6884a9cf2b077e24cdcc01], [3045022100a02493021c592adc35230a3912a14ac52ad0118e5737a20bc5185256fc8b2700022015aef33314ebf41a3912f79140caea295a612a4891e9d2a44daeae9f49360c7001] Witness Script: PUSH(33) [0265a4180ef2823b79c3c098710ddc1e27e2bd6f37722b5b3555ffc3b72ed47cf6] OP_CHECKSIGVERIFY OP_2 PUSH(33) [0216308c2faa227ffb3a94bab52088f8edbcc1c35fc494014a0d8447994cd22d83] PUSH(33) [0347cd1b8ac538c6e0619105a6a91d4281921ccb30e55a91bca62e73bef5479e4e] PUSH(33) [038d97b42b53b40bcfed94a92b3057ca28a5b4d7643921c4619cc4832ef7ad787b] OP_3 OP_CHECKMULTISIG Sequence: 00000000 [Output 1] Value: 0.00962062 Script: OP_FALSE PUSH(20) [ec292b45bf16f7a3417ab2641906a8a54519f7be]
As before, our OP_CHECKSIGVERIFY will run first.
PUSH(33) [0265a4180ef2823b79c3c098710ddc1e27e2bd6f37722b5b3555ffc3b72ed47cf6] OP_CHECKSIGVERIFY
It takes the top element on the witness stack 3045022100a02493021c592adc35230a3912a14ac52ad0118e5737a20bc5185256fc8b2700022015aef33314ebf41a3912f79140caea295a612a4891e9d2a44daeae9f49360c7001c7001 which is a signature, adds the public key 0265a4180ef2823b79c3c098710ddc1e27e2bd6f37722b5b3555ffc3b72ed47cf6 and calls OP_CHECKSIGVERIFY. Since the signature validates correctly with the public key, the script continues. Importantly, if they did not match the script would abort. This, again, is what enables our "on-chain 2FA."
Onto the second portion.
OP_2 PUSH(33) [0216308c2faa227ffb3a94bab52088f8edbcc1c35fc494014a0d8447994cd22d83] PUSH(33) [0347cd1b8ac538c6e0619105a6a91d4281921ccb30e55a91bca62e73bef5479e4e] PUSH(33) [038d97b42b53b40bcfed94a92b3057ca28a5b4d7643921c4619cc4832ef7ad787b] OP_3 OP_CHECKMULTISIG
This should look familiar to the last CHECKMULTISIG with a few changes. The OP_1s have become OP_2 and OP_3 respectively. This is what makes this a “2 of 3 multisig.” There’s also three PUSH’es instead of one from before. These are our three public keys (the 3 part of "2 of 3").
After CHECKSIGVERIFY completed and consumed its stack items, the stack now looks like this:
 [304402203710a6f4b8a52b5bcccb9e64337561385db10ea6a9eb52cec16d7a30491e009c022024f547e4fc0d376274de32b84bb12de02103ae92b0224a96546921fad1a164a801] [304502210099f55e1edcb8585a3622cca3716385735cd47efac864926795f4a6f0de77e620022061aed6fef15ea49fe539005661d6850155ef3afa907b6884a9cf2b077e24cdcc01]
This should again look familiar, but instead of one item it has two. These are the two signatures required. Again, because this is a 2 of 3 multisig, we have two signatures and three public keys respectively.
After executing OP_2, PUSH(33), PUSH(33), PUSH(33), and OP_3, our stack now looks like this:
 [304402203710a6f4b8a52b5bcccb9e64337561385db10ea6a9eb52cec16d7a30491e009c022024f547e4fc0d376274de32b84bb12de02103ae92b0224a96546921fad1a164a801] [304502210099f55e1edcb8585a3622cca3716385735cd47efac864926795f4a6f0de77e620022061aed6fef15ea49fe539005661d6850155ef3afa907b6884a9cf2b077e24cdcc01] OP_2 [0216308c2faa227ffb3a94bab52088f8edbcc1c35fc494014a0d8447994cd22d83] [0347cd1b8ac538c6e0619105a6a91d4281921ccb30e55a91bca62e73bef5479e4e] [038d97b42b53b40bcfed94a92b3057ca28a5b4d7643921c4619cc4832ef7ad787b] OP_3
The stack is again consumed by OP_CHECKMULTISIG. Since these signatures are all correct the stack is consumed and the stack is now:
Which makes the transaction valid and it can now be published.
In conclusion, we have demonstrated a way to add on-chain 2FA to any Bitcoin script. We went through two examples using 2FA + 1 signature and 2FA + 2 of 3 multisig. We showed how this approach can make a 2FA key required but not sufficient for unlocking funds.
With this approach we can shut off the supply chain attack entirely. Theft of the 2FA key does not grant access to the funds because the 2FA key is required but not sufficient. Leveraging both 2FA and hardware multisig gives us the benefits of both worlds and, most importantly, makes us immune to supply chain attacks.