Page MenuHomePhabricator

[Cashtab] Use ecash-lib to sign and verify msgs
ClosedPublic

Authored by bytesofman on Tue, Mar 11, 17:39.

Details

Reviewers
Fabien
Group Reviewers
Restricted Project
Commits
rABC5c1b0068bb80: [Cashtab] Use ecash-lib to sign and verify msgs
Summary

Now that we have the methods in our own stack to do this, implement in Cashtab. This allows us to stop using bitcoinjs-message.

Test Plan

npm test

Event Timeline

implement a de facto spec implementation of sign and verify at the lib level and apply in Cashtab

drop now-unused component function

use real "custom" prefix, back out forbid only change (should be its own diff)

bytesofman added inline comments.
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)

does electrum do this

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.

Fabien requested changes to this revision.Wed, Mar 12, 13:36
Fabien added a subscriber: Fabien.
Fabien added inline comments.
modules/ecash-lib/src/messages.ts
38 ↗(On Diff #53033)

What encoding is used / supported ?

114 ↗(On Diff #53033)

You also need to check the signature is correct, the ecash-lib recoverSig doesn't do it

This revision now requires changes to proceed.Wed, Mar 12, 13:36
modules/ecash-lib/src/messages.ts
114 ↗(On Diff #53033)

nvm, it does

bytesofman added inline comments.
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

bytesofman marked an inline comment as done.

add comments for verifyMsg

Fabien requested changes to this revision.Thu, Mar 13, 08:36

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"

This revision now requires changes to proceed.Thu, Mar 13, 08:36
bytesofman marked 4 inline comments as done.
bytesofman added inline comments.
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:

  • use Buffer and trust js devs to get webpack right, or perhaps our own typescript build could be modified to ensure Buffer is polyfilled at the build step
  • Use an npm lib for base64 conversions, like https://www.npmjs.com/package/base64-js

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.

This revision is now accepted and ready to land.Thu, Mar 13, 21:12
This revision was automatically updated to reflect the committed changes.
bytesofman marked 2 inline comments as done.