add tapsigner message signing support

This commit is contained in:
Craig Raw 2023-01-30 09:41:12 +02:00
parent 4fb8c5a61b
commit f938506a3f
7 changed files with 113 additions and 32 deletions

2
drongo

@ -1 +1 @@
Subproject commit e2a4c32db317b9e950cfbec822cc8103332d29ff Subproject commit b487396417fbdf3c73c24399a778855c97a26584

View file

@ -94,6 +94,10 @@ public class DevicePane extends TitledDescriptionPane {
initialise(device); initialise(device);
messageProperty.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setDescription(newValue));
});
buttonBox.getChildren().addAll(setPassphraseButton, importButton); buttonBox.getChildren().addAll(setPassphraseButton, importButton);
} }
@ -167,6 +171,10 @@ public class DevicePane extends TitledDescriptionPane {
initialise(device); initialise(device);
messageProperty.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setDescription(newValue));
});
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton); buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
} }
@ -581,6 +589,9 @@ public class DevicePane extends TitledDescriptionPane {
} }
Service<Keystore> importService = new CardImportPane.CardImportService(importer, pin.get(), derivation); Service<Keystore> importService = new CardImportPane.CardImportService(importer, pin.get(), derivation);
importService.messageProperty().addListener((observable, oldValue, newValue) -> {
messageProperty.set(newValue);
});
handleCardOperation(importService, importButton, "Import", event -> { handleCardOperation(importService, importButton, "Import", event -> {
importKeystore(derivation, importService.getValue()); importKeystore(derivation, importService.getValue());
}); });
@ -725,17 +736,32 @@ public class DevicePane extends TitledDescriptionPane {
} }
private void signMessage() { private void signMessage() {
Hwi.SignMessageService signMessageService = new Hwi.SignMessageService(device, passphrase.get(), message, keyDerivation.getDerivationPath()); if(device.isCard()) {
signMessageService.setOnSucceeded(successEvent -> { try {
String signature = signMessageService.getValue(); CardApi cardApi = new CardApi(pin.get());
EventManager.get().post(new MessageSignedEvent(wallet, signature)); Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty);
}); handleCardOperation(signMessageService, signMessageButton, "Signing", event -> {
signMessageService.setOnFailed(failedEvent -> { String signature = signMessageService.getValue();
setError("Could not sign message", signMessageService.getException().getMessage()); EventManager.get().post(new MessageSignedEvent(wallet, signature));
signMessageButton.setDisable(false); });
}); } catch(Exception e) {
setDescription("Signing message..."); log.error("Signing Error: " + e.getMessage(), e);
signMessageService.start(); 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() { private void discoverKeystores() {

View file

@ -353,7 +353,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static boolean canSignMessage(WalletNode walletNode) { private static boolean canSignMessage(WalletNode walletNode) {
Wallet wallet = walletNode.getWallet(); Wallet wallet = walletNode.getWallet();
return wallet.getKeystores().size() == 1 && wallet.getScriptType() != ScriptType.P2TR && 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); (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
} }

View file

@ -8,10 +8,7 @@ import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.PolicyType; import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.ScriptType; import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.Keystore; import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices; import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager; import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.OpenWalletsEvent; import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
@ -269,8 +266,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
if(wallet.getKeystores().size() != 1) { if(wallet.getKeystores().size() != 1) {
throw new IllegalArgumentException("Cannot sign messages using a wallet with multiple keystores - a single key is required"); 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) { 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 USB keystore"); 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<ButtonBar.ButtonData> {
} else { } else {
signUnencryptedKeystore(signingWallet); signUnencryptedKeystore(signingWallet);
} }
} else if(signingWallet.containsSource(KeystoreSource.HW_USB)) { } else if(signingWallet.containsSource(KeystoreSource.HW_USB) || wallet.getKeystores().get(0).getWalletModel().isCard()) {
signUsbKeystore(signingWallet); signDeviceKeystore(signingWallet);
} }
} }
@ -339,10 +336,10 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
} }
} }
private void signUsbKeystore(Wallet usbWallet) { private void signDeviceKeystore(Wallet deviceWallet) {
List<String> fingerprints = List.of(usbWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint()); List<String> fingerprints = List.of(deviceWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint());
KeyDerivation fullDerivation = usbWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation()); KeyDerivation fullDerivation = deviceWallet.getKeystores().get(0).getKeyDerivation().extend(walletNode.getDerivation());
DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, usbWallet, message.getText().trim(), fullDerivation); DeviceSignMessageDialog deviceSignMessageDialog = new DeviceSignMessageDialog(fingerprints, deviceWallet, message.getText().trim(), fullDerivation);
Optional<String> optSignature = deviceSignMessageDialog.showAndWait(); Optional<String> optSignature = deviceSignMessageDialog.showAndWait();
if(optSignature.isPresent()) { if(optSignature.isPresent()) {
signature.clear(); signature.clear();

View file

@ -5,10 +5,7 @@ import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.Base58; import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.SigHash;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput; import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTInputSigner; import com.sparrowwallet.drongo.psbt.PSBTInputSigner;
@ -186,6 +183,40 @@ public class CardApi {
} }
} }
public Service<String> getSignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty) {
return new SignMessageService(message, scriptType, derivation, messageProperty);
}
String signMessage(String message, ScriptType scriptType, List<ChildNumber> derivation) throws CardException {
List<ChildNumber> keystoreDerivation = derivation.subList(0, derivation.size() - 2);
List<ChildNumber> 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() { public void disconnect() {
try { try {
cardProtocol.disconnect(); cardProtocol.disconnect();
@ -289,4 +320,31 @@ public class CardApi {
return pubkey; return pubkey;
} }
} }
public class SignMessageService extends Service<String> {
private final String message;
private final ScriptType scriptType;
private final List<ChildNumber> derivation;
private final StringProperty messageProperty;
public SignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty) {
this.message = message;
this.scriptType = scriptType;
this.derivation = derivation;
this.messageProperty = messageProperty;
}
@Override
protected Task<String> createTask() {
return new Task<>() {
@Override
protected String call() throws Exception {
CardStatus cardStatus = getStatus();
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
return signMessage(message, scriptType, derivation);
}
};
}
}
} }

