LedgerHQ / nanos-secure-sdk

Secure (ST31) SDK for Ledger Nano S
Apache License 2.0
98 stars 53 forks source link

Change to cx_rsa_generate_pair breaks cx_rsa_decrypt #15

Closed nulldozer closed 1 year ago

nulldozer commented 6 years ago

The undocumented changes to the API affecting cx_rsa_generate_pair seem to cause cx_rsa_decrypt to fail with the undocumented error EXCEPTION_CXPORT.

I have written a program that can be built with sdk commit 1525802d and will work properly on a Ledger Nano S with firmware 1.3.1

The problem is that this program does not work when built with sdk commit 0be513c4 on a Ledger Nano S with firmware 1.4.2

src/main.c

#include "os.h"
#include "cx.h"

#include "os_io_seproxyhal.h"

unsigned char G_io_seproxyhal_spi_buffer[IO_SEPROXYHAL_BUFFER_SIZE_B];

static const bagl_element_t *io_seproxyhal_touch_exit(const bagl_element_t *e);

ux_state_t ux;

static const bagl_element_t bagl_ui_sample_nanos[] = {
    {
        {BAGL_RECTANGLE, 0x00, 0, 0, 128, 32, 0, 0, BAGL_FILL, 0x000000,
         0xFFFFFF, 0, 0},
        NULL,
        0,
        0,
        0,
        NULL,
        NULL,
        NULL,
    },
    {
        {BAGL_LABELINE, 0x01, 0, 12, 128, 32, 0, 0, 0, 0xFFFFFF, 0x000000,
         BAGL_FONT_OPEN_SANS_REGULAR_11px | BAGL_FONT_ALIGNMENT_CENTER, 0},
        "press both to exit",
        0,
        0,
        0,
        NULL,
        NULL,
        NULL,
    },
};

static unsigned int
bagl_ui_sample_nanos_button(unsigned int button_mask,
                            unsigned int button_mask_counter) {
    switch (button_mask) {
    case BUTTON_EVT_RELEASED | BUTTON_LEFT | BUTTON_RIGHT: // EXIT
        io_seproxyhal_touch_exit(NULL);
        break;
    }
    return 0;
}

static const bagl_element_t *io_seproxyhal_touch_exit(const bagl_element_t *e) {
    // Go back to the dashboard
    os_sched_exit(0);
    return NULL;
}

unsigned short io_exchange_al(unsigned char channel, unsigned short tx_len) {
    switch (channel & ~(IO_FLAGS)) {
    case CHANNEL_KEYBOARD:
        break;

    // multiplexed io exchange over a SPI channel and TLV encapsulated protocol
    case CHANNEL_SPI:
        if (tx_len) {
            io_seproxyhal_spi_send(G_io_apdu_buffer, tx_len);

            if (channel & IO_RESET_AFTER_REPLIED) {
                reset();
            }
            return 0; // nothing received from the master so far (it's a tx
                      // transaction)
        } else {
            return io_seproxyhal_spi_recv(G_io_apdu_buffer,
                                          sizeof(G_io_apdu_buffer), 0);
        }

    default:
        THROW(INVALID_PARAMETER);
    }
    return 0;
}

static void ui_idle(void) {
    UX_DISPLAY(bagl_ui_sample_nanos, NULL);
}

