Filiprogrammer / SimpleJadePinServer

Simple reimplementation of the blind_pin_server for the Blockstream Jade along with a very simple web interface
1 stars 1 forks source link

"Network or server error" on Step 2/4 start_handshake #4

Open petre-c opened 2 weeks ago

petre-c commented 2 weeks ago

Hey @Filiprogrammer , I saw you implemented setting a blind oracle with QR, well done and thank you!

I gave it a try - Oracle QR gets scanned successfully. After that, Jade produces a 'Network or server error' on Step 2/4 start_handshake: image

I tried it on a debian distro and with docker.

My Jade is on firmware version 1.0.31.

Log from the terminal: image

Filiprogrammer commented 2 weeks ago

This error message usually appears when the static public key of the pin server does not match the oracle public key configured on the Jade. Are you certain that you have not regenerated the private.key and public.key after configuring the blind oracle on the Jade?

petre-c commented 2 weeks ago

This error message usually appears when the static public key of the pin server does not match the oracle public key configured on the Jade. Are you certain that you have not regenerated the private.key and public.key after configuring the blind oracle on the Jade?

Yes, I am certain that I have not regenerated the private.key and public.key.

I have attached them for your reference server_keys.zip

Oracle QR step: image

Step 2/4 (video): https://github.com/user-attachments/assets/fdb41ea3-0b2a-492d-a24e-d83aa4c70a33

Filiprogrammer commented 2 weeks ago

Which firmware version is your Jade on? Mine is on 1.0.24. I have tried with your keys and I cannot reproduce your problem.

petre-c commented 2 weeks ago

I upgraded today to 1.0.32

Filiprogrammer commented 2 weeks ago

I have just scanned the QR codes from the video you provided and it works on firmware version 1.0.24

petre-c commented 2 weeks ago

I will try to downgrade to 1.0.24.

Will you please look at if 1.0.32 is complaining for you as well (and fix it, if so) or are you avoiding the firmware upgrade for some reason?

Filiprogrammer commented 2 weeks ago

Will you please look at if 1.0.32 is complaining for you as well (and fix it, if so) or are you avoiding the firmware upgrade for some reason?

I am looking into it

Filiprogrammer commented 2 weeks ago

Ok I see what's going on. In firmware version 1.0.28 the pinserver oracle protocol was changed to have only 2 steps.

Changed pinserver oracle protocol to only require a single roundtrip, exchanging a single base64-encoded string

My implementation was made for the old 4 step protocol.

petre-c commented 2 weeks ago

Ok I see what's going on. In firmware version 1.0.28 the pinserver oracle protocol was changed to have only 2 steps.

Changed pinserver oracle protocol to only require a single roundtrip, exchanging a single base64-encoded string

My implementation was made for the old 4 step protocol.

And you will update your code to reflect the change, is that correct reasoning on my part?

Filiprogrammer commented 2 weeks ago

From my testing I can confirm that 1.0.27 is the last version that works with SimpleJadePinServer.

And you will update your code to reflect the change, is that correct reasoning on my part?

I will look into it. It may take some time though, as I have a few other tasks on my plate at the moment.

petre-c commented 2 weeks ago

Okay, I will wait patiently 🙂

petre-c commented 1 week ago

@Filiprogrammer , have you had a chance to look into this?

Filiprogrammer commented 1 week ago

Not yet, I am somewhat busy with my job and my studies. I might tackle this on the weekend.

Filiprogrammer commented 5 days ago

I investigated the new protocol and written pseudocode for its implementation.


Step 1/2 - Jade --> pinserver

What is sent from the Jade to the pin server:

{
    "id": "qrauth",
    "result": {
        "http_request": {
            "params": {
                "urls": [
                    "http://127.0.0.1:4443/set_pin",
                    ""
                ],
                "method": "POST",
                "accept": "json",
                "data": {
                    "data": "<base64 encoded (cke + replay counter + encrypted payload)>"
                }
            },
            "on-reply": "pin"
        }
    }
}

Step 2/2 - pinserver --> Jade

What is sent from the pin server to the Jade:

{
    "data": "<base64 encoded (encrypted AES key with HMAC appended)>"
}

Server-side pseudocode for set_pin

data = b64decode(data)
assert len(data) > 37
cke = data[:33]
replay_counter = data[33:37]
encrypted_data = data[37:]

private_key, public_key = generate_ec_key_pair(replay_counter, cke)
    tweak = wally.sha256(wally.hmac_sha256(cke, replay_counter))
    private_key = wally.ec_private_key_bip341_tweak(STATIC_SERVER_PRIVATE_KEY, tweak, 0)
    wally.ec_private_key_verify(private_key)
    public_key = wally.ec_public_key_from_private_key(private_key)

payload = wally.aes_cbc_with_ecdh_key(private_key, None, encrypted_data, cke, b'blind_oracle_request', wally.AES_FLAG_DECRYPT)

