Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: p-460 bitcore message signature issue #2553

Merged
merged 20 commits into from
Mar 8, 2024
Merged

Conversation

0xverin
Copy link
Contributor

@0xverin 0xverin commented Mar 5, 2024

Context

resolves:p-460

Root cause: bitcore uses the ECDSA signature algorithm, which uses a nonce to sign messages. This causes the signature (base64) to not be consistent. When decoding, we might need to manually pad it with a 0 byte.

For example, under normal the signature is:
IJyax3CRyGOCceIyoBDpH9a03dwi3SoRJ4N1RZh36FxARkDCpEmUgrHqIhE5H8THYF8TcE5HuH/9BkovbYmMSbk=
But sometimes:
H6FuCBQt/fjshZIym0BBgGjbykkqBWO6CisWA9tBDDMD/5iODJehfyUXX3MrM21l5dHSuG46KqXiFEA7X94apw==
This is a base64 encoding issue.

I tested it and adding a 0 can make it work normally:

await signer.sign(payloadStr)------ Uint8Array(65) [
    0,  32, 157, 209, 236, 186,  54,  14, 139,  90, 150,
   47, 194, 137,  67,  86,  82, 112, 120, 102,  63, 223,
  156, 127, 240, 243, 129, 151,  19,  58, 202,  25, 246,
  121,  56, 254, 247,  39, 203,  68, 213, 168, 236,  53,
  163,  69, 207, 182,   3, 156, 234, 203, 145, 236,  34,
  150,  18, 212, 121, 148,  33, 130, 253, 117, 127
]
Got response: {"value":"0x010400","do_watch":false,"status":"Ok"}

Updates:
The solution of adding 0 was wrong, will change the content of sig, causing it to be invalid when used. I updated it with a new way.

After 2000 cycles of testing, there were no issues with the do-while sign method:

Got response: {"value":"0x010400","do_watch":false,"status":"Ok"}
    ✔ check idGraph from sidechain storage before linking (2034579ms)

Copy link

linear bot commented Mar 5, 2024

Copy link
Collaborator

@Kailai-Wang Kailai-Wang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for finding the root cause! 👍

Not sure if it's related to nonce tho (I haven't checked the lib impl and am not sure if the k is random or not) - maybe it's a sole consequence of having random btc key pairs each time.

