Closed nulldozer closed 1 year 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
``
Closing it as this one is old and probably already fixed. Please reopen if needed.
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
host_code.py
requirements.txt for host_code.py
``