# set_pin requires client-passed entropy
assert len(payload) == wally.SHA256_LEN + wally.SHA256_LEN + wally.EC_SIGNATURE_RECOVERABLE_LEN
pin_secret = payload[:wally.SHA256_LEN]
entropy = payload[wally.SHA256_LEN: wally.SHA256_LEN + wally.SHA256_LEN]
sig = payload[wally.SHA256_LEN + wally.SHA256_LEN:]
signed_msg = wally.sha256(cke + replay_counter + pin_secret + entropy)
pin_pubkey = wally.ec_sig_to_public_key(signed_msg, sig)

pin_pubkey_hash = bytes(wally.sha256(pin_pubkey))

replay_local = None
try:
    _, _, _, replay_local = load_pin_fields(pin_pubkey_hash, pin_pubkey)

    # Enforce anti replay (client counter must be greater than the server counter)
    client_counter = int.from_bytes(replay_counter, byteorder='little', signed=False)
    server_counter = int.from_bytes(replay_local, byteorder='little', signed=False)
    assert client_counter > server_counter
except FileNotFoundError:
    pass

new_key = wally.hmac_sha256(os.urandom(32), entropy)

hash_pin_secret = wally.sha256(pin_secret)
replay_bytes = b'\x00\x00\x00\x00'
save_pin_fields(pin_pubkey_hash, hash_pin_secret, new_key, pin_pubkey, 0, replay_bytes)
aes_key = wally.hmac_sha256(new_key, pin_secret)
assert len(aes_key) == wally.AES_KEY_LEN_256

iv = os.urandom(wally.AES_BLOCK_LEN)
encrypted_key = wally.aes_cbc_with_ecdh_key(private_key, iv, aes_key, cke, b'blind_oracle_response', wally.AES_FLAG_ENCRYPT)
assert len(encrypted_key) == wally.AES_KEY_LEN_256 + (2*wally.AES_BLOCK_LEN) + wally.HMAC_SHA256_LEN

self.wfile.write(b'{"data":"' + b64encode(encrypted_key) + b'"}')

Server-side pseudocode for get_pin

data = b64decode(data)
assert len(data) > 37
cke = data[:33]
replay_counter = data[33:37]
encrypted_data = data[37:]

private_key, public_key = generate_ec_key_pair(replay_counter, cke)
    tweak = wally.sha256(wally.hmac_sha256(cke, replay_counter))
    private_key = wally.ec_private_key_bip341_tweak(STATIC_SERVER_PRIVATE_KEY, tweak, 0)
    wally.ec_private_key_verify(private_key)
    public_key = wally.ec_public_key_from_private_key(private_key)

payload = wally.aes_cbc_with_ecdh_key(private_key, None, encrypted_data, cke, b'blind_oracle_request', wally.AES_FLAG_DECRYPT)

# get_pin does not need client-passed entropy
assert len(payload) == wally.SHA256_LEN + wally.EC_SIGNATURE_RECOVERABLE_LEN
pin_secret = payload[:wally.SHA256_LEN]
sig = payload[wally.SHA256_LEN:]
signed_msg = wally.sha256(cke + replay_counter + pin_secret)
pin_pubkey = wally.ec_sig_to_public_key(signed_msg, sig)

pin_pubkey_hash = bytes(wally.sha256(pin_pubkey))

try:
    saved_hash_pin_secret, saved_key, counter, replay_local = load_pin_fields(pin_pubkey_hash, pin_pubkey)

    # Enforce anti replay (client counter must be greater than the server counter)
    client_counter = int.from_bytes(replay_counter, byteorder='little', signed=False)
    server_counter = int.from_bytes(replay_local, byteorder='little', signed=False)
    assert client_counter > server_counter

    hash_pin_secret = wally.sha256(pin_secret)
    if hmac.compare_digest(saved_hash_pin_secret, hash_pin_secret):
        print("Correct pin on the " + str(counter + 1) + ". attempt")

        # Zero the 'bad guess counter' and update the replay_counter
        save_pin_fields(pin_pubkey_hash, saved_hash_pin_secret, saved_key, pin_pubkey, 0, replay_counter)
    else:
        print("Wrong pin (" + str(counter + 1) + ". attempt)")

        if counter >= 2:
            os.remove(pins_path + "/" + bytes2hex(pin_pubkey_hash) + ".pin")
            print("Too many wrong attempts")
        else:
            save_pin_fields(pin_pubkey_hash, saved_hash_pin_secret, saved_key, pin_pubkey, counter + 1, replay_counter)

        # return junk key
        saved_key = os.urandom(wally.AES_KEY_LEN_256)
except Exception:
    # return junk key
    saved_key = os.urandom(wally.AES_KEY_LEN_256)

aes_key = wally.hmac_sha256(saved_key, pin_secret)
assert len(aes_key) == wally.AES_KEY_LEN_256

iv = os.urandom(wally.AES_BLOCK_LEN)
encrypted_key = wally.aes_cbc_with_ecdh_key(private_key, iv, aes_key, cke, b'blind_oracle_response', wally.AES_FLAG_ENCRYPT)
assert len(encrypted_key) == wally.AES_KEY_LEN_256 + (2*wally.AES_BLOCK_LEN) + wally.HMAC_SHA256_LEN

self.wfile.write(b'{"data":"' + b64encode(encrypted_key) + b'"}')