@@ -145,7 +145,11 @@ export class BitcoinSigner implements Signer {
return new Promise((resolve, reject) => {
if (isString(message)) {
const sig = new bitcore.Message(message).sign(this.keypair);
resolve(bufferToU8a(Buffer.from(sig, 'base64')));
let buf = Buffer.from(sig, 'base64');
if (buf.length === 64) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to have more idiomatic code by using e.g. a fixed length buffer? Something like [u8; 65] in rust to make sure it's always 65 bytes 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After testing, it is not just the error case where buffer.length === 64. In fact, there will also be cases where it equals 63 (not excluding other cases, 63 is the result of 1000 loops). Therefore, change to using do while to ensure getting u8aArray 65 sig.

1) Test Identity (bitcoin direct invocation)
       check idGraph from sidechain storage before linking:
     Error: createType(LitentryMultiSignature):: Enum(bitcoin):: Expected input with 65 bytes (520 bits), found 63 bytes

The way of creating a buffer seems to be impractical, as it will change the content of the signature, such as this way:

let buf = Buffer.from(sig, 'base64');
Buffer.alloc(65)
buf.copy(resultBuf, 65 - buf.length);

Will get an invalid error:
Got response: {"value":"TGetterIsNotAuthorized","do_watch":false,"status":"Error"}

So my initial approach was also incorrect. The reason why the test passed is that when I console.log here, actually called the sign method. Due to the uncertainty of the signature result, after two console calls in front, the third call here happened to match the sig we used, so the test passed strangely.

Copy link
Collaborator

@BillyWooo BillyWooo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for finding the root cause.

@0xverin
Copy link
Contributor Author

0xverin commented Mar 5, 2024

I haven't checked the lib impl and am not sure if the k is random or not

I checked:
image

image

@0xverin
Copy link
Contributor Author

0xverin commented Mar 5, 2024

Please do not merge first, there is a small issue that needs to be confirmed.

@0xverin 0xverin force-pushed the fix-bitcoin-identity-failure branch from d7efdf0 to 7877432 Compare March 6, 2024 04:27
Copy link
Collaborator

@Kailai-Wang Kailai-Wang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw what about bitcore.Message in identity-helper.ts?

// Here we must use the format [u8,65],
// so we can only accept a signature that has been decoded from base64 and has a length of 65.
// Therefore, using a do -while loop to resign unnecessary signatures.We should not encounter an infinite loop situation, so no condition is set to break out of the loop.
do {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I somehow feel this isn't the right way, still.

A btc message signature should be 65 bytes - the encoded base64 array should be 88 bytes. This should be firm. If we get less than 65 bytes in decoding, I can only think of padding?

Let it run again and hope to get 65 bytes isn't the good practice IMO, as in reality you should be able to verify each btc signature the user generated, with arbitrary k

cc @kziemianek - maybe you know it as you've done some btc sig related impl

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion it should always produce 65 bytes signature, even bitcore-lib developers assumes that.
If you try to use bitcode-lib api to recreate signature from Buffer it will fail:

const buffer = Buffer.from(sig, 'base64')
Signature.fromCompact(buffer)

Invalid Argument: Error: s must be 32 bytes
Error
    at new NodeError (/home/kziemianek/projects/kziemianek/sig-test/node_modules/bitcore-lib/lib/errors/index.js:20:41)
    at Object.checkArgument (/home/kziemianek/projects/kziemianek/sig-test/node_modules/bitcore-lib/lib/util/preconditions.js:14:13)
    at Signature.fromCompact (/home/kziemianek/projects/kziemianek/sig-test/node_modules/bitcore-lib/lib/crypto/signature.js:53:5)

I debugged it for a while and it turns out the r value is sometimes 31 bytes long, which results in 64 (1+31+32) bytes signature (instead of 1+32+32). Above error says s must be 32 bytes but the root cause is still r being 31 bytes long.

64-byte-sig

It happens ocassionally so it's really low probability of happeining twice in the row.

Copy link
Member

@kziemianek kziemianek Mar 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to sum up, I see two options:

  • switch to other js bitcoin lib (https://www.npmjs.com/package/bitcoinjs-lib maybe ?)
  • implement a workaround (try to generate new signature untill it has 65 bytes) and wait for the potential fix in bitcore-lib - but before that happening it needs to be reported, confirmed and released.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your summary @kziemianek , I will switch to bitcoinjs for implementation. Btw, I have looked at the SDK of unisat-wallet and switched to the version they use, but there is still the same problem.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @jonalvarezz - in case you have some experience when implementing SDK..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new bitcoinjs-lib was tested twice in 2000 loops with no signature decoding issues. I also tested identity-helper several times, and there was no problem:

Got response: {"value":"0x010400","do_watch":false,"status":"Ok"}
    ✔ check idGraph from sidechain storage before linking (2012011ms)

cc @kziemianek @Kailai-Wang

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jumping in late, but I hope it helps.

Here is what we do on the SDK for Bitcoin in a nutshell:

  1. The hex challenge code should be signed without the 0x prefix. Only relevant if signing in hex/string format. We were using raw uint signing before, but I remember we had to change this because of bitcoin/ unisat.

https://github.com/litentry/identity-hub/blob/develop/libs/enclave/src/lib/requests/link-identity.request.ts#L63

  1. The produced signature is base64 decoded and the 0x prefix put back. Otherwise, LitentryValidationData fails at encoding.

https://github.com/litentry/identity-hub/blob/develop/libs/enclave/src/lib/type-creators/validation-data.ts#L145-L153

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks @jonalvarezz I actually looked at the code on idehub, and in the Bitcoin signature part, mainly use unisat-wallet. This wallet uses their own integrated unisat-sdk, and you will find that they also use bitcore-lib and bitcoinjs-lib.So, essentially there is no difference, I guess.:p

@0xverin 0xverin force-pushed the fix-bitcoin-identity-failure branch from de78d82 to 50da585 Compare March 7, 2024 05:12
@0xverin 0xverin requested a review from Kailai-Wang March 7, 2024 05:12
Copy link
Collaborator

@Kailai-Wang Kailai-Wang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me it's ready to be merged

@0xverin 0xverin merged commit 744bc85 into dev Mar 8, 2024
31 checks passed
@0xverin 0xverin deleted the fix-bitcoin-identity-failure branch March 8, 2024 07:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants