require the policy line and check the declared cosigner count when importing coldcard format multisig wallets
What changed, and why it matters
This commit tightens how Sparrow Wallet imports multisig wallet files from Coldcard. Previously, if the file omitted the 'Policy' line or listed fewer cosigner keys than the policy required, Sparrow would silently assume a default 2-of-2 setup. That could let a malformed or attacker-crafted import file create a wallet whose security rules don't match what the user expects. The patch now requires the policy line and verifies the number of cosigner keys matches the declared policy, throwing an error if they don't.
Users should upgrade to a Sparrow release containing this commit before importing Coldcard-format multisig wallets. Treat any previously imported Coldcard multisig wallets as potentially misconfigured if the source file lacked a Policy line or had an inconsistent cosigner count; re-import from a verified source or verify the wallet policy against the intended M-of-N.
Security signals we found
Input validation added to wallet import parser
Previously silent default fallback for missing policy removed
Mismatch between declared cosigner count and supplied keys now rejected
New negative test fixtures for missing policy and missing cosigner
Evidence from the diff
ColdcardMultisig.importWallet() previously initialized threshold to 2 and never validated the number of cosigners. The patch initializes threshold and a new cosigners counter to 0, parses the Policy field strictly as ‘M of N’, requires the field to be present, and compares N against wallet.getKeystores().size(). Missing or mismatched configurations now raise IllegalStateException wrapped in ImportException. Tests and fixtures were added for a missing Policy line and a missing cosigner key.
Changed components
src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.javaColdcard multisig wallet import flowInspect captured patch +53 / −4
### src/main/java/com/sparrowwallet/sparrow/io/ColdcardMultisig.java
@@ -121,7 +121,8 @@ public Wallet importWallet(InputStream inputStream, String password) throws Impo
Wallet wallet = new Wallet();
wallet.setPolicyType(PolicyType.MULTI_HD);
- int threshold = 2;
+ int threshold = 0;
+ int cosigners = 0;
ScriptType scriptType = ScriptType.P2SH;
String derivation = null;
@@ -143,7 +144,12 @@ public Wallet importWallet(InputStream inputStream, String password) throws Impo
wallet.setName(value.trim());
break;
case "Policy":
- threshold = Integer.parseInt(value.split(" ")[0]);
+ String[] policy = value.split("\\s+");
+ if(policy.length != 3) {
+ throw new IllegalStateException("Could not determine the multisig policy from \"" + line + "\"");
+ }
+ threshold = Integer.parseInt(policy[0]);
+ cosigners = Integer.parseInt(policy[2]);
break;
case "Derivation":
case "# derivation":
@@ -167,8 +173,16 @@ public Wallet importWallet(InputStream inputStream, String password) throws Impo
}
- Policy policy = Policy.getPolicy(PolicyType.MULTI_HD, scriptType, wallet.getKeystores(), threshold);
- wallet.setDefaultPolicy(policy);
+ if(threshold == 0) {
+ throw new IllegalStateException("This file does not specify the multisig policy");
+ }
+
+ if(cosigners != wallet.getKeystores().size()) {
+ throw new IllegalStateException("This file specifies a policy of " + threshold + " of " + cosigners + ", but contains " + wallet.getKeystores().size() + " cosigner key" + (wallet.getKeystores().size() == 1 ? "" : "s"));
+ }
+
+ Policy walletPolicy = Policy.getPolicy(PolicyType.MULTI_HD, scriptType, wallet.getKeystores(), threshold);
+ wallet.setDefaultPolicy(walletPolicy);
wallet.setScriptType(scriptType);
try {
### src/test/java/com/sparrowwallet/sparrow/io/ColdcardMultisigTest.java
@@ -99,6 +99,18 @@ public void importWalletMultiDeriv() throws ImportException {
Assertions.assertTrue(wallet.isValid());
}
+ @Test
+ public void importWalletMissingPolicy() {
+ ColdcardMultisig ccMultisig = new ColdcardMultisig();
+ Assertions.assertThrows(ImportException.class, () -> ccMultisig.importWallet(getInputStream("cc-multisig-export-nopolicy.txt"), null));
+ }
+
+ @Test
+ public void importWalletMissingCosigner() {
+ ColdcardMultisig ccMultisig = new ColdcardMultisig();
+ Assertions.assertThrows(ImportException.class, () -> ccMultisig.importWallet(getInputStream("cc-multisig-export-missingkey.txt"), null));
+ }
+
@Test
public void exportWallet1() throws ImportException, ExportException, IOException {
ColdcardMultisig ccMultisig = new ColdcardMultisig();
### src/test/resources/com/sparrowwallet/sparrow/io/cc-multisig-export-missingkey.txt
@@ -0,0 +1,10 @@
+# Coldcard Multisig setup file (created on 0F056943)
+#
+Name: CC-2-of-4
+Policy: 2 of 4
+Derivation: m/48'/0'/0'/2'
+Format: P2WSH
+
+0F056943: xpub6EfEGa5isJbQFSswM5Uptw5BSq2Td1ZDJr3QUNUcMySpC7itZ3ccypVHtLPnvMzKQ2qxrAgH49vhVxRcaQLFbixAVRR8RACrYTp88Uv9h8Z
+6BA6CFD0: xpub6FFEQVG6QR28giDuML74Y7EMPwqEiKftNjScLzg5WKM41bf6LMP2XspjBgNp28tvkNUZdokmTY4TcRbuGZBSMvNoUECrKW1y3TBPeQJVmAg
+747B698E: xpub6Eb6Z1xtmWRiWKgRpHf6dHiEagGd6FLiBXrnma1nFK4PGRYqSVqVyJaxna5Mb8etSP4ATKVAvKnXG1a9HZauoAawuSDJT5RgH2HqEVHZVHY
### src/test/resources/com/sparrowwallet/sparrow/io/cc-multisig-export-nopolicy.txt
@@ -0,0 +1,13 @@
+# Exported from Electrum
+Name: el-CC-3-of-3-sb-2
+Format: P2WSH
+
+Derivation: m/48'/0'/0'/2'
+06B57041: xpub6EfEGa5isJbQFSswM5Uptw5BSq2Td1ZDJr3QUNUcMySpC7itZ3ccypVHtLPnvMzKQ2qxrAgH49vhVxRcaQLFbixAVRR8RACrYTp88Uv9h8Z
+
+Derivation: m/48'/0'/0'/2'
+4B569672: xpub6FFEQVG6QR28giDuML74Y7EMPwqEiKftNjScLzg5WKM41bf6LMP2XspjBgNp28tvkNUZdokmTY4TcRbuGZBSMvNoUECrKW1y3TBPeQJVmAg
+
+Derivation: m/47'/0'/0'/1'
+CA9A2B19: xpub6Eb6Z1xtmWRiWKgRpHf6dHiEagGd6FLiBXrnma1nFK4PGRYqSVqVyJaxna5Mb8etSP4ATKVAvKnXG1a9HZauoAawuSDJT5RgH2HqEVHZVHY
+Why this scored 56/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.