static void sample_main(void) {
    // make sure these match the python script
    #define MODULUS_LEN 128
    cx_rsa_1024_public_key_t public_key;
    cx_rsa_1024_private_key_t private_key;
    const unsigned char exp_be_buf[] = {0x00, 0x01, 0x00, 0x01};

    #define INS_GENERAGE_KEY 0x01
    #define INS_GET_N 0x02
    #define INS_GET_D 0x03
    #define INS_DECRYPT 0x04

    unsigned char plaintext[MODULUS_LEN]; // plaintext length can't be longer than the modulus
    int plaintext_length;

    volatile unsigned int rx = 0;
    volatile unsigned int tx = 0;
    volatile unsigned int flags = 0;

    for (;;) {
        volatile unsigned short sw = 0;

        BEGIN_TRY {
            TRY {
                rx = tx;
                tx = 0; // ensure no race in catch_other if io_exchange throws
                        // an error
                rx = io_exchange(CHANNEL_APDU | flags, rx);
                flags = 0;

                // no apdu received, well, reset the session, and reset the
                // bootloader configuration
                if (rx == 0) {
                    THROW(0x6982);
                }

                if (G_io_apdu_buffer[0] != 0x80) {
                    THROW(0x6E00);
                }

                // unauthenticated instruction
                switch (G_io_apdu_buffer[1]) {
                case 0x00: // reset
                    flags |= IO_RESET_AFTER_REPLIED;
                    THROW(0x9000);
                    break;

                case INS_GENERAGE_KEY: // generate key
#if CX_APILEVEL < 8
                    // for 1.3.1
                    // builds with the sdk in nanos-secure-sdk 1525802dda0b5437439c61b79f49e632b2080d14
                    cx_rsa_generate_pair(
                        MODULUS_LEN,
                        &public_key,
                        &private_key,
                        0,      // use default e (65537)
                        NULL);  // no externalPQ
#else
                    // for 1.4.2
                    // builds with the sdk in nanos-secure-sdk 0be513c4bd85ad03f446da25a35425f480ccdc3f
                    cx_rsa_generate_pair(
                        MODULUS_LEN,
                        &public_key,
                        &private_key,
                        exp_be_buf,
                        4,
                        NULL); // no externalPQ
#endif                    
                    THROW(0x9000);
                    break;

                case INS_GET_N: // get private.n
                  os_memmove(G_io_apdu_buffer, private_key.n, MODULUS_LEN);
                  tx = MODULUS_LEN;
                  THROW(0x9000);

                case INS_GET_D: // get private.d
                  os_memmove(G_io_apdu_buffer, private_key.d, MODULUS_LEN);
                  tx = MODULUS_LEN;
                  THROW(0x9000);

                case INS_DECRYPT: //decrypt
                    if(G_io_apdu_buffer[4] != MODULUS_LEN) THROW(0x6AAC);
                    plaintext_length = cx_rsa_decrypt(
                            (cx_rsa_private_key_t*) &private_key,
                            CX_PAD_PKCS1_OAEP,
                            CX_SHA256,
                            (unsigned char*) &(G_io_apdu_buffer[5]),
                            MODULUS_LEN,
                            plaintext,
                            MODULUS_LEN);
                    if(plaintext_length < 0) THROW(0x6AAB);
                    os_memmove(G_io_apdu_buffer, plaintext, plaintext_length);
                    tx = plaintext_length;
                    THROW(0x9000);
                    break;

                case 0xFF: // return to dashboard
                    goto return_to_dashboard;

                default:
                    THROW(0x6D00);
                    break;
                }
            }
            CATCH_OTHER(e) {
                switch (e & 0xF000) {
                case 0x6000:
                case 0x9000:
                    sw = e;
                    break;
                default:
                    sw = 0x6800 | (e & 0x7FF);
                    break;
                }
                // Unexpected exception => report
                G_io_apdu_buffer[tx] = sw >> 8;
                G_io_apdu_buffer[tx + 1] = sw;
                tx += 2;
            }
            FINALLY {
            }
        }
        END_TRY;
    }

return_to_dashboard:
    return;
}

void io_seproxyhal_display(const bagl_element_t *element) {
    io_seproxyhal_display_default((bagl_element_t *)element);
}

unsigned char io_event(unsigned char channel) {
    // nothing done with the event, throw an error on the transport layer if
    // needed

    // can't have more than one tag in the reply, not supported yet.
    switch (G_io_seproxyhal_spi_buffer[0]) {
    case SEPROXYHAL_TAG_FINGER_EVENT:
        UX_FINGER_EVENT(G_io_seproxyhal_spi_buffer);
        break;

    case SEPROXYHAL_TAG_BUTTON_PUSH_EVENT: // for Nano S
        UX_BUTTON_PUSH_EVENT(G_io_seproxyhal_spi_buffer);
        break;

    case SEPROXYHAL_TAG_DISPLAY_PROCESSED_EVENT:
        if (UX_DISPLAYED()) {
            // TODO perform actions after all screen elements have been
            // displayed
        } else {
            UX_DISPLAYED_EVENT();
        }
        break;

    // unknown events are acknowledged
    default:
        break;
    }

    // close the event if not done previously (by a display or whatever)
    if (!io_seproxyhal_spi_is_status_sent()) {
        io_seproxyhal_general_status();
    }

    // command has been processed, DO NOT reset the current APDU transport
    return 1;
}