View file

@ -754,7 +754,7 @@ public class HeadersController extends TransactionFormController implements Init
Optional<Keystore> softwareKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_SEED)).findAny(); Optional<Keystore> softwareKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_SEED)).findAny();
Optional<Keystore> usbKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB) || keystore.getSource().equals(KeystoreSource.SW_WATCH)).findAny(); Optional<Keystore> usbKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.HW_USB) || keystore.getSource().equals(KeystoreSource.SW_WATCH)).findAny();
Optional<Keystore> bip47Keystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_PAYMENT_CODE)).findAny(); Optional<Keystore> bip47Keystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getSource().equals(KeystoreSource.SW_PAYMENT_CODE)).findAny();
Optional<Keystore> cardKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getWalletModel().equals(WalletModel.TAPSIGNER)).findAny(); Optional<Keystore> cardKeystore = signingWallet.getKeystores().stream().filter(keystore -> keystore.getWalletModel().isCard()).findAny();
if(softwareKeystore.isEmpty() && usbKeystore.isEmpty() && bip47Keystore.isEmpty() && cardKeystore.isEmpty()) { if(softwareKeystore.isEmpty() && usbKeystore.isEmpty() && bip47Keystore.isEmpty() && cardKeystore.isEmpty()) {
signButton.setDisable(true); signButton.setDisable(true);
} else if(softwareKeystore.isEmpty() && bip47Keystore.isEmpty() && usbKeystore.isEmpty()) { } else if(softwareKeystore.isEmpty() && bip47Keystore.isEmpty() && usbKeystore.isEmpty()) {
@ -995,7 +995,7 @@ public class HeadersController extends TransactionFormController implements Init
List<String> fingerprints = headersForm.getSigningWallet().getKeystores().stream().map(keystore -> keystore.getKeyDerivation().getMasterFingerprint()).collect(Collectors.toList()); List<String> fingerprints = headersForm.getSigningWallet().getKeystores().stream().map(keystore -> keystore.getKeyDerivation().getMasterFingerprint()).collect(Collectors.toList());
List<Device> signingDevices = AppServices.getDevices().stream().filter(device -> fingerprints.contains(device.getFingerprint())).collect(Collectors.toList()); List<Device> signingDevices = AppServices.getDevices().stream().filter(device -> fingerprints.contains(device.getFingerprint())).collect(Collectors.toList());
if(signingDevices.isEmpty() && 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))))) { (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; return;
} }

View file

@ -282,7 +282,7 @@ public class KeystoreController extends WalletFormController implements Initiali
type.setGraphic(getTypeIcon(keystore)); type.setGraphic(getTypeIcon(keystore));
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed()); viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed());
viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey()); 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.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")); importButton.setTooltip(new Tooltip(keystore.getSource() == KeystoreSource.SW_WATCH ? "Import a keystore from an external source" : "Replace this keystore with another source"));