Now that we have the methods in our own stack to do this, implement in Cashtab. This allows us to stop using bitcoinjs-message.
Details
- Reviewers
Fabien - Group Reviewers
Restricted Project - Commits
- rABC5c1b0068bb80: [Cashtab] Use ecash-lib to sign and verify msgs
npm test
Diff Detail
- Repository
- rABC Bitcoin ABC
- Branch
- ecash-lib-sign-msgs
- Lint
Lint Passed - Unit
No Test Coverage - Build Status
Event Timeline
implement a de facto spec implementation of sign and verify at the lib level and apply in Cashtab
cashtab/src/components/SignVerifyMsg/__tests__/SignVerifyMsg.test.js | ||
---|---|---|
81 ↗ | (On Diff #53033) | the signature changing is ... potentially not expected behavior. I am not sure why it changes. The prefixes are identical. There are some conventions in bitcoinjs-message that could lead to distinct values, i.e. adjusting the recovery byte https://github.com/bitcoinjs/bitcoinjs-message/blob/master/index.js#L34 does electrum / the node do this? |
81 ↗ | (On Diff #53032) | the signature changing is ... potentially not expected behavior. I am not sure why it changes. The prefixes are identical. There are some conventions in bitcoinjs-message that could lead to distinct values, i.e. adjusting the recovery byte https://github.com/bitcoinjs/bitcoinjs-message/blob/master/index.js#L34 does electrum / the node do this? |
cashtab/src/components/SignVerifyMsg/__tests__/SignVerifyMsg.test.js | ||
---|---|---|
81 ↗ | (On Diff #53033) |
relevant file looks like electrumabc/ecc.py def sign_message( self, message: bytes, is_compressed, *, sigtype: SignatureType = SignatureType.ECASH, ) -> bytes: def bruteforce_recid(sig_string): for recid in range(4): sig65 = construct_sig65(sig_string, recid, is_compressed) if not self.verify_message(sig65, message, sigtype=sigtype): continue return sig65, recid else: raise Exception("error: cannot sign message. no recid fits..") message = to_bytes(message, "utf8") msg_hash = Hash(msg_magic(message, sigtype)) sig_string = self.sign( msg_hash, sigencode=sig_string_from_r_and_s, ) sig65, recid = bruteforce_recid(sig_string) return sig65 ref def construct_sig65(sig_string, recid, is_compressed): comp = 4 if is_compressed else 0 return bytes([27 + recid + comp]) + sig_string so, it looks like there is a standard as such about signing with / without a compressed key ... and the standard exists bc we need to know which key to recover. that said ... I'm not sure we need to have this in ecash-lib. the way ecash-lib is contained, we would always be signing with 32-byte sk, and always be verifying against the 33-byte compressed public key. So question -- do we need to support uncompressed public keys? Cashtab does not need this. I can see the value in having deterministic signatures for any given private key tho. |
modules/ecash-lib/src/messages.ts | ||
---|---|---|
114 ↗ | (On Diff #53033) | nvm, it does |
modules/ecash-lib/src/messages.ts | ||
---|---|---|
38 ↗ | (On Diff #53033) | utf8 is default + only supported by TextEncoder https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder |
114 ↗ | (On Diff #53033) | per tg chat -- this is validated by recoverSig |
More a question than requesting change here
modules/ecash-lib/src/messages.ts | ||
---|---|---|
91 ↗ | (On Diff #53033) | This is encoding using utf-16 |
92 ↗ | (On Diff #53033) | This expects the sequence to never contain chars outside of the utf-8 range. It's unclear to me if this could fail depending on the signature because decoding will see as utf-16 and btoa() raise. Such a concern is documented in the btoa() doc https://developer.mozilla.org/fr/docs/Web/API/Window/btoa and the base64 conversion even has its own page https://developer.mozilla.org/fr/docs/Glossary/Base64 |
105 ↗ | (On Diff #53033) | You could catch the decoding errors here and return a better message, like "signature is not base64 encoded" |
modules/ecash-lib/src/messages.ts | ||
---|---|---|
92 ↗ | (On Diff #53033) | interesting rabbit hole from here short answer is: should be fine, since sig as utf8 can't have anything outside the 0-255 range, the string (even tho techically utf16) is expected to have the same char restriction longer answer: In nodejs, we could skip the whole string.fromCharChode step and just convert uint8array to base64 using Buffer. I've used btoa instead because, historically, using Buffer in libs tends to raise weird issues with browser-based users of libraries. Cashtab required webpack shims for this. btoa has only recently been added to nodejs if i console.log and run the test, we can see its implementation console.log(typeof btoa, 'Where does btoa come from?', btoa.toString()); function Where does btoa come from? function btoa(input) { // The implementation here has not been performance optimized in any way and // should not be. // Refs: https://github.com/nodejs/node/pull/38433#issuecomment-828426932 if (arguments.length === 0) { throw new ERR_MISSING_ARGS('input'); } input = `${input}`; for (let n = 0; n < input.length; n++) { if (input[n].charCodeAt(0) > 0xff) throw lazyDOMException('Invalid character', 'InvalidCharacterError'); } const buf = Buffer.from(input, 'latin1'); return buf.toString('base64'); } ...so in nodejs environments, it's ultimately using Buffer anyway. We know that it is working in nodejs because the mocha tests pass, and we know it is working in the browser because it is working in Cashtab. We could take these alt approaches to avoid btoa:
imo it's best to just use btoa since we know it works in nodejs and the browser without any further adjustments, and it prevents us from needing a lib which, while popular and free of its own deps, has not been updated in 4 years and still would be another supply chain concern. |
105 ↗ | (On Diff #53033) | I think it's best to just keep this function boolean with no error throwing. This does mean a hypothetical user with a valid but non-base64-encoded signature could get the "technically wrong" answer that a signature is invalid. I haven't come across this issue. If it's important for a lib user, they can validate for this in their input field. If we added this type of error throwing, it would mean that all implementers would need to try...catch for verifyMsg, to handle instances like "user has copy pasted a base64 signature into the check field, but then backspaced a character" -- the most common case would be bad input (signature is indeed wrong) -- the case of a correct but non-base64 sig would I think be niche .... and if we specifically only throw for this case, we might as well accept non-base64-but-still-valid sigs. |