__attribute__((section(".boot"))) int main(void) {
    // exit critical section
    __asm volatile("cpsie i");

    UX_INIT();

    // ensure exception will work as planned
    os_boot();

    BEGIN_TRY {
        TRY {
            io_seproxyhal_init();

            USB_power(0);
            USB_power(1);

            ui_idle();

            sample_main();
        }
        CATCH_OTHER(e) {
        }
        FINALLY {
        }
    }
    END_TRY;
}

host_code.py

from ledgerblue.comm import getDongle
from ledgerblue.commException import CommException

import binascii
from datetime import datetime

from Cryptodome.Cipher import PKCS1_OAEP
from Cryptodome.Cipher import PKCS1_v1_5
from Cryptodome.PublicKey import RSA
from Cryptodome.Hash import SHA256

dongle = getDongle(True)

# make sure these match main.c
MODULUS_LEN = 128
INS_GENERATE_KEY = 0x01
INS_GET_N = 0x02
INS_GET_D = 0x03
INS_DECRYPT = 0x04

# n and d are byte strings
def key_from_components(n,d,e=65537):
    components = (int.from_bytes(n, byteorder='big'), e, int.from_bytes(d, byteorder='big'))
    key = RSA.construct(components)
    return key

# plaintext should be bytes
def encrypt(public_key, plaintext):
    cipher = PKCS1_OAEP.new(public_key, hashAlgo=SHA256)
    #cipher = PKCS1_v1_5.new(public_key)
    print('plaintext',plaintext)
    ciphertext = cipher.encrypt(plaintext)
    return ciphertext

def ledger_keygen():
    # tell the device to generate the key
    cmd = bytes([0x80,INS_GENERATE_KEY,0x00,0x00,0x00])
    resp = dongle.exchange(cmd, 300)

    # get key.n
    cmd = bytes([0x80,INS_GET_N,0x00,0x00,0x00])
    n = dongle.exchange(cmd, 300)
    assert len(n) == MODULUS_LEN, 'length of n doesn\'t match modulus'

    # get key.d
    cmd = bytes([0x80,INS_GET_D,0x00,0x00,0x00])
    d = dongle.exchange(cmd, 300)
    assert len(d) == MODULUS_LEN, 'length of d doesn\'t match modulus'

    print('ledger_keygen n',binascii.hexlify(n).decode('utf8'))
    print('ledger_keygen d',binascii.hexlify(d).decode('utf8'))

    key = key_from_components(n,d)

    return key

#ciphertext is byte string
def ledger_decrypt(ciphertext):
    cmd = bytes([0x80,INS_DECRYPT,0x00,0x00,len(ciphertext)]) + ciphertext
    resp = dongle.exchange(cmd,300)
    return resp

key = ledger_keygen()

plaintext = bytes([i%0xff for i in range(16)])
print('plaintext',binascii.hexlify(plaintext).decode('utf8'))

ciphertext = encrypt(key, plaintext)
print('ciphertext',binascii.hexlify(ciphertext).decode('utf8'))
print('ciphertext length',len(ciphertext))

decrypted = ledger_decrypt(ciphertext)
print('decrypted',binascii.hexlify(decrypted).decode('utf8'))

requirements.txt for host_code.py

certifi==2018.4.16
chardet==3.0.4
ECPy==0.9.0
future==0.16.0
hidapi==0.7.99.post21
idna==2.6
ledgerblue==0.1.17
Pillow==5.1.0
protobuf==3.5.2.post1
pycrypto==2.6.1
pycryptodomex==3.6.1
python-u2flib-host==3.0.3
requests==2.18.4
six==1.11.0
urllib3==1.22

``

xchapron-ledger commented 1 year ago

Closing it as this one is old and probably already fixed. Please reopen if needed.