diff --git a/.changeset/twelve-eagles-admire.md b/.changeset/twelve-eagles-admire.md new file mode 100644 index 0000000000..77c7727c52 --- /dev/null +++ b/.changeset/twelve-eagles-admire.md @@ -0,0 +1,5 @@ +--- +"caravan-coordinator": minor +--- + +Added a backwards compatible message signing functionality for ledger and tenzor wallet using caravan-wallets diff --git a/apps/coordinator/package.json b/apps/coordinator/package.json index 30f82e4611..a1ef42c8eb 100644 --- a/apps/coordinator/package.json +++ b/apps/coordinator/package.json @@ -114,6 +114,7 @@ "base58check": "^2.0.0", "bignumber.js": "^9.0.0", "bip32": "^2.0.4", + "bip322-js": "^2.0.0", "bitcoin-address-validation": "^1.0.2", "bitcoinjs-lib": "^5.1.7", "bowser": "^2.6.1", diff --git a/apps/coordinator/src/components/CreateAddress/AddressGenerator.jsx b/apps/coordinator/src/components/CreateAddress/AddressGenerator.jsx index 1c5f4332ae..0662bc87c8 100644 --- a/apps/coordinator/src/components/CreateAddress/AddressGenerator.jsx +++ b/apps/coordinator/src/components/CreateAddress/AddressGenerator.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import PropTypes from "prop-types"; import { connect } from "react-redux"; import { @@ -19,7 +19,7 @@ import { } from "@mui/material"; import { downloadFile } from "utils"; import { externalLink } from "utils/ExternalLink"; - +import SignMessageModal from "./SignMessageModal"; // Actions import { sortPublicKeyImporters as sortPublicKeyImportersAction, @@ -39,6 +39,7 @@ const AddressGenerator = ({ requiredSigners, setMultisigAddress, }) => { + const [showSignMessageModal, setShowSignMessageModal] = useState(false); const isInConflict = () => { return Object.values(publicKeyImporters).some( (importer) => importer.conflict, @@ -212,6 +213,23 @@ ${redeemScriptLine}${scriptsSpacer}${witnessScriptLine} Download Address Details + + + + + {showSignMessageModal && ( + setShowSignMessageModal(false)} + /> + )} ); } diff --git a/apps/coordinator/src/components/CreateAddress/SignMessageModal.jsx b/apps/coordinator/src/components/CreateAddress/SignMessageModal.jsx new file mode 100644 index 0000000000..39a5ba27a2 --- /dev/null +++ b/apps/coordinator/src/components/CreateAddress/SignMessageModal.jsx @@ -0,0 +1,173 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { SignMessage, LEDGER, TREZOR } from "@caravan/wallets"; +import { verifyMessageSignature } from "utils/verifyMessage"; +import Copyable from "../Copyable"; +import { + Modal, + Card, + Typography, + TextField, + Box, + Button, + CircularProgress, + MenuItem, + Grid, +} from "@mui/material"; +const SUPPORTED_KEYSTORES = { + ledger: LEDGER, + trezor: TREZOR, +}; + +const SignMessageModal = ({ onClose, multisig, publicKeyImporters }) => { + const [keystoreType, setKeystoreType] = useState(""); + const [selectedKeyIndex, setSelectedKeyIndex] = useState(""); + const [message, setMessage] = useState( + "Sign to prove ownership of this multisig address.", + ); + const [signatureResult, setSignatureResult] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSignerChange = (e) => { + const index = e.target.value; + setSelectedKeyIndex(index); + }; + + const selectedImporter = publicKeyImporters?.[selectedKeyIndex] || {}; + const bip32Path = selectedImporter?.bip32Path || ""; + const publicKey = selectedImporter?.publicKey || ""; + + const handleSign = async () => { + setLoading(true); + setSignatureResult(null); + setError(null); + + try { + const keystore = SUPPORTED_KEYSTORES[keystoreType]; + if (!keystore) throw new Error("Unsupported or missing keystore"); + if (!bip32Path || !publicKey) throw new Error("No signer selected"); + + const interaction = SignMessage({ keystore, bip32Path, message }); + const result = await interaction.run(); + + const verified = verifyMessageSignature( + multisig.address, + message, + result.signature, + ); + + setSignatureResult({ + signature: result.signature, + verified, + }); + } catch (e) { + console.error("Signing failed:", e); + setError(e.message); + } finally { + setLoading(false); + } + }; + + return ( + + + Sign Message + + setKeystoreType(e.target.value)} + margin="normal" + > + Ledger + Trezor + + + + {Object.entries(publicKeyImporters).map(([index, importer]) => ( + + {importer.name || `Signer ${index}`} -{" "} + {importer.publicKey.slice(0, 16)}... + + ))} + + + + + setMessage(e.target.value)} + multiline + margin="normal" + /> + + + + + + {signatureResult && ( + + + {signatureResult.verified + ? "✅ Signature verified!" + : "❌ Signature invalid"} + + + + Signature (base64): + + + + + + + )} + + {error && ( + + ❌ Error: {error} + + )} + + + + + + + ); +}; + +SignMessageModal.propTypes = { + onClose: PropTypes.func.isRequired, + multisig: PropTypes.object, + publicKeyImporters: PropTypes.object.isRequired, +}; + +export default SignMessageModal; diff --git a/apps/coordinator/src/utils/verifyMessage.js b/apps/coordinator/src/utils/verifyMessage.js new file mode 100644 index 0000000000..aef6a1aa40 --- /dev/null +++ b/apps/coordinator/src/utils/verifyMessage.js @@ -0,0 +1,10 @@ +import { Verifier } from "bip322-js"; + +export const verifyMessageSignature = (address, message, signatureBase64) => { + try { + return Verifier.verifySignature(address, message, signatureBase64, false); + } catch (err) { + console.log("Signature verification failed:", err); + return false; + } +}; diff --git a/package-lock.json b/package-lock.json index 4b9ea13f71..5c8a2a1cae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,6 +55,7 @@ "base58check": "^2.0.0", "bignumber.js": "^9.0.0", "bip32": "^2.0.4", + "bip322-js": "^2.0.0", "bitcoin-address-validation": "^1.0.2", "bitcoinjs-lib": "^5.1.7", "bowser": "^2.6.1", @@ -2875,12 +2876,27 @@ } }, "node_modules/@bitcoinerlab/secp256k1": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.0.5.tgz", - "integrity": "sha512-8gT+ukTCFN2rTxn4hD9Jq3k+UJwcprgYjfK/SQUSLgznXoIgsBnlPuARMkyyuEjycQK9VvnPiejKdszVTflh+w==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.2.0.tgz", + "integrity": "sha512-jeujZSzb3JOZfmJYI0ph1PVpCRV5oaexCgy+RvCXV8XlY+XFB/2n3WOcvBsKLsOw78KYgnQrQWb2HrKE4be88Q==", + "license": "MIT", "dependencies": { - "@noble/hashes": "^1.1.5", - "@noble/secp256k1": "^1.7.1" + "@noble/curves": "^1.7.0" + } + }, + "node_modules/@bitcoinerlab/secp256k1/node_modules/@noble/curves": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.8.1.tgz", + "integrity": "sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.7.1" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/@caravan/bip32": { @@ -6075,17 +6091,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@noble/secp256k1": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", - "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -9735,6 +9740,69 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.18.tgz", "integrity": "sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==" }, + "node_modules/bip322-js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bip322-js/-/bip322-js-2.0.0.tgz", + "integrity": "sha512-wyewxyCLl+wudZWiyvA46SaNQL41dVDJ+sx4HvD6zRXScHzAycwuKEMmbvr2qN+P/IIYArF4XVqlyZVnjutELQ==", + "license": "MIT", + "dependencies": { + "@bitcoinerlab/secp256k1": "^1.1.1", + "bitcoinjs-lib": "^6.1.5", + "bitcoinjs-message": "^2.2.0", + "ecpair": "^2.1.0", + "elliptic": "^6.5.5", + "fast-sha256": "^1.3.0", + "secp256k1": "^5.0.0" + } + }, + "node_modules/bip322-js/node_modules/base-x": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", + "license": "MIT" + }, + "node_modules/bip322-js/node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT" + }, + "node_modules/bip322-js/node_modules/bitcoinjs-lib": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.1.7.tgz", + "integrity": "sha512-tlf/r2DGMbF7ky1MgUqXHzypYHakkEnm0SZP23CJKIqNY/5uNAnMbFhMJdhjrL/7anfb/U8+AlpdjPWjPnAalg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bech32": "^2.0.0", + "bip174": "^2.1.1", + "bs58check": "^3.0.1", + "typeforce": "^1.11.3", + "varuint-bitcoin": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bip322-js/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "license": "MIT", + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/bip322-js/node_modules/bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" + } + }, "node_modules/bip66": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", @@ -9861,6 +9929,43 @@ "bs58": "^5.0.0" } }, + "node_modules/bitcoinjs-message": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bitcoinjs-message/-/bitcoinjs-message-2.2.0.tgz", + "integrity": "sha512-103Wy3xg8Y9o+pdhGP4M3/mtQQuUWs6sPuOp1mYphSUoSMHjHTlkj32K4zxU8qMH0Ckv23emfkGlFWtoWZ7YFA==", + "license": "MIT", + "dependencies": { + "bech32": "^1.1.3", + "bs58check": "^2.1.2", + "buffer-equals": "^1.0.3", + "create-hash": "^1.1.2", + "secp256k1": "^3.0.1", + "varuint-bitcoin": "^1.0.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/bitcoinjs-message/node_modules/secp256k1": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.1.tgz", + "integrity": "sha512-tArjQw2P0RTdY7QmkNehgp6TVvQXq6ulIhxv8gaH6YubKG/wxxAoNKcbuXjDhybbc+b2Ihc7e0xxiGN744UIiQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.5.7", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/blake-hash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/blake-hash/-/blake-hash-2.0.0.tgz", @@ -9940,7 +10045,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -10142,6 +10246,15 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-equals": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/buffer-equals/-/buffer-equals-1.0.4.tgz", + "integrity": "sha512-99MsCq0j5+RhubVEtKQgKaD6EM+UP3xJgIvQqwJ3SOLDUekzxMX1ylXBng+Wa2sh7mGT0W6RUly8ojjr1Tt6nA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -10151,8 +10264,7 @@ "node_modules/buffer-xor": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" }, "node_modules/bufferutil": { "version": "4.0.8", @@ -11769,6 +11881,20 @@ "node": ">=4" } }, + "node_modules/drbg.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", + "integrity": "sha512-F4wZ06PvqxYLFEZKkFxTDcns9oFNk34hvmJSEwdzsxVQ8YI5YaxtACgQatkYgv2VI2CFkUd2Y+xosPQnHv809g==", + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.6", + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -13131,7 +13257,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" @@ -13275,6 +13400,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fastestsmallesttextencoderdecoder": { "version": "1.0.22", "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", @@ -21941,6 +22072,27 @@ "object-assign": "^4.1.1" } }, + "node_modules/secp256k1": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-5.0.1.tgz", + "integrity": "sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "elliptic": "^6.5.7", + "node-addon-api": "^5.0.0", + "node-gyp-build": "^4.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/secp256k1/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "license": "MIT" + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",