From f938506a3fa7da9505b95b98f40eea3009bd6abc Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 30 Jan 2023 09:41:12 +0200 Subject: [PATCH] add tapsigner message signing support --- drongo | 2 +- .../sparrow/control/DevicePane.java | 48 ++++++++++---- .../sparrow/control/EntryCell.java | 2 +- .../sparrow/control/MessageSignDialog.java | 21 +++--- .../sparrow/io/ckcard/CardApi.java | 66 +++++++++++++++++-- .../transaction/HeadersController.java | 4 +- .../sparrow/wallet/KeystoreController.java | 2 +- 7 files changed, 113 insertions(+), 32 deletions(-) diff --git a/drongo b/drongo index e2a4c32d..b4873964 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit e2a4c32db317b9e950cfbec822cc8103332d29ff +Subproject commit b487396417fbdf3c73c24399a778855c97a26584 diff --git a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java index ca0860fc..e02682ae 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/DevicePane.java @@ -94,6 +94,10 @@ public class DevicePane extends TitledDescriptionPane { initialise(device); + messageProperty.addListener((observable, oldValue, newValue) -> { + Platform.runLater(() -> setDescription(newValue)); + }); + buttonBox.getChildren().addAll(setPassphraseButton, importButton); } @@ -167,6 +171,10 @@ public class DevicePane extends TitledDescriptionPane { initialise(device); + messageProperty.addListener((observable, oldValue, newValue) -> { + Platform.runLater(() -> setDescription(newValue)); + }); + buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton); } @@ -581,6 +589,9 @@ public class DevicePane extends TitledDescriptionPane { } Service importService = new CardImportPane.CardImportService(importer, pin.get(), derivation); + importService.messageProperty().addListener((observable, oldValue, newValue) -> { + messageProperty.set(newValue); + }); handleCardOperation(importService, importButton, "Import", event -> { importKeystore(derivation, importService.getValue()); }); @@ -725,17 +736,32 @@ public class DevicePane extends TitledDescriptionPane { } private void signMessage() { - Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath()); - signMessageService.setOnSucceeded(successEvent -> { - String signature = signMessageService.getValue(); - EventManager.get().post(new MessageSignedEvent(wallet, signature)); - }); - signMessageService.setOnFailed(failedEvent -> { - setError("Could not sign message", signMessageService.getException().getMessage()); - signMessageButton.setDisable(false); - }); - setDescription("Signing message..."); - signMessageService.start(); + if(device.isCard()) { + try { + CardApi cardApi = new CardApi(pin.get()); + Service signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty); + handleCardOperation(signMessageService, signMessageButton, "Signing", event -> { + String signature = signMessageService.getValue(); + EventManager.get().post(new MessageSignedEvent(wallet, signature)); + }); + } catch(Exception e) { + log.error("Signing Error: " + e.getMessage(), e); + setError("Signing Error", e.getMessage()); + signButton.setDisable(false); + } + } else { + Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath()); + signMessageService.setOnSucceeded(successEvent -> { + String signature = signMessageService.getValue(); + EventManager.get().post(new MessageSignedEvent(wallet, signature)); + }); + signMessageService.setOnFailed(failedEvent -> { + setError("Could not sign message", signMessageService.getException().getMessage()); + signMessageButton.setDisable(false); + }); + setDescription("Signing message..."); + signMessageService.start(); + } } private void discoverKeystores() { diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java index 65c0eda7..54b58daf 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java @@ -353,7 +353,7 @@ public class EntryCell extends TreeTableCell implements Confirmati private static boolean canSignMessage(WalletNode walletNode) { Wallet wallet = walletNode.getWallet(); return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR && - (wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB) && + (wallet.getKeystores().get(0).hasPrivateKey() || wallet.getKeystores().get(0).getSource() == KeystoreSource.HW_USB || wallet.getKeystores().get(0).getWalletModel().isCard()) && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE); } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java index fd13dbf1..c8db21b8 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java @@ -8,10 +8,7 @@ import com.sparrowwallet.drongo.address.InvalidAddressException; import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.protocol.ScriptType; -import com.sparrowwallet.drongo.wallet.Keystore; -import com.sparrowwallet.drongo.wallet.KeystoreSource; -import com.sparrowwallet.drongo.wallet.Wallet; -import com.sparrowwallet.drongo.wallet.WalletNode; +import com.sparrowwallet.drongo.wallet.*; import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.event.OpenWalletsEvent; @@ -269,8 +266,8 @@ public class MessageSignDialog extends Dialog { if(wallet.getKeystores().size() != 1) { throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required"); } - if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB) { - throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a USB keystore"); + if(!wallet.getKeystores().get(0).hasPrivateKey() && wallet.getKeystores().get(0).getSource() != KeystoreSource.HW_USB && !wallet.getKeystores().get(0).getWalletModel().isCard()) { + throw new IllegalArgumentException("Cannot sign messages using a wallet without private keys or a connected keystore"); } } @@ -319,8 +316,8 @@ public class MessageSignDialog extends Dialog { } else { signUnencryptedKeystore(signingWallet); } - } else if(signingWallet.containsSource(KeystoreSource.HW_USB)) { - signUsbKeystore(signingWallet); + } else if(signingWallet.containsSource(KeystoreSource.HW_USB) || wallet.getKeystores().get(0).getWalletModel().isCard()) { + signDeviceKeystore(signingWallet); } } @@ -339,10 +336,10 @@ public class MessageSignDialog extends Dialog { } } - private void signUsbKeystore(Wallet usbWallet) { - List fingerprints = List.of(usbWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()); - KeyDerivation fullDerivation = usbWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation()); - DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, usbWallet, message.getText().trim(), fullDerivation); + private void signDeviceKeystore(Wallet deviceWallet) { + List fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()); + KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation()); + DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation); Optional optSignature = deviceSignMessageDialog.showAndWait(); if(optSignature.isPresent()) { signature.clear(); diff --git a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java index 8020c354..6fa78d7c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/ckcard/CardApi.java @@ -5,10 +5,7 @@ import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ECKey; -import com.sparrowwallet.drongo.protocol.Base58; -import com.sparrowwallet.drongo.protocol.Sha256Hash; -import com.sparrowwallet.drongo.protocol.SigHash; -import com.sparrowwallet.drongo.protocol.TransactionSignature; +import com.sparrowwallet.drongo.protocol.*; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBTInput; import com.sparrowwallet.drongo.psbt.PSBTInputSigner; @@ -186,6 +183,40 @@ public class CardApi { } } + public Service getSignMessageService(String message, ScriptType scriptType, List derivation, StringProperty messageProperty) { + return new SignMessageService(message, scriptType, derivation, messageProperty); + } + + String signMessage(String message, ScriptType scriptType, List derivation) throws CardException { + List keystoreDerivation = derivation.subList(0, derivation.size() - 2); + List subPathDerivation = derivation.subList(derivation.size() - 2, derivation.size()); + + Keystore cardKeystore = getKeystore(); + KeyDerivation cardKeyDerivation = cardKeystore.getKeyDerivation(); + Keystore signingKeystore = cardKeystore; + try { + if(!cardKeyDerivation.getDerivation().equals(keystoreDerivation)) { + setDerivation(keystoreDerivation); + signingKeystore = getKeystore(); + } + + WalletNode addressNode = new WalletNode(KeyDerivation.writePath(subPathDerivation)); + ECKey addressPubKey = signingKeystore.getPubKey(addressNode); + return addressPubKey.signMessage(message, scriptType, hash -> { + try { + CardSign cardSign = cardProtocol.sign(cvc, subPathDerivation, hash); + return cardSign.getSignature(); + } catch(CardException e) { + throw new RuntimeException(e); + } + }); + } finally { + if(signingKeystore != cardKeystore) { + setDerivation(cardKeystore.getKeyDerivation().getDerivation()); + } + } + } + public void disconnect() { try { cardProtocol.disconnect(); @@ -289,4 +320,31 @@ public class CardApi { return pubkey; } } + + public class SignMessageService extends Service { + private final String message; + private final ScriptType scriptType; + private final List derivation; + private final StringProperty messageProperty; + + public SignMessageService(String message, ScriptType scriptType, List derivation, StringProperty messageProperty) { + this.message = message; + this.scriptType = scriptType; + this.derivation = derivation; + this.messageProperty = messageProperty; + } + + @Override + protected Task createTask() { + return new Task<>() { + @Override + protected String call() throws Exception { + CardStatus cardStatus = getStatus(); + checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty); + + return signMessage(message, scriptType, derivation); + } + }; + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index d5ef19f3..7e2a5d03 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -754,7 +754,7 @@ public class HeadersController extends TransactionFormController implements Init Optional softwareKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_SEED)).findAny(); Optional usbKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB) || keystore.getSource().equals(KeystoreSource.SW_WATCH)).findAny(); Optional bip47Keystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_PAYMENT_CODE)).findAny(); - Optional cardKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getWalletModel().equals(WalletModel.TAPSIGNER)).findAny(); + Optional cardKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getWalletModel().isCard()).findAny(); if(softwareKeystore.isEmpty() && usbKeystore.isEmpty() && bip47Keystore.isEmpty() && cardKeystore.isEmpty()) { signButton.setDisable(true); } else if(softwareKeystore.isEmpty() && bip47Keystore.isEmpty() && usbKeystore.isEmpty()) { @@ -995,7 +995,7 @@ public class HeadersController extends TransactionFormController implements Init List fingerprints = headersForm.getSigningWallet().getKeystores().stream().map(keystore -> keystore.getKeyDerivation().getMasterFingerprint()).collect(Collectors.toList()); List signingDevices = AppServices.getDevices().stream().filter(device -> fingerprints.contains(device.getFingerprint())).collect(Collectors.toList()); if(signingDevices.isEmpty() && - (headersForm.getSigningWallet().getKeystores().stream().noneMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB) || keystore.getSource().equals(KeystoreSource.SW_WATCH) || keystore.getWalletModel().equals(WalletModel.TAPSIGNER)) || + (headersForm.getSigningWallet().getKeystores().stream().noneMatch(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB) || keystore.getSource().equals(KeystoreSource.SW_WATCH) || keystore.getWalletModel().isCard()) || (headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getSource().equals(KeystoreSource.SW_SEED)) && headersForm.getSigningWallet().getKeystores().stream().anyMatch(keystore -> keystore.getSource().equals(KeystoreSource.SW_WATCH))))) { return; } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java index aff05531..8e8fdb18 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/KeystoreController.java @@ -282,7 +282,7 @@ public class KeystoreController extends WalletFormController implements Initiali type.setGraphic(getTypeIcon(keystore)); viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed()); viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey()); - changePinButton.setVisible(keystore.getWalletModel() == WalletModel.TAPSIGNER); + changePinButton.setVisible(keystore.getWalletModel().isCard()); importButton.setText(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import..." : "Replace..."); importButton.setTooltip(new Tooltip(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import a keystore from an external source" : "Replace this keystore with another source"));