From d5761ff2607072f2b37a2ed417e5cc9bed36e897 Mon Sep 17 00:00:00 2001 From: Jeff S Date: Mon, 21 Mar 2022 10:24:22 +0000 Subject: [PATCH 1/5] Add message signing feature and tweak firmware sig verification --- i18n/translations/de-DE.json | 30 ++-- i18n/translations/en-US.json | 28 ++-- i18n/translations/es-MX.json | 30 ++-- i18n/translations/fr-FR.json | 28 ++-- i18n/translations/vi-VN.json | 28 ++-- src/krux/firmware.py | 21 +-- src/krux/key.py | 15 +- src/krux/pages/home.py | 74 +++++++++- tests/firmware-v0.0.0.bin.bad.sig | Bin 0 -> 72 bytes ...dsig => firmware-v0.0.0.bin.malformed.sig} | 0 tests/firmware-v0.0.0.bin.sha256.txt | 2 +- tests/firmware-v0.0.0.bin.sig | 3 +- .../firmware-v0.0.0.bin.withheader.sha256.txt | 1 + tests/pages/test_home.py | 127 +++++++++++++++++ tests/test_firmware.py | 128 ++++++++++-------- tests/test_key.py | 31 +++++ tests/test_wallet.py | 10 +- 17 files changed, 423 insertions(+), 133 deletions(-) create mode 100644 tests/firmware-v0.0.0.bin.bad.sig rename tests/{firmware-v0.0.0.bin.badsig => firmware-v0.0.0.bin.malformed.sig} (100%) create mode 100644 tests/firmware-v0.0.0.bin.withheader.sha256.txt diff --git a/i18n/translations/de-DE.json b/i18n/translations/de-DE.json index 835f8871c..304725b30 100644 --- a/i18n/translations/de-DE.json +++ b/i18n/translations/de-DE.json @@ -1,30 +1,32 @@ { + ",": ".", + ".": ",", "%d of %d multisig": "%d von %d Multisig", "%s\n\nis a valid receive address": "%s\n\nist eine gültige Empfangsadresse", "%s\n\nwas NOT FOUND in the first %d receive addresses": "%s\n\nwurde in den ersten %d Empfangsadressen NICHT GEFUNDEN", - ",": ".", - ".": ",", "About": "Über", - "BIP39 Mnemonic": "BIP39 Mnemonic", "Back": "Zurückkehren", "Backing up bootloader..\n\n%d%%": "Bootloader unterstützen..\n\n%d%%", "Bad signature": "Ungültige Unterschrift", "Baudrate\n%s": "Baudrate\n%s", + "BIP39 Mnemonic": "BIP39 Mnemonic", "Check that address belongs to this wallet?": "Überprüfen Sie die Adresse dieser Wallet gehört?", "Checked %d receive addresses with no matches.": "Überprüfte %d Empfängsadressen ohne Übereinstimmungen.", "Checking receive address %d for match..": "Überprüfen der Empfängsadresse %d auf Übereinstimmung.", "Continue?": "Fortsetzen?", - "DEBUG": "DEBUG", "Debug": "Debug", + "DEBUG": "DEBUG", "Del": "Del", "Done?": "Fertig?", "Enter each word of your BIP-39 mnemonic as a number from 1 to 2048.": "Geben Sie jedes Wort Ihrer BIP-39 Mnemonic als Zahl von 1 bis 2048 ein.", "Enter each word of your BIP-39 mnemonic as a series of binary digits.": "Geben Sie jedes Wort Ihrer BIP-39 Mnemonic als Reihe von Binärziffern ein.", "Enter each word of your BIP-39 mnemonic.": "Geben Sie jedes Wort Ihrer BIP-39 Mnemonic ein.", "Error:\n%s": "Fehler:\n%s", - "Failed to load PSBT": "PSBT könnte nicht geladen werden", + "Extended Public Key": "Öffentlicher Schlüssel", "Failed to load address": "Adresse könnte nicht geladen werden", + "Failed to load message": "Nachricht konnte nicht geladen werden", "Failed to load mnemonic": "Mnemonic könnte nicht geladen werden", + "Failed to load PSBT": "PSBT könnte nicht geladen werden", "Failed to load wallet": "Wallet könnte nicht geladen werden", "Fee:\n₿%s": "Gebühr:\n₿%s", "Fingerprint: %s\n\nDerivation: m%s\n\n%s": "Fingerabdruck: %s\n\nAbleitung: m%s\n\n%s", @@ -44,6 +46,7 @@ "Locale": "Gebietsschema", "Locale\n%s": "Gebietsschema\n%s", "Log Level\n%s": "Protokollebene\n%s", + "Message": "Nachricht", "Missing signature file": "Fehlende Signaturdatei", "Mnemonic": "Mnemonic", "Multisig": "Multisig", @@ -56,31 +59,34 @@ "Printer": "Drucker", "Printing\n%d / %d": "Drucken\n%d / %d", "Proceed?": "Weitermachen?", - "Public Key (xpub)": "Öffentlicher Schlüssel (xpub)", + "PSBT": "PSBT", + "Public Key:\n\n%s": "Öffentlicher Schlüssel:\n\n%s", "Roll %d": "Rolle %d", "Roll die %d or %d times to generate a 12- or 24-word mnemonic, respectively.": "Rollen Sie %d oder %d Mal, um ein 12- oder 24-Wort-Mnemonic zu erzeugen.", "Rolls:\n\n%s": "Rollen:\n\n%s", - "SHA256 of rolls:\n\n%s": "SHA256 von Rollen:\n\n%s", "Scan Address": "Adresse\nscannen", "Sending:\n₿%s\n\nTo:\n%s": "Senden:\n₿%s\n\nZu:\n%s", "Settings": "Einstellungen", + "SHA256 of rolls:\n\n%s": "SHA256 von Rollen:\n\n%s", + "SHA256:\n%s\n\n\n\nSign?": "SHA256:\n%s\n\n\n\nUnterzeichnen?", "Shutdown": "Abschalten", "Shutting down..": "Herunterfahren..", - "Sign PSBT": "PSBT unterzeichnen", "Sign?": "Unterzeichnen?", + "Sign": "Unterzeichnen", + "Signature:\n\n%s": "Unterschrift:\n\n%s", "Single-key": "Single-key", "Try more?": "Mehr versuchen?", "Updating bootloader..\n\n%d%%": "Bootloader aktualisieren..\n\n%d%%", "Upgrade complete.\n\nShutting down..": "Upgrade abgeschlossen.\n\nHerunterfahren..", "Upgrading firmware..\n\n%d%%": "Firmware aktualisieren..\n\n%d%%", "Via Bits": "Via Bits", - "Via D6": "Via D6", "Via D20": "Via D20", + "Via D6": "Via D6", "Via Numbers": "Via Nummern", "Via QR Code": "Via QR Code", "Via Text": "Via Text", - "WARNING:\nWallet not loaded.\n\nSome checks cannot be performed.": "WARNUNG:\nWallet nicht geladen.\n\nEinige Checks konnten nicht durchgeführt werden.", - "Wallet": "Wallet", "Wallet not found.": "Wallet nicht gefunden.", + "Wallet": "Wallet", + "WARNING:\nWallet not loaded.\n\nSome checks cannot be performed.": "WARNUNG:\nWallet nicht geladen.\n\nEinige Checks konnten nicht durchgeführt werden.", "Word %d": "Wort %d" -} +} \ No newline at end of file diff --git a/i18n/translations/en-US.json b/i18n/translations/en-US.json index 1cce848b8..a2a6c43a4 100644 --- a/i18n/translations/en-US.json +++ b/i18n/translations/en-US.json @@ -1,30 +1,32 @@ { + ",": ",", + ".": ".", "%d of %d multisig": "%d of %d multisig", "%s\n\nis a valid receive address": "%s\n\nis a valid receive address", "%s\n\nwas NOT FOUND in the first %d receive addresses": "%s\n\nwas NOT FOUND in the first %d receive addresses", - ",": ",", - ".": ".", "About": "About", - "BIP39 Mnemonic": "BIP39 Mnemonic", "Back": "Back", "Backing up bootloader..\n\n%d%%": "Backing up bootloader..\n\n%d%%", "Bad signature": "Bad signature", "Baudrate\n%s": "Baudrate\n%s", + "BIP39 Mnemonic": "BIP39 Mnemonic", "Check that address belongs to this wallet?": "Check that address belongs to this wallet?", "Checked %d receive addresses with no matches.": "Checked %d receive addresses with no matches.", "Checking receive address %d for match..": "Checking receive address %d for match..", "Continue?": "Continue?", - "DEBUG": "DEBUG", "Debug": "Debug", + "DEBUG": "DEBUG", "Del": "Del", "Done?": "Done?", "Enter each word of your BIP-39 mnemonic as a number from 1 to 2048.": "Enter each word of your BIP-39 mnemonic as a number from 1 to 2048.", "Enter each word of your BIP-39 mnemonic as a series of binary digits.": "Enter each word of your BIP-39 mnemonic as a series of binary digits.", "Enter each word of your BIP-39 mnemonic.": "Enter each word of your BIP-39 mnemonic.", "Error:\n%s": "Error:\n%s", - "Failed to load PSBT": "Failed to load PSBT", + "Extended Public Key": "Extended Public Key", "Failed to load address": "Failed to load address", + "Failed to load message": "Failed to load message", "Failed to load mnemonic": "Failed to load mnemonic", + "Failed to load PSBT": "Failed to load PSBT", "Failed to load wallet": "Failed to load wallet", "Fee:\n₿%s": "Fee:\n₿%s", "Fingerprint: %s\n\nDerivation: m%s\n\n%s": "Fingerprint: %s\n\nDerivation: m%s\n\n%s", @@ -44,6 +46,7 @@ "Locale": "Locale", "Locale\n%s": "Locale\n%s", "Log Level\n%s": "Log Level\n%s", + "Message": "Message", "Missing signature file": "Missing signature file", "Mnemonic": "Mnemonic", "Multisig": "Multisig", @@ -56,31 +59,34 @@ "Printer": "Printer", "Printing\n%d / %d": "Printing\n%d / %d", "Proceed?": "Proceed?", - "Public Key (xpub)": "Public Key (xpub)", + "PSBT": "PSBT", + "Public Key:\n\n%s": "Public Key:\n\n%s", "Roll %d": "Roll %d", "Roll die %d or %d times to generate a 12- or 24-word mnemonic, respectively.": "Roll die %d or %d times to generate a 12- or 24-word mnemonic, respectively.", "Rolls:\n\n%s": "Rolls:\n\n%s", - "SHA256 of rolls:\n\n%s": "SHA256 of rolls:\n\n%s", "Scan Address": "Scan Address", "Sending:\n₿%s\n\nTo:\n%s": "Sending:\n₿%s\n\nTo:\n%s", "Settings": "Settings", + "SHA256 of rolls:\n\n%s": "SHA256 of rolls:\n\n%s", + "SHA256:\n%s\n\n\n\nSign?": "SHA256:\n%s\n\n\n\nSign?", "Shutdown": "Shutdown", "Shutting down..": "Shutting down..", - "Sign PSBT": "Sign PSBT", "Sign?": "Sign?", + "Sign": "Sign", + "Signature:\n\n%s": "Signature:\n\n%s", "Single-key": "Single-key", "Try more?": "Try more?", "Updating bootloader..\n\n%d%%": "Updating bootloader..\n\n%d%%", "Upgrade complete.\n\nShutting down..": "Upgrade complete.\n\nShutting down..", "Upgrading firmware..\n\n%d%%": "Upgrading firmware..\n\n%d%%", "Via Bits": "Via Bits", - "Via D6": "Via D6", "Via D20": "Via D20", + "Via D6": "Via D6", "Via Numbers": "Via Numbers", "Via QR Code": "Via QR Code", "Via Text": "Via Text", - "WARNING:\nWallet not loaded.\n\nSome checks cannot be performed.": "WARNING:\nWallet not loaded.\n\nSome checks cannot be performed.", - "Wallet": "Wallet", "Wallet not found.": "Wallet not found.", + "Wallet": "Wallet", + "WARNING:\nWallet not loaded.\n\nSome checks cannot be performed.": "WARNING:\nWallet not loaded.\n\nSome checks cannot be performed.", "Word %d": "Word %d" } \ No newline at end of file diff --git a/i18n/translations/es-MX.json b/i18n/translations/es-MX.json index e87df7ea3..b2a8500e7 100644 --- a/i18n/translations/es-MX.json +++ b/i18n/translations/es-MX.json @@ -1,30 +1,32 @@ { + ",": ",", + ".": ".", "%d of %d multisig": "%d de %d multisig", "%s\n\nis a valid receive address": "%s\n\nes un dirección de depósito válido", "%s\n\nwas NOT FOUND in the first %d receive addresses": "%s\n\nNO FUE ENCONTRADO en la primera %d dirección de depósito", - ",": ",", - ".": ".", "About": "Nosotros", - "BIP39 Mnemonic": "BIP39 Mnemónico", "Back": "Atrás", "Backing up bootloader..\n\n%d%%": "Copia de seguridad del cargador de arranque..\n\n%d%%", "Bad signature": "Mala asignatura", "Baudrate\n%s": "Velocidad en baudios\n%s", + "BIP39 Mnemonic": "BIP39 Mnemónico", "Check that address belongs to this wallet?": "Compruebe que la dirección pertenece a esta cartera?", "Checked %d receive addresses with no matches.": "Comprobado %d dirección de depósito sin coincidencias.", "Checking receive address %d for match..": "Comprobando la dirección de depósito %d para alguna coincidencia..", "Continue?": "Continuar?", - "DEBUG": "DEBUG", "Debug": "Debug", + "DEBUG": "DEBUG", "Del": "Del", "Done?": "Listo?", "Enter each word of your BIP-39 mnemonic as a number from 1 to 2048.": "Ingrese cada palabra de su mnemónico BIP-39 como un número del 1 al 2048.", "Enter each word of your BIP-39 mnemonic as a series of binary digits.": "Ingrese cada palabra de su mnemónico BIP-39 como una serie de dígitos binarios.", "Enter each word of your BIP-39 mnemonic.": "Ingrese cada palabra de su mnemónico BIP-39.", "Error:\n%s": "Error:\n%s", - "Failed to load PSBT": "No se puede cargar la PSBT", + "Extended Public Key": "Llave Pública", "Failed to load address": "No se puede cargar la address", + "Failed to load message": "No se pudo cargar el mensaje", "Failed to load mnemonic": "No se puede cargar la mnemotécnica", + "Failed to load PSBT": "No se puede cargar la PSBT", "Failed to load wallet": "No se puede cargar la cartera", "Fee:\n₿%s": "Comisión:\n₿%s", "Fingerprint: %s\n\nDerivation: m%s\n\n%s": "Huella Dactilar: %s\n\nDerivación: m%s\n\n%s", @@ -44,6 +46,7 @@ "Locale": "Idioma", "Locale\n%s": "Idioma\n%s", "Log Level\n%s": "Nivel de registro\n%s", + "Message": "Mensaje", "Missing signature file": "Falta de firma faltante", "Mnemonic": "Mnemotécnica", "Multisig": "Multisig", @@ -56,31 +59,34 @@ "Printer": "Impresora", "Printing\n%d / %d": "Imprimiendo\n%d / %d", "Proceed?": "Continuar?", - "Public Key (xpub)": "Llave Pública (xpub)", + "PSBT": "PSBT", + "Public Key:\n\n%s": "Llave Pública:\n\n%s", "Roll %d": "Rollo %d", "Roll die %d or %d times to generate a 12- or 24-word mnemonic, respectively.": "Roll die %d o %d veces para generar una mnemotécnica de 12 o 24 palabras, respectivamente.", "Rolls:\n\n%s": "Rollos:\n\n%s", - "SHA256 of rolls:\n\n%s": "SHA256 de rollos:\n\n%s", "Scan Address": "Escanear Dirección", "Sending:\n₿%s\n\nTo:\n%s": "Enviando:\n₿%s\n\na:\n%s", "Settings": "Ajustes", + "SHA256 of rolls:\n\n%s": "SHA256 de rollos:\n\n%s", + "SHA256:\n%s\n\n\n\nSign?": "SHA256:\n%s\n\n\n\nFirmar?", "Shutdown": "Apagar", "Shutting down..": "Apagando..", - "Sign PSBT": "Firmar PSBT", "Sign?": "Firmar?", + "Sign": "Firmar", + "Signature:\n\n%s": "Firma:\n\n%s", "Single-key": "Single-key", "Try more?": "Intentar con mas?", "Updating bootloader..\n\n%d%%": "Actualización de Bootloader..\n\n%d%%", "Upgrade complete.\n\nShutting down..": "Actualización completa.\n\nApagando..", "Upgrading firmware..\n\n%d%%": "Actualización de firmware..\n\n%d%%", "Via Bits": "Via Bits", - "Via D6": "Via D6", "Via D20": "Via D20", + "Via D6": "Via D6", "Via Numbers": "Via Números", "Via QR Code": "Via Código QR", "Via Text": "Via Texto", - "WARNING:\nWallet not loaded.\n\nSome checks cannot be performed.": "ADVERTENCIA:\nCartera no cargada.\n\nNo se pueden realizar algunas comprobaciones.", - "Wallet": "Cartera", "Wallet not found.": "No se encontró la cartera.", + "Wallet": "Cartera", + "WARNING:\nWallet not loaded.\n\nSome checks cannot be performed.": "ADVERTENCIA:\nCartera no cargada.\n\nNo se pueden realizar algunas comprobaciones.", "Word %d": "Palabra %d" -} +} \ No newline at end of file diff --git a/i18n/translations/fr-FR.json b/i18n/translations/fr-FR.json index b0a86693b..afff4aee3 100644 --- a/i18n/translations/fr-FR.json +++ b/i18n/translations/fr-FR.json @@ -1,30 +1,32 @@ { + ",": ".", + ".": ",", "%d of %d multisig": "%d de %d multisignature", "%s\n\nis a valid receive address": "%s\n\nest une adresse reçue valide", "%s\n\nwas NOT FOUND in the first %d receive addresses": "%s\n\nn'a pas été trouvé dans les premières %d adresses reçues", - ",": ".", - ".": ",", "About": "À propos", - "BIP39 Mnemonic": "BIP39 Mnémonique", "Back": "Retour", "Backing up bootloader..\n\n%d%%": "Sauvegarde du chargeur de démarrage..\n\n%d%%", "Bad signature": "Mauvaise signature", "Baudrate\n%s": "Débit en bauds\n%s", + "BIP39 Mnemonic": "BIP39 Mnémonique", "Check that address belongs to this wallet?": "Vérifiez que l'adresse appartient à cette portefeuille?", "Checked %d receive addresses with no matches.": "Adresses %d reçues vérifiées sans correspondance.", "Checking receive address %d for match..": "Vérification de l'adresse reçue %d pour correspondance..", "Continue?": "Continuer?", - "DEBUG": "DEBUG", "Debug": "Debug", + "DEBUG": "DEBUG", "Del": "Del", "Done?": "Terminé?", "Enter each word of your BIP-39 mnemonic as a number from 1 to 2048.": "Entrez chaque mot de votre BIP-39 mnémonique sous la forme d'un nombre 1 jusqu'à 2048.", "Enter each word of your BIP-39 mnemonic as a series of binary digits.": "Entrez chaque mot de votre BIP-39 mnémonique sous la forme de série de chiffre binaire.", "Enter each word of your BIP-39 mnemonic.": "Entrez chaque mot de votre BIP-39 mnémonique.", "Error:\n%s": "Erreur:\n%s", - "Failed to load PSBT": "Erreur de chargement PSBT", + "Extended Public Key": "Clé publique", "Failed to load address": "Erreur de chargement d'adresse", + "Failed to load message": "Échec du chargement du message", "Failed to load mnemonic": "Erreur de chargement mnémonique", + "Failed to load PSBT": "Erreur de chargement PSBT", "Failed to load wallet": "Erreur de chargement du portefeuille", "Fee:\n₿%s": "Frais:\n₿%s", "Fingerprint: %s\n\nDerivation: m%s\n\n%s": "Empreinte Digitale: %s\n\nDérivation: m%s\n\n%s", @@ -44,6 +46,7 @@ "Locale": "Paramètres régionaux", "Locale\n%s": "Paramètres régionaux\n%s", "Log Level\n%s": "Niveau de journalisation\n%s", + "Message": "Message", "Missing signature file": "Fichier de signature manquant", "Mnemonic": "Mnémonique", "Multisig": "Multi\nsignature", @@ -56,31 +59,34 @@ "Printer": "Imprimante", "Printing\n%d / %d": "Impression\n%d / %d", "Proceed?": "Procéder?", - "Public Key (xpub)": "Clé publique (xpub)", + "PSBT": "PSBT", + "Public Key:\n\n%s": "Clé publique:\n\n%s", "Roll %d": "Rouler %d", "Roll die %d or %d times to generate a 12- or 24-word mnemonic, respectively.": "Rouler matrice %d ou %d fois pour générer un mnémonique de 12 ou 24 mots, respectivement.", "Rolls:\n\n%s": "Rouleaux:\n\n%s", - "SHA256 of rolls:\n\n%s": "SHA256 de rouleaux:\n\n%s", "Scan Address": "Scannez l'adresse", "Sending:\n₿%s\n\nTo:\n%s": "Envoie:\n₿%s\n\nSur:\n%s", "Settings": "Paramètres", + "SHA256 of rolls:\n\n%s": "SHA256 de rouleaux:\n\n%s", + "SHA256:\n%s\n\n\n\nSign?": "SHA256:\n%s\n\n\n\nSignature?", "Shutdown": "Fermer", "Shutting down..": "Éteindre..", - "Sign PSBT": "PSBT\nsignature", "Sign?": "Signature?", + "Sign": "Signature", + "Signature:\n\n%s": "Signature:\n\n%s", "Single-key": "Clé unique", "Try more?": "Réessayer?", "Updating bootloader..\n\n%d%%": "Mise à jour du chargeur de démarrage..\n\n%d%%", "Upgrade complete.\n\nShutting down..": "Mise à niveau complète.\n\nÉteindre..", "Upgrading firmware..\n\n%d%%": "Mise à niveau du micrologiciel..\n\n%d%%", "Via Bits": "Via Bits", - "Via D6": "Via D6", "Via D20": "Via D20", + "Via D6": "Via D6", "Via Numbers": "Via Nombres", "Via QR Code": "Via QR Code", "Via Text": "Via Texte", - "WARNING:\nWallet not loaded.\n\nSome checks cannot be performed.": "Attention:\nPortefeuille non chargé.\n\nLes vérifications ne peuvent pas être effectuées.", - "Wallet": "Portefeuille", "Wallet not found.": "Portefeuille non trouvé.", + "Wallet": "Portefeuille", + "WARNING:\nWallet not loaded.\n\nSome checks cannot be performed.": "Attention:\nPortefeuille non chargé.\n\nLes vérifications ne peuvent pas être effectuées.", "Word %d": "Mot %d" } \ No newline at end of file diff --git a/i18n/translations/vi-VN.json b/i18n/translations/vi-VN.json index 4d8af8b51..236fc6884 100644 --- a/i18n/translations/vi-VN.json +++ b/i18n/translations/vi-VN.json @@ -1,30 +1,32 @@ { + ",": ",", + ".": ".", "%d of %d multisig": "%d của %d đa chữ kí", "%s\n\nis a valid receive address": "%s\n\nlà một địa chỉ khả dụng", "%s\n\nwas NOT FOUND in the first %d receive addresses": "%s\n\nkhông được tìm thấy trong số %d địa chỉ đầu tiên", - ",": ",", - ".": ".", "About": "Về chúng tôi", - "BIP39 Mnemonic": "Mã mnemonic dạng chuẩn BIP39", "Back": "Trở lại", "Backing up bootloader..\n\n%d%%": "Sao lưu bộ tải khởi động..\n\n%d%%", "Bad signature": "Chữ ký xấu", "Baudrate\n%s": "Tốc độ baud\n%s", + "BIP39 Mnemonic": "Mã mnemonic dạng chuẩn BIP39", "Check that address belongs to this wallet?": "Kiểm tra địa chỉ đó có thuộc về ví này không?", "Checked %d receive addresses with no matches.": "Đã kiểm tra %d địa chỉ nhận được và không tìm thấy tương thích/", "Checking receive address %d for match..": "Đang kiểm tra %d địa chỉ ..", "Continue?": "Tiếp tục?", - "DEBUG": "DEBUG", "Debug": "Debug", + "DEBUG": "DEBUG", "Del": "Xóa", "Done?": "Hoàn tất?", "Enter each word of your BIP-39 mnemonic as a number from 1 to 2048.": "Nhập từng kí tự của mã mnemonic BIP-39 của bạn dưới dạng số từ 1 đến 2048.", "Enter each word of your BIP-39 mnemonic as a series of binary digits.": "Nhập từng kí tự của mã mnemonic BIP-39 của bạn dưới dạng một chuỗi số nhị phân.", "Enter each word of your BIP-39 mnemonic.": "Nhập từng kí tự của mã mnemonic BIP-39 của bạn.", "Error:\n%s": "Lỗi:\n%s", - "Failed to load PSBT": "Tải PSBT thất bại", + "Extended Public Key": "Khóa công cộng", "Failed to load address": "Tải địa chỉ thất bại", + "Failed to load message": "Không tải được tin nhắn", "Failed to load mnemonic": "Tải mã mnemonic thất bại", + "Failed to load PSBT": "Tải PSBT thất bại", "Failed to load wallet": "Tải ví thất bại", "Fee:\n₿%s": "Phí:\n₿%s", "Fingerprint: %s\n\nDerivation: m%s\n\n%s": "Dấu vân tay: %s\n\nNguồn gốc: m%s\n\n%s", @@ -44,6 +46,7 @@ "Locale": "Ngôn ngữ", "Locale\n%s": "Ngôn ngữ\n%s", "Log Level\n%s": "Log Level\n%s", + "Message": "Tin nhắn", "Missing signature file": "Thiếu tập tin chữ ký", "Mnemonic": "Mã mnemonic", "Multisig": "Đa chữ kí", @@ -56,31 +59,34 @@ "Printer": "Máy in", "Printing\n%d / %d": "Đang in\n%d / %d", "Proceed?": "Thực hiện?", - "Public Key (xpub)": "Khóa công cộng (xpub)", + "PSBT": "PSBT", + "Public Key:\n\n%s": "Khóa công cộng:\n\n%s", "Roll %d": "Cuộn %d", "Roll die %d or %d times to generate a 12- or 24-word mnemonic, respectively.": "Cuộn chết %d hoặc %d lần để tạo một mnemonic 12 hoặc 24 từ, tương ứng.", "Rolls:\n\n%s": "Súc sắc cuộn:\n\n%s", - "SHA256 of rolls:\n\n%s": "SHA256 của súc sắc cuộn:\n\n%s", "Scan Address": "Quét địa chỉ", "Sending:\n₿%s\n\nTo:\n%s": "Đang gửi:\n₿%s\n\nĐến\n%s", "Settings": "Cài đặt", + "SHA256 of rolls:\n\n%s": "SHA256 của súc sắc cuộn:\n\n%s", + "SHA256:\n%s\n\n\n\nSign?": "SHA256:\n%s\n\n\n\nKí?", "Shutdown": "Tắt", "Shutting down..": "Đang tắt..", - "Sign PSBT": "Chữ kí PSBT", "Sign?": "Kí?", + "Sign": "Chữ kí", + "Signature:\n\n%s": "Chữ ký:\n\n%s", "Single-key": "Khóa đơn", "Try more?": "Thử thêm nữa?", "Updating bootloader..\n\n%d%%": "Cập nhật bộ tải khởi động..\n\n%d%%", "Upgrade complete.\n\nShutting down..": "Nâng cấp hoàn tất.\n\nĐang Tắt..", "Upgrading firmware..\n\n%d%%": "Nâng cấp firmware..\n\n%d%%", "Via Bits": "Qua Bits", - "Via D6": "Qua D6", "Via D20": "Qua D20", + "Via D6": "Qua D6", "Via Numbers": "Qua số điện thoại", "Via QR Code": "Qua mã QR", "Via Text": "Qua tin nhắn", - "WARNING:\nWallet not loaded.\n\nSome checks cannot be performed.": "CẢNH BÁO:\nKhông tải được ví.\n\nMột số kiểm tra có thể chưa được thực hiện.", - "Wallet": "Ví", "Wallet not found.": "Không tìm thấy ví.", + "Wallet": "Ví", + "WARNING:\nWallet not loaded.\n\nSome checks cannot be performed.": "CẢNH BÁO:\nKhông tải được ví.\n\nMột số kiểm tra có thể chưa được thực hiện.", "Word %d": "Kí tự %d" } \ No newline at end of file diff --git a/src/krux/firmware.py b/src/krux/firmware.py index 07123935e..8063ad945 100644 --- a/src/krux/firmware.py +++ b/src/krux/firmware.py @@ -22,10 +22,10 @@ import io import os import binascii -import secp256k1 import hashlib import time import flash +from embit import ec from .input import Input, BUTTON_ENTER from .metadata import SIGNER_PUBKEY from .display import Display @@ -149,10 +149,12 @@ def fsize(firmware_filename): return size -def sha256(firmware_filename, firmware_size): +def sha256(firmware_filename, firmware_size=None): """Returns the sha256 hash of the firmware""" hasher = hashlib.sha256() - hasher.update(b"\x00" + firmware_size.to_bytes(4, "little")) + # If firmware size is supplied, then we want a sha256 of the firmware with its header + if firmware_size is not None: + hasher.update(b"\x00" + firmware_size.to_bytes(4, "little")) with open(firmware_filename, "rb", buffering=0) as file: while True: chunk = file.read(128) @@ -185,12 +187,13 @@ def upgrade(): inp = Input() new_size = fsize(firmware_path) - new_hash = sha256(firmware_path, new_size) + firmware_hash = sha256(firmware_path) + firmware_with_header_hash = sha256(firmware_path, new_size) display.clear() display.draw_centered_text( t("New firmware detected.\n\nSHA256:\n%s\n\n\n\nInstall?") - % binascii.hexlify(new_hash).decode() + % binascii.hexlify(firmware_hash).decode() ) if inp.wait_for_button() != BUTTON_ENTER: return False @@ -201,7 +204,7 @@ def upgrade(): pubkey = None try: - pubkey = secp256k1.ec_pubkey_parse(binascii.unhexlify(SIGNER_PUBKEY)) + pubkey = ec.PublicKey.from_string(SIGNER_PUBKEY) except: display.flash_text(t("Invalid public key")) return False @@ -214,7 +217,9 @@ def upgrade(): return False try: - if not secp256k1.ecdsa_verify(sig, new_hash, pubkey): + # Parse, serialize, and reparse to ensure signature is compact prior to verification + sig = ec.Signature.parse(ec.Signature.parse(sig).serialize()) + if not pubkey.verify(sig, firmware_hash): display.flash_text(t("Bad signature")) return False except: @@ -244,7 +249,7 @@ def status_text(text): new_size, 65536, True, - new_hash, + firmware_with_header_hash, ) write_data( diff --git a/src/krux/key.py b/src/krux/key.py index 337979d0a..9b37c409c 100644 --- a/src/krux/key.py +++ b/src/krux/key.py @@ -19,9 +19,14 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# pylint: disable=W0102 import hashlib import time -import urandom + +try: + import urandom as random +except: + import random from binascii import hexlify from embit import bip32, bip39 from embit.wordlists.bip39 import WORDLIST @@ -66,6 +71,10 @@ def key_expression(self, version=None, pretty=False): self.account.to_base58(version), ) + def sign(self, message_hash): + """Signs a message with the extended master private key""" + return self.root.derive(self.derivation).sign(message_hash) + # Adapted from: https://github.com/trezor/python-mnemonic def to_mnemonic_words(entropy): @@ -88,9 +97,9 @@ def pick_final_word(ctx, words): if len(words) != 11 and len(words) != 23: raise ValueError("must provide 11 or 23 words") - urandom.seed(int(time.ticks_ms() + ctx.input.entropy)) + random.seed(int(time.ticks_ms() + ctx.input.entropy)) while True: - word = urandom.choice(WORDLIST) + word = random.choice(WORDLIST) mnemonic = " ".join(words) + " " + word if bip39.mnemonic_is_valid(mnemonic): return word diff --git a/src/krux/pages/home.py b/src/krux/pages/home.py index a7f09e0fe..e844d33aa 100644 --- a/src/krux/pages/home.py +++ b/src/krux/pages/home.py @@ -19,15 +19,18 @@ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +import binascii import gc +import hashlib import lcd +from ..baseconv import base_encode from ..display import DEFAULT_PADDING from ..psbt import PSBTSigner from ..qr import FORMAT_NONE, FORMAT_PMOFN from ..input import BUTTON_ENTER from ..wallet import Wallet, parse_address from ..i18n import t -from . import Page, Menu, MENU_CONTINUE +from . import Page, Menu, MENU_CONTINUE, MENU_EXIT class Home(Page): @@ -41,10 +44,10 @@ def __init__(self, ctx): ctx, [ (t("Mnemonic"), self.mnemonic), - (t("Public Key (xpub)"), self.public_key), + (t("Extended Public Key"), self.public_key), (t("Wallet"), self.wallet), (t("Scan Address"), self.scan_address), - (t("Sign PSBT"), self.sign_psbt), + (t("Sign"), self.sign), (t("Shutdown"), self.shutdown), ], ), @@ -178,6 +181,21 @@ def scan_address(self): self.ctx.input.wait_for_button() return MENU_CONTINUE + def sign(self): + """Handler for the 'sign' menu item""" + submenu = Menu( + self.ctx, + [ + (t("PSBT"), self.sign_psbt), + (t("Message"), self.sign_message), + (t("Back"), lambda: MENU_EXIT), + ], + ) + index, status = submenu.run_loop() + if index == len(submenu.menu) - 1: + return MENU_CONTINUE + return status + def sign_psbt(self): """Handler for the 'sign psbt' menu item""" if not self.ctx.wallet.is_loaded(): @@ -217,6 +235,56 @@ def sign_psbt(self): self.print_qr_prompt(signed_psbt, qr_format) return MENU_CONTINUE + def sign_message(self): + """Handler for the 'sign message' menu item""" + data, qr_format = self.capture_qr_code() + if data is None or qr_format != FORMAT_NONE: + self.ctx.display.flash_text(t("Failed to load message"), lcd.RED) + return MENU_CONTINUE + + data = data.encode() if isinstance(data, str) else data + + message_hash = None + if len(data) == 32: + # It's a sha256 hash already + message_hash = data + else: + if len(data) == 64: + # It may be a hex-encoded sha256 hash + try: + message_hash = binascii.unhexlify(data) + except: + pass + if message_hash is None: + # It's a message, so compute its sha256 hash + message_hash = hashlib.sha256(data).digest() + + self.ctx.display.clear() + self.ctx.display.draw_centered_text( + t("SHA256:\n%s\n\n\n\nSign?") % binascii.hexlify(message_hash).decode() + ) + if self.ctx.input.wait_for_button() != BUTTON_ENTER: + return MENU_CONTINUE + + sig = self.ctx.wallet.key.sign(message_hash) + + # Encode sig as base64 string + encoded_sig = base_encode(sig.serialize(), 64).decode() + self.ctx.display.clear() + self.ctx.display.draw_centered_text(t("Signature:\n\n%s") % encoded_sig) + self.ctx.input.wait_for_button() + self.display_qr_codes(encoded_sig, qr_format) + self.print_qr_prompt(encoded_sig, qr_format) + + pubkey = binascii.hexlify(self.ctx.wallet.key.account.sec()).decode() + self.ctx.display.clear() + self.ctx.display.draw_centered_text(t("Public Key:\n\n%s") % pubkey) + self.ctx.input.wait_for_button() + self.display_qr_codes(pubkey, qr_format) + self.print_qr_prompt(pubkey, qr_format) + + return MENU_CONTINUE + def display_wallet(self, wallet, include_qr=True): """Displays a wallet, including its label and abbreviated xpubs. If include_qr is True, a QR code of the wallet will be shown diff --git a/tests/firmware-v0.0.0.bin.bad.sig b/tests/firmware-v0.0.0.bin.bad.sig new file mode 100644 index 0000000000000000000000000000000000000000..51fa4a4b41684b9cb37ee8a95723d5517d60043e GIT binary patch literal 72 zcmV-O0Jr}zMgk!KhjbMk6Na1*#KgL6ul=4z%bnwsA}5~r*qwdXHL-={0wDmG`E0h% eoAJ$|Sx-OTo)wINZa6{AiuV*VlEWc|$$aq34kPOT literal 0 HcmV?d00001 diff --git a/tests/firmware-v0.0.0.bin.badsig b/tests/firmware-v0.0.0.bin.malformed.sig similarity index 100% rename from tests/firmware-v0.0.0.bin.badsig rename to tests/firmware-v0.0.0.bin.malformed.sig diff --git a/tests/firmware-v0.0.0.bin.sha256.txt b/tests/firmware-v0.0.0.bin.sha256.txt index a013b2f35..23ca6ac5c 100644 --- a/tests/firmware-v0.0.0.bin.sha256.txt +++ b/tests/firmware-v0.0.0.bin.sha256.txt @@ -1 +1 @@ -deeae4ddb780d3790b61dd537a008a9c06c9d148077708021c1aaa1b00705bb1 \ No newline at end of file +1af9487b14714080ce5556b4455fd06c4e0a5f719d8c0ea2b5a884e5ebfc6de7 \ No newline at end of file diff --git a/tests/firmware-v0.0.0.bin.sig b/tests/firmware-v0.0.0.bin.sig index 37ba5839e..48193760c 100644 --- a/tests/firmware-v0.0.0.bin.sig +++ b/tests/firmware-v0.0.0.bin.sig @@ -1,2 +1 @@ -1O({9"M -0[MR4\LaޢG-.F5͂SZmכj \ No newline at end of file +0D ,YtPs֖SD ^=s Lnzv Kρ_T_P3/ \ No newline at end of file diff --git a/tests/firmware-v0.0.0.bin.withheader.sha256.txt b/tests/firmware-v0.0.0.bin.withheader.sha256.txt new file mode 100644 index 000000000..a013b2f35 --- /dev/null +++ b/tests/firmware-v0.0.0.bin.withheader.sha256.txt @@ -0,0 +1 @@ +deeae4ddb780d3790b61dd537a008a9c06c9d148077708021c1aaa1b00705bb1 \ No newline at end of file diff --git a/tests/pages/test_home.py b/tests/pages/test_home.py index abcd9965f..6da52c66e 100644 --- a/tests/pages/test_home.py +++ b/tests/pages/test_home.py @@ -1,3 +1,4 @@ +import binascii from krux.display import DEFAULT_PADDING from krux.key import Key from ..shared_mocks import * @@ -701,3 +702,129 @@ def test_sign_psbt(mocker): home.capture_qr_code.assert_not_called() assert ctx.input.wait_for_button.call_count == len(case[9]) + + +def test_sign_message(mocker): + from krux.pages.home import Home + from krux.wallet import Wallet + + cases = [ + # Hex-encoded hash, Sign, No print prompt + ( + "1af9487b14714080ce5556b4455fd06c4e0a5f719d8c0ea2b5a884e5ebfc6de7", + FORMAT_NONE, + None, + [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], + "MEQCID/PulsmI+E1HhJ55HdJJnKoMbUHw3c1WZnSrHqW5jlKAiB+vPbnRtmw6R9ZP8jUB8o02n+6QsX9uKy3hDiv9R2SuA==", + "02707a62fdacc26ea9b63b1c197906f56ee0180d0bcf1966e1a2da34f5f3a09a9b", + ), + # Hash, Sign, No print prompt + ( + binascii.unhexlify( + "1af9487b14714080ce5556b4455fd06c4e0a5f719d8c0ea2b5a884e5ebfc6de7" + ), + FORMAT_NONE, + None, + [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], + "MEQCID/PulsmI+E1HhJ55HdJJnKoMbUHw3c1WZnSrHqW5jlKAiB+vPbnRtmw6R9ZP8jUB8o02n+6QsX9uKy3hDiv9R2SuA==", + "02707a62fdacc26ea9b63b1c197906f56ee0180d0bcf1966e1a2da34f5f3a09a9b", + ), + # Message, Sign, No print prompt + ( + "hello world", + FORMAT_NONE, + None, + [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], + "MEQCIHKmpv1+vgPpFTN0JXjyrMK2TtLHVeJJ2TydPYmEt0RnAiBJVt/Y61ef5VlWjG08zf92AeF++BWdYm1Yd9IEy2cSqA==", + "02707a62fdacc26ea9b63b1c197906f56ee0180d0bcf1966e1a2da34f5f3a09a9b", + ), + # 64-byte message, Sign, No print prompt + ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", + FORMAT_NONE, + None, + [BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER, BUTTON_ENTER], + "MEQCIEHpCMfQ+5mBAOH//OCxF6iojpVtIS6G7X+3r3qB/0CaAiAkbjW2SGrPLvju+O05yH2x/4EKL2qlkdWnquiVkUY3jQ==", + "02707a62fdacc26ea9b63b1c197906f56ee0180d0bcf1966e1a2da34f5f3a09a9b", + ), + # Hex-encoded hash, Sign, Print + ( + "1af9487b14714080ce5556b4455fd06c4e0a5f719d8c0ea2b5a884e5ebfc6de7", + FORMAT_NONE, + MockPrinter(), + [ + BUTTON_ENTER, + BUTTON_ENTER, + BUTTON_ENTER, + BUTTON_ENTER, + BUTTON_ENTER, + BUTTON_ENTER, + BUTTON_ENTER, + ], + "MEQCID/PulsmI+E1HhJ55HdJJnKoMbUHw3c1WZnSrHqW5jlKAiB+vPbnRtmw6R9ZP8jUB8o02n+6QsX9uKy3hDiv9R2SuA==", + "02707a62fdacc26ea9b63b1c197906f56ee0180d0bcf1966e1a2da34f5f3a09a9b", + ), + # Hex-encoded hash, Sign, Decline to print + ( + "1af9487b14714080ce5556b4455fd06c4e0a5f719d8c0ea2b5a884e5ebfc6de7", + FORMAT_NONE, + MockPrinter(), + [ + BUTTON_ENTER, + BUTTON_ENTER, + BUTTON_ENTER, + BUTTON_ENTER, + BUTTON_PAGE, + BUTTON_ENTER, + BUTTON_PAGE, + ], + "MEQCID/PulsmI+E1HhJ55HdJJnKoMbUHw3c1WZnSrHqW5jlKAiB+vPbnRtmw6R9ZP8jUB8o02n+6QsX9uKy3hDiv9R2SuA==", + "02707a62fdacc26ea9b63b1c197906f56ee0180d0bcf1966e1a2da34f5f3a09a9b", + ), + # Hex-encoded hash, Decline to sign + ( + "1af9487b14714080ce5556b4455fd06c4e0a5f719d8c0ea2b5a884e5ebfc6de7", + FORMAT_NONE, + None, + [BUTTON_PAGE], + None, + None, + ), + # Failed to capture message QR + (None, FORMAT_NONE, None, [], None, None), + ] + for case in cases: + wallet = Wallet(SINGLEKEY_SIGNING_KEY) + + ctx = mock.MagicMock( + input=mock.MagicMock(wait_for_button=mock.MagicMock(side_effect=case[3])), + wallet=wallet, + printer=case[2], + ) + + home = Home(ctx) + mocker.patch.object(home, "capture_qr_code", new=lambda: (case[0], case[1])) + mocker.patch.object( + home, + "display_qr_codes", + new=lambda data, qr_format, title=None, title_padding=10: ctx.input.wait_for_button(), + ) + mocker.spy(home, "print_qr_prompt") + mocker.spy(home, "capture_qr_code") + mocker.spy(home, "display_qr_codes") + + home.sign_message() + + home.capture_qr_code.assert_called_once() + if case[0] and case[3][0] == BUTTON_ENTER: + home.display_qr_codes.assert_has_calls( + [mock.call(case[4], case[1]), mock.call(case[5], case[1])] + ) + home.print_qr_prompt.assert_has_calls( + [mock.call(case[4], case[1]), mock.call(case[5], case[1])] + ) + else: + home.display_qr_codes.assert_not_called() + home.print_qr_prompt.assert_not_called() + + assert ctx.input.wait_for_button.call_count == len(case[3]) diff --git a/tests/test_firmware.py b/tests/test_firmware.py index 70f77f723..e9a485da7 100644 --- a/tests/test_firmware.py +++ b/tests/test_firmware.py @@ -12,17 +12,25 @@ MAX_FIRMWARE_SIZE, ) from .shared_mocks import * +from embit import ec -TEST_SIGNER_PRIVKEY = "0de8a470e0616b52c0658c38b85a5709b0ec4cdff6c57d2d04ab99554cc62421" TEST_SIGNER_PUBKEY = ( - "0352c03cc5ad8b146248fd12a9a48ce2d6a236f14dd2ab7e65ec9c9611659e5642" + "03dc8ffc42845af7a30be30e7fc342363f43bc6d11a6c1d833e4e2fc339a1d0491" ) +TEST_SIGNER_PUBLIC_KEY = ec.PublicKey.from_string(TEST_SIGNER_PUBKEY) TEST_FIRMWARE_FILENAME = os.path.join(os.path.dirname(__file__), "firmware-v0.0.0.bin") TEST_FIRMWARE = open(TEST_FIRMWARE_FILENAME, "rb").read() TEST_FIRMWARE_SHA256 = open(TEST_FIRMWARE_FILENAME + ".sha256.txt", "r").read() +TEST_FIRMWARE_WITH_HEADER_SHA256 = open( + TEST_FIRMWARE_FILENAME + ".withheader.sha256.txt", "r" +).read() TEST_FIRMWARE_SIG = open(TEST_FIRMWARE_FILENAME + ".sig", "rb").read() -TEST_FIRMWARE_BADSIG = open(TEST_FIRMWARE_FILENAME + ".badsig", "rb").read() +TEST_FIRMWARE_SIGNATURE = ec.Signature.parse(TEST_FIRMWARE_SIG) +TEST_FIRMWARE_MALFORMED_SIG = open( + TEST_FIRMWARE_FILENAME + ".malformed.sig", "rb" +).read() +TEST_FIRMWARE_BAD_SIG = open(TEST_FIRMWARE_FILENAME + ".bad.sig", "rb").read() SECTOR_WITH_ACTIVE_FIRMWARE_AT_INDEX_1_SLOT_1 = [ 0x5A, @@ -12733,9 +12741,6 @@ def read(self, num_bytes): def test_upgrade(mocker): mocker.patch("krux.firmware.flash", new=mock.MagicMock()) - mocker.patch( - "krux.firmware.secp256k1", new=mock.MagicMock(wraps=sys.modules["secp256k1"]) - ) mocker.patch( "builtins.open", new=get_mock_open( @@ -12760,6 +12765,12 @@ def test_upgrade(mocker): return_value=bytes(SECTOR_WITH_ACTIVE_FIRMWARE_AT_INDEX_1_SLOT_1) ), ) + mocker.patch("krux.firmware.ec", new=mock.MagicMock(wraps=ec)) + mocker.spy(TEST_SIGNER_PUBLIC_KEY, "verify") + mocker.patch( + "krux.firmware.ec.PublicKey.from_string", + new=mock.MagicMock(return_value=TEST_SIGNER_PUBLIC_KEY), + ) import krux from krux import firmware @@ -12768,18 +12779,15 @@ def test_upgrade(mocker): assert firmware.upgrade() - krux.firmware.secp256k1.ec_pubkey_parse.assert_called_with( - binascii.unhexlify(TEST_SIGNER_PUBKEY) - ) - krux.firmware.secp256k1.ecdsa_verify.assert_called_with( - TEST_FIRMWARE_SIG, + krux.firmware.ec.PublicKey.from_string.assert_called_with(TEST_SIGNER_PUBKEY) + krux.firmware.ec.Signature.parse.assert_called_with(TEST_FIRMWARE_SIG) + TEST_SIGNER_PUBLIC_KEY.verify.assert_called_with( + TEST_FIRMWARE_SIGNATURE, binascii.unhexlify(TEST_FIRMWARE_SHA256), - krux.firmware.secp256k1.ec_pubkey_parse(binascii.unhexlify(TEST_SIGNER_PUBKEY)), ) - assert krux.firmware.secp256k1.ecdsa_verify( - TEST_FIRMWARE_SIG, + assert TEST_SIGNER_PUBLIC_KEY.verify( + TEST_FIRMWARE_SIGNATURE, binascii.unhexlify(TEST_FIRMWARE_SHA256), - krux.firmware.secp256k1.ec_pubkey_parse(binascii.unhexlify(TEST_SIGNER_PUBKEY)), ) krux.firmware.flash.read.assert_called_with(MAIN_BOOT_CONFIG_SECTOR_ADDRESS, 4096) @@ -12800,7 +12808,7 @@ def test_upgrade(mocker): len(TEST_FIRMWARE), 65536, True, - binascii.unhexlify(TEST_FIRMWARE_SHA256), + binascii.unhexlify(TEST_FIRMWARE_WITH_HEADER_SHA256), ), mock.call( mock.ANY, @@ -12822,9 +12830,6 @@ def test_upgrade(mocker): def test_upgrade_uses_backup_sector_when_main_sector_is_missing_active_firmware(mocker): mocker.patch("krux.firmware.flash", new=mock.MagicMock()) - mocker.patch( - "krux.firmware.secp256k1", new=mock.MagicMock(wraps=sys.modules["secp256k1"]) - ) mocker.patch( "builtins.open", new=get_mock_open( @@ -12852,6 +12857,12 @@ def test_upgrade_uses_backup_sector_when_main_sector_is_missing_active_firmware( ] ), ) + mocker.patch("krux.firmware.ec", new=mock.MagicMock(wraps=ec)) + mocker.spy(TEST_SIGNER_PUBLIC_KEY, "verify") + mocker.patch( + "krux.firmware.ec.PublicKey.from_string", + new=mock.MagicMock(return_value=TEST_SIGNER_PUBLIC_KEY), + ) import krux from krux import firmware @@ -12860,18 +12871,15 @@ def test_upgrade_uses_backup_sector_when_main_sector_is_missing_active_firmware( assert firmware.upgrade() - krux.firmware.secp256k1.ec_pubkey_parse.assert_called_with( - binascii.unhexlify(TEST_SIGNER_PUBKEY) - ) - krux.firmware.secp256k1.ecdsa_verify.assert_called_with( - TEST_FIRMWARE_SIG, + krux.firmware.ec.PublicKey.from_string.assert_called_with(TEST_SIGNER_PUBKEY) + krux.firmware.ec.Signature.parse.assert_called_with(TEST_FIRMWARE_SIG) + TEST_SIGNER_PUBLIC_KEY.verify.assert_called_with( + TEST_FIRMWARE_SIGNATURE, binascii.unhexlify(TEST_FIRMWARE_SHA256), - krux.firmware.secp256k1.ec_pubkey_parse(binascii.unhexlify(TEST_SIGNER_PUBKEY)), ) - assert krux.firmware.secp256k1.ecdsa_verify( - TEST_FIRMWARE_SIG, + assert TEST_SIGNER_PUBLIC_KEY.verify( + TEST_FIRMWARE_SIGNATURE, binascii.unhexlify(TEST_FIRMWARE_SHA256), - krux.firmware.secp256k1.ec_pubkey_parse(binascii.unhexlify(TEST_SIGNER_PUBKEY)), ) krux.firmware.flash.read.assert_has_calls( @@ -12897,7 +12905,7 @@ def test_upgrade_uses_backup_sector_when_main_sector_is_missing_active_firmware( len(TEST_FIRMWARE), 65536, True, - binascii.unhexlify(TEST_FIRMWARE_SHA256), + binascii.unhexlify(TEST_FIRMWARE_WITH_HEADER_SHA256), ), mock.call( mock.ANY, @@ -12919,9 +12927,6 @@ def test_upgrade_uses_backup_sector_when_main_sector_is_missing_active_firmware( def test_upgrade_uses_slot_1_when_firmware_is_in_slot_2(mocker): mocker.patch("krux.firmware.flash", new=mock.MagicMock()) - mocker.patch( - "krux.firmware.secp256k1", new=mock.MagicMock(wraps=sys.modules["secp256k1"]) - ) mocker.patch( "builtins.open", new=get_mock_open( @@ -12946,6 +12951,12 @@ def test_upgrade_uses_slot_1_when_firmware_is_in_slot_2(mocker): return_value=bytes(SECTOR_WITH_ACTIVE_FIRMWARE_AT_INDEX_1_SLOT_2) ), ) + mocker.patch("krux.firmware.ec", new=mock.MagicMock(wraps=ec)) + mocker.spy(TEST_SIGNER_PUBLIC_KEY, "verify") + mocker.patch( + "krux.firmware.ec.PublicKey.from_string", + new=mock.MagicMock(return_value=TEST_SIGNER_PUBLIC_KEY), + ) import krux from krux import firmware @@ -12954,18 +12965,15 @@ def test_upgrade_uses_slot_1_when_firmware_is_in_slot_2(mocker): assert firmware.upgrade() - krux.firmware.secp256k1.ec_pubkey_parse.assert_called_with( - binascii.unhexlify(TEST_SIGNER_PUBKEY) - ) - krux.firmware.secp256k1.ecdsa_verify.assert_called_with( - TEST_FIRMWARE_SIG, + krux.firmware.ec.PublicKey.from_string.assert_called_with(TEST_SIGNER_PUBKEY) + krux.firmware.ec.Signature.parse.assert_called_with(TEST_FIRMWARE_SIG) + TEST_SIGNER_PUBLIC_KEY.verify.assert_called_with( + TEST_FIRMWARE_SIGNATURE, binascii.unhexlify(TEST_FIRMWARE_SHA256), - krux.firmware.secp256k1.ec_pubkey_parse(binascii.unhexlify(TEST_SIGNER_PUBKEY)), ) - assert krux.firmware.secp256k1.ecdsa_verify( - TEST_FIRMWARE_SIG, + assert TEST_SIGNER_PUBLIC_KEY.verify( + TEST_FIRMWARE_SIGNATURE, binascii.unhexlify(TEST_FIRMWARE_SHA256), - krux.firmware.secp256k1.ec_pubkey_parse(binascii.unhexlify(TEST_SIGNER_PUBKEY)), ) krux.firmware.flash.read.assert_called_with(MAIN_BOOT_CONFIG_SECTOR_ADDRESS, 4096) @@ -12986,7 +12994,7 @@ def test_upgrade_uses_slot_1_when_firmware_is_in_slot_2(mocker): len(TEST_FIRMWARE), 65536, True, - binascii.unhexlify(TEST_FIRMWARE_SHA256), + binascii.unhexlify(TEST_FIRMWARE_WITH_HEADER_SHA256), ), mock.call( mock.ANY, @@ -13068,9 +13076,6 @@ def test_upgrade_fails_when_firmware_too_big(mocker): def test_upgrade_fails_when_pubkey_is_invalid(mocker): - mocker.patch( - "krux.firmware.secp256k1", new=mock.MagicMock(wraps=sys.modules["secp256k1"]) - ) mocker.patch( "builtins.open", new=get_mock_open( @@ -13119,9 +13124,6 @@ def test_upgrade_fails_when_sig_file_missing(mocker): def test_upgrade_fails_when_sig_is_invalid(mocker): - mocker.patch( - "krux.firmware.secp256k1", new=mock.MagicMock(wraps=sys.modules["secp256k1"]) - ) mocker.patch( "builtins.open", new=get_mock_open( @@ -13145,16 +13147,37 @@ def test_upgrade_fails_when_sig_is_invalid(mocker): assert not firmware.upgrade() -def test_upgrade_fails_when_sig_is_bad(mocker): +def test_upgrade_fails_when_sig_is_malformed(mocker): mocker.patch( - "krux.firmware.secp256k1", new=mock.MagicMock(wraps=sys.modules["secp256k1"]) + "builtins.open", + new=get_mock_open( + { + "/sd/firmware-v0.0.0.bin": TEST_FIRMWARE, + "/sd/firmware-v0.0.0.bin.sig": TEST_FIRMWARE_MALFORMED_SIG, + } + ), ) + mocker.patch( + "os.listdir", + new=mock.MagicMock( + return_value=["firmware-v0.0.0.bin", "firmware-v0.0.0.bin.sig"] + ), + ) + mocker.patch("krux.firmware.Display", new=mock.MagicMock()) + mocker.patch("krux.firmware.Input", new=MockSuccessInput) + mocker.patch("krux.firmware.SIGNER_PUBKEY", TEST_SIGNER_PUBKEY) + from krux import firmware + + assert not firmware.upgrade() + + +def test_upgrade_fails_when_sig_is_bad(mocker): mocker.patch( "builtins.open", new=get_mock_open( { "/sd/firmware-v0.0.0.bin": TEST_FIRMWARE, - "/sd/firmware-v0.0.0.bin.sig": TEST_FIRMWARE_BADSIG, + "/sd/firmware-v0.0.0.bin.sig": TEST_FIRMWARE_BAD_SIG, } ), ) @@ -13174,9 +13197,6 @@ def test_upgrade_fails_when_sig_is_bad(mocker): def test_upgrade_fails_when_both_sectors_missing_active_firmware(mocker): mocker.patch("krux.firmware.flash", new=mock.MagicMock()) - mocker.patch( - "krux.firmware.secp256k1", new=mock.MagicMock(wraps=sys.modules["secp256k1"]) - ) mocker.patch( "builtins.open", new=get_mock_open( diff --git a/tests/test_key.py b/tests/test_key.py index bf26e8f3b..029227fd5 100644 --- a/tests/test_key.py +++ b/tests/test_key.py @@ -1,7 +1,9 @@ +import binascii import hashlib import pytest from .shared_mocks import * from embit.networks import NETWORKS +from embit import ec TEST_12_WORD_MNEMONIC = ( "olympic term tissue route sense program under choose bean emerge velvet absurd" @@ -23,6 +25,14 @@ TEST_P2WSH_ZPUB = "Vpub5j5qqZeDSW2z2PiEnggvphpeiz1ipDtXHAAwMnV7quhsEWZnv5xrSwDm2hyxsyHLzeUM4EVX3P9V82inZkpeLpszEkwwsk1jNYq63ygjZ6V" TEST_P2WPKH_ZPUB = "vpub5YBkiKumsYUcbpYrr2DwzdUr1ByTbsCvxtXGSXDaU8sTcKzt9gaaMpMqE12VKY4SmBQNBeVQAAkyzs72GXfhCLmKQHqYULYjUpZDU4Y7tv6" +TEST_HASH = binascii.unhexlify( + "1af9487b14714080ce5556b4455fd06c4e0a5f719d8c0ea2b5a884e5ebfc6de7" +) +TEST_INVALID_HASH = binascii.unhexlify("deadbeef") +TEST_SIG = binascii.unhexlify( + "3044022050655d5880d719680a445bc4dbf31971869a37cc30d874480d740ee82407f43102202352f1e779e2de5a63dd90dc210db6ff80544ec0cf7410e8ffda1d2d4f002740" +) + def mock_modules(mocker): from embit import bip39, bip32 @@ -171,6 +181,27 @@ def test_key_expression(mocker): key.account.to_base58.assert_called() +def test_sign(mocker): + mock_modules(mocker) + from krux.key import Key + + key = Key(TEST_MNEMONIC, False) + + signature = key.sign(TEST_HASH) + assert isinstance(signature, ec.Signature) + assert signature.serialize() == TEST_SIG + + +def test_sign_fails_with_invalid_hash(mocker): + mock_modules(mocker) + from krux.key import Key + + key = Key(TEST_MNEMONIC, False) + + with pytest.raises(ValueError): + key.sign(TEST_INVALID_HASH) + + def test_to_mnemonic_words(mocker): mock_modules(mocker) from krux.key import to_mnemonic_words diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 47aa79f94..8291836bf 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -493,23 +493,23 @@ def test_parse_address(): "3KLoUhwLihgC5aPQPFHakWUtJ4QoBkT7Aw", ), ( - "bitcoin:14ihRbmxbgZ6JN9HdDDo6u6nGradHDy4GJ?note=test", + "bitcoin:14ihRbmxbgZ6JN9HdDDo6u6nGradHDy4GJ?message=test", "14ihRbmxbgZ6JN9HdDDo6u6nGradHDy4GJ", ), ( - "bitcoin:1BRwWQ3GHabCV5DP6MfnCpr6dF6GBAwQ7k?note=test", + "bitcoin:1BRwWQ3GHabCV5DP6MfnCpr6dF6GBAwQ7k?message=test", "1BRwWQ3GHabCV5DP6MfnCpr6dF6GBAwQ7k", ), ( - "bitcoin:bc1qx2zuday8d6j4ufh4df6e9ttd06lnfmn2cuz0vn?note=test", + "bitcoin:bc1qx2zuday8d6j4ufh4df6e9ttd06lnfmn2cuz0vn?message=test", "bc1qx2zuday8d6j4ufh4df6e9ttd06lnfmn2cuz0vn", ), ( - "bitcoin:32iCX1pY1iztdgM5qzurGLPMu5xhNfAUtg?note=test", + "bitcoin:32iCX1pY1iztdgM5qzurGLPMu5xhNfAUtg?message=test", "32iCX1pY1iztdgM5qzurGLPMu5xhNfAUtg", ), ( - "bitcoin:3KLoUhwLihgC5aPQPFHakWUtJ4QoBkT7Aw?note=test", + "bitcoin:3KLoUhwLihgC5aPQPFHakWUtJ4QoBkT7Aw?message=test", "3KLoUhwLihgC5aPQPFHakWUtJ4QoBkT7Aw", ), ] From 4faf53f5a5e105f53165ddc9963b7cd959a7ab17 Mon Sep 17 00:00:00 2001 From: Jeff S Date: Mon, 21 Mar 2022 10:49:36 +0000 Subject: [PATCH 2/5] Render startup logo for shorter duration --- src/boot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/boot.py b/src/boot.py index bf00079aa..5764f74cb 100644 --- a/src/boot.py +++ b/src/boot.py @@ -63,7 +63,7 @@ ctx = Context() -ctx.display.flash_text(SPLASH.split("\n"), color=lcd.WHITE, padding=8) +ctx.display.flash_text(SPLASH.split("\n"), color=lcd.WHITE, padding=8, duration=1000) while True: if not Login(ctx).run(): From 97cbf8d9aecf8c31c5e128bfb7a8dcbbb61e392e Mon Sep 17 00:00:00 2001 From: Jeff S Date: Mon, 21 Mar 2022 10:50:45 +0000 Subject: [PATCH 3/5] Update URL --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ef9ff005e..18e15f2ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: with: context: . push: false - tags: jreesun/krux-builder:latest + tags: selfcustody/krux-builder:latest cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max - name: Move cache From daaaf026e70bab41e0bc2b6263ad7dcac0781f10 Mon Sep 17 00:00:00 2001 From: Jeff S Date: Mon, 21 Mar 2022 10:56:39 +0000 Subject: [PATCH 4/5] Add krux script helpers to generate keypairs, sign, and verify sigs --- .gitignore | 9 +- Dockerfile | 6 +- firmware/scripts/create_private_key.py | 25 ----- firmware/scripts/derive_public_key.py | 38 ------- firmware/scripts/sign.py | 74 ------------- firmware/scripts/verify_signature.py | 64 ------------ krux | 139 ++++++++++++++++++------- 7 files changed, 113 insertions(+), 242 deletions(-) delete mode 100644 firmware/scripts/create_private_key.py delete mode 100644 firmware/scripts/derive_public_key.py delete mode 100644 firmware/scripts/sign.py delete mode 100644 firmware/scripts/verify_signature.py diff --git a/.gitignore b/.gitignore index 7cba452f8..01789dd7a 100644 --- a/.gitignore +++ b/.gitignore @@ -59,10 +59,17 @@ sitemap.xml.gz *.bin *.bin.sig *.bin.sha256.txt -kboot.kfpkg +*.kfpkg +*.kfpkg.sig +*.kfpkg.sha256.txt +*.zip +*.zip.sig +*.zip.sha256.txt memzip-files.c memzip-files.zip .coverage +privkey.pem +pubkey.pem !tests/firmware-v0.0.0.bin !tests/firmware-v0.0.0.bin.badsig diff --git a/Dockerfile b/Dockerfile index c48cf6177..286571d33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -70,7 +70,11 @@ RUN pip3 install astor FROM build-base as build-software ARG DEVICE="maixpy_m5stickv" -COPY . /src +RUN mkdir /src +COPY ./LICENSE.md /src/LICENSE.md +COPY ./firmware /src/firmware +COPY ./src /src/src +COPY ./vendor /src/vendor WORKDIR /src RUN cd vendor/embit && pip3 install -e . RUN mkdir build && \ diff --git a/firmware/scripts/create_private_key.py b/firmware/scripts/create_private_key.py deleted file mode 100644 index 9edb3b346..000000000 --- a/firmware/scripts/create_private_key.py +++ /dev/null @@ -1,25 +0,0 @@ -# The MIT License (MIT) - -# Copyright (c) 2021 Tom J. Sun - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -import binascii -from embit.util import key - -print(binascii.hexlify(key.generate_privkey()).decode(), end="") diff --git a/firmware/scripts/derive_public_key.py b/firmware/scripts/derive_public_key.py deleted file mode 100644 index a470e64c1..000000000 --- a/firmware/scripts/derive_public_key.py +++ /dev/null @@ -1,38 +0,0 @@ -# The MIT License (MIT) - -# Copyright (c) 2021 Tom J. Sun - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -import os -import sys -import binascii -from embit.util import secp256k1 - -private_key_hex = os.environ.get("SIGNER_PRIVKEY") -if not private_key_hex: - sys.exit("Private key must be provided") - -private_key = binascii.unhexlify(private_key_hex) - -if not secp256k1.ec_seckey_verify(private_key): - sys.exit("Private key is invalid") - -public_key = secp256k1.ec_pubkey_create(private_key) -sec = secp256k1.ec_pubkey_serialize(public_key) -print(binascii.hexlify(sec).decode(), end="") diff --git a/firmware/scripts/sign.py b/firmware/scripts/sign.py deleted file mode 100644 index 9f8ca5c19..000000000 --- a/firmware/scripts/sign.py +++ /dev/null @@ -1,74 +0,0 @@ -# The MIT License (MIT) - -# Copyright (c) 2021 Tom J. Sun - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -import os -import sys -import hashlib -import binascii -from embit.util import secp256k1 - -# Pulled from firmware.py -def fsize(firmware_filename): - """Returns the size of the firmware""" - size = 0 - with open(firmware_filename, "rb", buffering=0) as file: - while True: - chunk = file.read(128) - if not chunk: - break - size += len(chunk) - return size - - -# Pulled from firmware.py -def sha256(firmware_filename, firmware_size): - """Returns the sha256 hash of the firmware""" - hasher = hashlib.sha256() - hasher.update(b"\x00" + firmware_size.to_bytes(4, "little")) - with open(firmware_filename, "rb", buffering=0) as file: - while True: - chunk = file.read(128) - if not chunk: - break - hasher.update(chunk) - return hasher.digest() - - -private_key_hex = os.environ.get("SIGNER_PRIVKEY") -if not private_key_hex: - sys.exit("Private key must be provided") - -if len(sys.argv) != 2: - sys.exit("Firmware must be provided") - -firmware_path = sys.argv[1] -private_key = binascii.unhexlify(private_key_hex) - -if not secp256k1.ec_seckey_verify(private_key): - sys.exit("Private key is invalid") - -firmware_hash = sha256(firmware_path, fsize(firmware_path)) -with open(firmware_path + ".sha256.txt", "w") as hashfile: - hashfile.write(binascii.hexlify(firmware_hash).decode()) - -sig = secp256k1.ecdsa_sign(firmware_hash, private_key) -with open(firmware_path + ".sig", "wb") as sigfile: - sigfile.write(sig) diff --git a/firmware/scripts/verify_signature.py b/firmware/scripts/verify_signature.py deleted file mode 100644 index d311a4462..000000000 --- a/firmware/scripts/verify_signature.py +++ /dev/null @@ -1,64 +0,0 @@ -# The MIT License (MIT) - -# Copyright (c) 2021 Tom J. Sun - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -import sys -import hashlib -import binascii -from embit.util import secp256k1 - -# Pulled from firmware.py -def fsize(firmware_filename): - """Returns the size of the firmware""" - size = 0 - with open(firmware_filename, "rb", buffering=0) as file: - while True: - chunk = file.read(128) - if not chunk: - break - size += len(chunk) - return size - - -# Pulled from firmware.py -def sha256(firmware_filename, firmware_size): - """Returns the sha256 hash of the firmware""" - hasher = hashlib.sha256() - hasher.update(b"\x00" + firmware_size.to_bytes(4, "little")) - with open(firmware_filename, "rb", buffering=0) as file: - while True: - chunk = file.read(128) - if not chunk: - break - hasher.update(chunk) - return hasher.digest() - - -if len(sys.argv) != 4: - sys.exit("All arguments must be provided") - -sig = open(sys.argv[1], "rb").read() -firmware_path = sys.argv[2] -pubkey = secp256k1.ec_pubkey_parse(binascii.unhexlify(sys.argv[3])) - -if not secp256k1.ecdsa_verify(sig, sha256(firmware_path, fsize(firmware_path)), pubkey): - print("Bad signature") -else: - print("ok") diff --git a/krux b/krux index 33c8d69cf..dba634ba6 100755 --- a/krux +++ b/krux @@ -45,49 +45,24 @@ panic() { exit 1 } -if [ "$1" == "setup" ]; then - docker build . -t krux-builder -elif [ "$1" == "build" ]; then - device="$2" - version="$3" - - printf "Signer Private Key (Leave empty to generate): " - IFS= read -rs privkey - printf "\n" - if [ -z "$privkey" ]; then - privkey_cmd="docker run --rm -w "$SCRIPTS_DIR" krux-builder python3 create_private_key.py" - privkey=$($privkey_cmd) - fi +filesize() { + [ -f "$1" ] && ls -dnL -- "$1" | awk '{print $5;exit}' || { echo 0; return 1; } +} - export SIGNER_PRIVKEY="$privkey" - pubkey_cmd="docker run --rm -w "$SCRIPTS_DIR" --env SIGNER_PRIVKEY krux-builder python3 derive_public_key.py" - pubkey=$($pubkey_cmd) - export -n SIGNER_PRIVKEY - unset SIGNER_PRIVKEY +if [ "$1" == "build" ]; then + device="$2" - sed -i -- "s/SIGNER_PUBKEY = '.*'/SIGNER_PUBKEY = '$pubkey'/g" src/krux/metadata.py - sed -i -- "s/VERSION = '.*'/VERSION = '$version'/g" src/krux/metadata.py + declare $(grep version pyproject.toml | sed 's/ *= */=/g' | sed 's/"//g') + sed -i -- 's/VERSION = ".*"/VERSION = "'"$version"'"/g' src/krux/metadata.py + mkdir -p build docker build . -t krux-builder --build-arg DEVICE=$device - export SIGNER_PRIVKEY="$privkey" - sign_cmd="docker create -w "$SCRIPTS_DIR" --env SIGNER_PRIVKEY krux-builder python3 sign.py $BUILD_DIR/firmware.bin" - CID=$($sign_cmd) - docker start ${CID} - docker wait ${CID} - export -n SIGNER_PRIVKEY - unset SIGNER_PRIVKEY - - docker cp ${CID}:$BUILD_DIR/firmware.bin "firmware-v${version}.bin" - docker cp ${CID}:$BUILD_DIR/firmware.bin.sig "firmware-v${version}.bin.sig" - docker cp ${CID}:$BUILD_DIR/firmware.bin.sha256.txt "firmware-v${version}.bin.sha256.txt" - docker rm ${CID} - - if [ ! -z "$privkey_cmd" ]; then - echo "Your generated Signer Private Key is below. Please write it down somewhere safe." - echo $privkey - fi - unset privkey + # Create a container to extract the built firmware + docker create --name extract-firmware krux-builder + docker cp extract-firmware:$BUILD_DIR/firmware.bin build/firmware.bin + docker cp extract-firmware:$BUILD_DIR/kboot.kfpkg build/kboot.kfpkg + docker rm extract-firmware elif [ "$1" == "flash" ]; then device="maixpy_m5stickv" if [ ! -z "$2" ]; then @@ -109,7 +84,93 @@ elif [ "$1" == "flash" ]; then fi docker run --privileged --device=/dev/$usb_device_name:/dev/ttyUSB0 --rm -w "$BUILD_DIR" -it krux-builder python3 ktool.py -B goE -p /dev/ttyUSB0 -b 1500000 kboot.kfpkg +elif [ "$1" == "sha256" ]; then + file="$2" + hash=$(openssl dgst -sha256 $file) + hash=($hash) + echo ${hash[1]} +elif [ "$1" == "sha256-with-header" ]; then + firmware="$2" + # Get size of firmware + fsize=$(filesize $firmware) + # Convert size to little endian + fsize_byte1=`printf '%02x' "$((fsize & 255))"` + fsize_byte2=`printf '%02x' "$((fsize >> 8 & 255))"` + fsize_byte3=`printf '%02x' "$((fsize >> 16 & 255))"` + fsize_byte4=`printf '%02x' "$((fsize >> 24 & 255))"` + # Header is leading 0 byte followed by 4-byte little-endian fsize + header="\x00\x$fsize_byte1\x$fsize_byte2\x$fsize_byte3\x$fsize_byte4" + # Copy firmware binary and increase copy's size by 5 bytes + xxd $firmware | xxd -r -s 5 > $firmware.withheader + # Prepend copy with header bytes + printf "%b" $header | xxd -p | xxd -r -p - $firmware.withheader + # Output sha256 of copy with header bytes + ./krux sha256 $firmware.withheader + rm -f $firmware.withheader +elif [ "$1" == "b64decode" ]; then + b64="$2" + echo "$b64" | openssl base64 -d +elif [ "$1" == "generate-keypair" ]; then + openssl ecparam -name secp256k1 -conv_form compressed -genkey -noout -out privkey.pem + openssl ec -in privkey.pem -pubout -out pubkey.pem +elif [ "$1" == "pubkey-to-pem" ]; then + $pubkey="$2" + # https://github.com/pebwindkraft/trx_cl_suite/blob/master/tcls_key2pem.sh#L134 + pem="3036301006072a8648ce3d020106052b8104000a032200$pubkey" + pem=$( echo $pem | sed 's/[[:xdigit:]]\{2\}/\\x&/g' ) + echo "-----BEGIN PUBLIC KEY-----" + printf $pem | openssl enc -base64 + echo "-----END PUBLIC KEY-----" +elif [ "$1" == "pem-to-pubkey" ]; then + pem="$2" + pubkey=$(openssl ec -noout -text -inform PEM -in $pem -pubin | tr -d '\n') + pubkey=$( echo $pubkey | sed 's/Public-Key: (256 bit)pub://g' ) + pubkey=$( echo $pubkey | sed 's/ASN1 OID: secp256k1//g' ) + pubkey=$( echo $pubkey | sed 's/://g' ) + pubkey=$( echo $pubkey | sed 's/ //g' ) + echo $pubkey +elif [ "$1" == "sign" ]; then + file="$2" + privkey_pem="$3" + sig_bin=$file.sig + openssl dgst -sign $privkey_pem -keyform PEM -sha256 -out $sig_bin -binary $file +elif [ "$1" == "verify" ]; then + file="$2" + pubkey="$3" + sig_bin=$file.sig + openssl sha256 <$file -binary | openssl pkeyutl -verify -pubin -inkey $pubkey -sigfile $sig_bin +elif [ "$1" == "build-release" ]; then + declare $(grep version pyproject.toml | sed 's/ *= */=/g' | sed 's/"//g') + + release_dir="krux-v$version" + mkdir -p $release_dir + + cd build + if [ ! -f "ktool-linux" ]; then + wget https://github.com/selfcustody/ktool/releases/download/v0.1.0/ktool-linux + echo "ea09c20bf3812ed0f356cc1efd1c88092d957795fb851fabab5ae6bca9a406a8 ktool-linux" | sha256sum -c + fi + if [ ! -f "ktool-mac" ]; then + wget https://github.com/selfcustody/ktool/releases/download/v0.1.0/ktool-mac + echo "41ac5d87bf51b7d317b866c65dbb58f2d004502e6af6e3a3e527a622fb2217f9 ktool-mac" | sha256sum -c + fi + if [ ! -f "ktool-win.exe" ]; then + wget https://github.com/selfcustody/ktool/releases/download/v0.1.0/ktool-win.exe + echo "7b1e6af613279473d8f69050643ca6159f596735afd0d7e21b712e71f8c7775c ktool-win.exe" | sha256sum -c + fi + cp -f ktool-linux ../$release_dir/ktool-linux + cp -f ktool-mac ../$release_dir/ktool-mac + cp -f ktool-win.exe ../$release_dir/ktool-win.exe + cd .. + + devices=("maixpy_m5stickv") + for device in ${devices[@]}; do + ./krux build $device + mkdir -p $release_dir/$device + cp -f build/firmware.bin $release_dir/$device/firmware.bin + cp -f build/kboot.kfpkg $release_dir/$device/kboot.kfpkg + done elif [ "$1" == "clean" ]; then rm -rf build - docker system prune --all --force + docker system prune fi From e4d14a9f4f6e236b18aab48a36d05f29aaf9f253 Mon Sep 17 00:00:00 2001 From: Jeff S Date: Mon, 21 Mar 2022 14:32:05 +0000 Subject: [PATCH 5/5] Allow patches with lower coverage than project target --- codecov.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/codecov.yml b/codecov.yml index 1d11fd2e3..62ae5a387 100644 --- a/codecov.yml +++ b/codecov.yml @@ -4,3 +4,7 @@ coverage: default: target: 100% threshold: 5% + patch: + default: + target: 90% + threshold: 5% \ No newline at end of file