add bip322 message signing for silent payments wallets
What changed, and why it matters
This commit adds the ability for Sparrow Wallet users with 'silent payments' wallets to sign messages using the BIP322 standard. It is a feature addition that extends existing message-signing support to a new wallet type. There is no indication in the commit that it fixes a security bug or vulnerability.
No security action required. Treat as a normal feature commit. If reviewing for release notes, note the new BIP322 signing support for silent payments wallets.
Security signals we found
No security-relevant signals detected in the diff
Feature addition: BIP322 message signing for silent payments wallets
Private key material is cleared from memory after signing (spendPrivKey.clear(), privKey.clear())
Evidence from the diff
The change enables BIP322 message signing for PolicyType.SINGLE_SP (silent payments) wallets, alongside the already-supported SINGLE_HD wallets. It adds silent-payment-specific signing paths using spend private keys and silent-payment tweaks, builds PSBTs for hardware/air-gapped signing, and extracts signatures accordingly. The commit is purely additive feature work; no bug fixes or security patches are visible in the diff.
Changed components
AppController.javaEntryCell.javaMessageSignDialog.javadrongo (submodule dependency)Inspect captured patch +47 / −24
diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java
index 68ec821..2288e60 100644
--- a/src/main/java/com/sparrowwallet/sparrow/AppController.java
+++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java
@@ -1476,7 +1476,7 @@ public class AppController implements Initializable {
WalletForm selectedWalletForm = getSelectedWalletForm();
if(selectedWalletForm != null) {
Wallet wallet = selectedWalletForm.getWallet();
- if(wallet.getPolicyType() == PolicyType.SINGLE_HD) {
+ if(wallet.getPolicyType() == PolicyType.SINGLE_HD || wallet.getPolicyType() == PolicyType.SINGLE_SP) {
//Can sign and verify
messageSignDialog = new MessageSignDialog(wallet);
}
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
index e57cabf..f82dc30 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/EntryCell.java
@@ -414,7 +414,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
private static boolean canSignMessage(WalletNode walletNode) {
Wallet wallet = walletNode.getWallet();
- return wallet.getPolicyType() == PolicyType.SINGLE_HD && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
+ PolicyType policyType = wallet.getPolicyType();
+ return (policyType == PolicyType.SINGLE_HD || policyType == PolicyType.SINGLE_SP) && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
}
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
diff --git a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java
index 360d333..3aa467d 100644
--- a/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java
+++ b/src/main/java/com/sparrowwallet/sparrow/control/MessageSignDialog.java
@@ -295,8 +295,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
private void checkWalletSigning(Wallet wallet) {
- if(wallet.getKeystores().size() != 1 || wallet.getPolicyType() != PolicyType.SINGLE_HD) {
- throw new IllegalArgumentException("Cannot sign messages using a non-HD wallet or a wallet with multiple keystores");
+ if(wallet.getKeystores().size() != 1 || (wallet.getPolicyType() != PolicyType.SINGLE_HD && wallet.getPolicyType() != PolicyType.SINGLE_SP)) {
+ throw new IllegalArgumentException("Cannot sign messages using this wallet type");
}
}
@@ -377,18 +377,24 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private void signUnencryptedKeystore(Wallet decryptedWallet) {
try {
Keystore keystore = decryptedWallet.getKeystores().getFirst();
- ECKey privKey = keystore.getKey(walletNode);
String signatureText;
- if(isBip322()) {
- ScriptType scriptType = decryptedWallet.getScriptType();
- signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
+ if(decryptedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
+ ECKey spendPrivKey = keystore.getSpendPrivateKey(Collections.emptyMap());
+ signatureText = Bip322.signMessageBip322Sp(walletNode.getAddress(), message.getText().trim(), spendPrivKey, walletNode.getSilentPaymentTweak());
+ spendPrivKey.clear();
} else {
- ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
- signatureText = privKey.signMessage(message.getText().trim(), scriptType);
+ ECKey privKey = keystore.getKey(walletNode);
+ if(isBip322()) {
+ ScriptType scriptType = decryptedWallet.getScriptType();
+ signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
+ } else {
+ ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
+ signatureText = privKey.signMessage(message.getText().trim(), scriptType);
+ }
+ privKey.clear();
}
signature.clear();
signature.appendText(signatureText);
- privKey.clear();
} catch(Exception e) {
log.error("Could not sign message", e);
AppServices.showErrorDialog("Could not sign message", e.getMessage());
@@ -498,10 +504,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private void showBip322Qr() {
Wallet signingWallet = walletNode.getWallet();
- ScriptType scriptType = signingWallet.getScriptType();
-
- PSBT psbt = Bip322.getBip322Psbt(scriptType, walletNode.getAddress(), message.getText().trim());
- addBip322DerivationInfo(psbt, signingWallet);
+ PSBT psbt = buildBip322Psbt(signingWallet);
byte[] psbtBytes = psbt.getForExport().serialize();
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
@@ -514,6 +517,30 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
}
}
+ private PSBT buildBip322Psbt(Wallet signingWallet) {
+ if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
+ Keystore keystore = signingWallet.getKeystores().getFirst();
+ ECKey spendPubKey = keystore.getSilentPaymentScanAddress().getSpendKey();
+ KeyDerivation spendDerivation = new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), KeyDerivation.writePath(KeyDerivation.getBip352SpendDerivation(keystore.getKeyDerivation().getDerivation())));
+ return Bip322.getBip322PsbtSp(walletNode.getAddress(), message.getText().trim(), walletNode.getSilentPaymentTweak(), Map.of(spendPubKey, spendDerivation));
+ }
+
+ PSBT psbt = Bip322.getBip322Psbt(signingWallet.getScriptType(), walletNode.getAddress(), message.getText().trim());
+ addBip322DerivationInfo(psbt, signingWallet);
+
+ return psbt;
+ }
+
+ private String extractBip322Signature(PSBT signedPsbt) {
+ Wallet signingWallet = walletNode.getWallet();
+ if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
+ return Bip322.getBip322SignatureFromPsbtSp(signedPsbt);
+ }
+
+ ECKey pubKey = signingWallet.getKeystores().getFirst().getPubKey(walletNode);
+ return Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), signedPsbt, pubKey);
+ }
+
private void addBip322DerivationInfo(PSBT psbt, Wallet signingWallet) {
ScriptType scriptType = signingWallet.getScriptType();
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
@@ -537,9 +564,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
QRScanDialog.Result result = optionalResult.get();
if(result.psbt != null) {
try {
- Wallet signingWallet = walletNode.getWallet();
- ECKey pubKey = signingWallet.getKeystores().get(0).getPubKey(walletNode);
- String sig = Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), result.psbt, pubKey);
+ String sig = extractBip322Signature(result.psbt);
signature.clear();
signature.appendText(sig);
} catch(Exception e) {
@@ -599,9 +624,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
private void exportBip322File() {
Wallet signingWallet = walletNode.getWallet();
- ScriptType scriptType = signingWallet.getScriptType();
- PSBT psbt = Bip322.getBip322Psbt(scriptType, walletNode.getAddress(), message.getText().trim());
- addBip322DerivationInfo(psbt, signingWallet);
+ PSBT psbt = buildBip322Psbt(signingWallet);
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
@@ -642,8 +665,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
try {
byte[] psbtBytes = Files.readAllBytes(file.toPath());
PSBT signedPsbt = new PSBT(psbtBytes, false);
- ECKey pubKey = walletNode.getWallet().getKeystores().get(0).getPubKey(walletNode);
- String sig = Bip322.getBip322SignatureFromPsbt(walletNode.getWallet().getScriptType(), signedPsbt, pubKey);
+ String sig = extractBip322Signature(signedPsbt);
signature.clear();
signature.appendText(sig);
return;
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.