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

Signed messages should return a signature that has recoverable public key #2419

Closed
aulneau opened this issue May 16, 2022 · 11 comments · Fixed by #2484
Closed

Signed messages should return a signature that has recoverable public key #2419

aulneau opened this issue May 16, 2022 · 11 comments · Fixed by #2484
Assignees

Comments

@aulneau
Copy link
Contributor

aulneau commented May 16, 2022

Talking with @pradel about how to use the new signed messages feature in thing like ceramic.network, we realized that the signatures the wallet is currently sending down to the apps are ones that make it impossible to recover the public key from the signature.

The wallet is using signECDSA versus signWithKey.

Sign with Key allows for recoverable public keys from signatures:

export function signWithKey(privateKey: StacksPrivateKey, input: string): MessageSignature {
  const [rawSignature, recoveryParam] = signSync(input, privateKey.data.slice(0, 32), {
    canonical: true,
    recovered: true,
  });
  const signature = Signature.fromHex(rawSignature);
  const coordinateValueBytes = 32;
  const r = leftPadHexToLength(signature.r.toString(16), coordinateValueBytes * 2);
  const s = leftPadHexToLength(signature.s.toString(16), coordinateValueBytes * 2);

  if (recoveryParam === undefined || recoveryParam === null) {
    throw new Error('"signature.recoveryParam" is not set');
  }
  const recoveryParamHex = intToHexString(recoveryParam, 1);
  const recoverableSignatureString = recoveryParamHex + r + s;
  return createMessageSignature(recoverableSignatureString);
}

Where as signECDSA does not:

export function signECDSA(
  privateKey: string,
  content: string | Buffer
): {
  publicKey: string;
  signature: string;
} {
  const contentBuffer = content instanceof Buffer ? content : Buffer.from(content);
  const publicKey = getPublicKeyFromPrivate(privateKey);
  const contentHash = hashSha256Sync(contentBuffer);
  const signature = signSync(contentHash, privateKey);

  return {
    signature: utils.bytesToHex(signature),
    publicKey,
  };
}

We can also see that Marvins example implementation correctly uses signWithKey https://github.com/MarvinJanssen/stx-signed-structured-data/blob/4c5714e2b9957db0037863d15c3640a0524abe20/scripts/deps.ts#L28

I'd suggested shipping a change to where you switch out these functions.

@markmhendrickson
Copy link
Collaborator

Thanks for this input 🙏 Is this a progressive enhancement of sorts, or should this be in place before developers start using message signing generally?

@aulneau
Copy link
Contributor Author

aulneau commented May 16, 2022

@markmhx i think it is not a progressive enhancement, but should be a requirement. perhaps @pradel can chime in too, or @MarvinJanssen

@pradel
Copy link
Contributor

pradel commented May 16, 2022

@markmhx yeah this should be shipped asap before apps start using it in prod (it would be a breaking change otherwise).

@markmhendrickson
Copy link
Collaborator

@aulneau @MarvinJanssen Mind helping us test the build here as well? #2420

@pradel says it checks out 💪

@MarvinJanssen
Copy link
Contributor

Good find @aulneau & @pradel!

Quick observation. signWithKey returns the signature in vrs order. publicKeyFromSignature likewise uses/expects this order. However, Clarity functions secp256k1-recover? and secp256k1-verify expect the signature in rsv order. Since the main focus of (SIP018) signed messages was to (at some point) post them to the chain, my reference library returns the signature in rsv order:

https://github.com/MarvinJanssen/stx-signed-structured-data/blob/4c5714e2b9957db0037863d15c3640a0524abe20/scripts/deps.ts#L29

return Buffer.from(data.slice(2) + data.slice(0, 2), 'hex');

The SIP document also assumes rsv order:

I therefore think it makes sense to return the signature in rsv order lest we confuse end-developers.

@beguene
Copy link
Contributor

beguene commented May 23, 2022

@aulneau The wallet return the ECDSA signature and the publicKey in an explicit way like this
image

So it's easily verifiable and it's clear for the developer that he can just verify the signature with the pubKey (with no extra step) and the message hash. Anyone who has used or is familiar with cryptographic primitives for signing messages and verifying signature will be cognisant of this even without any blockchain knowledge.

Besides
secp256k1-verify from clarity expects an ECDSA signature and a publicKey.

(secp256k1-verify message-hash signature public-key)

https://docs.stacks.co/en/write-smart-contracts/language-functions#secp256k1-verify

and the corresponding code in the stack blockchain confirm this

pub fn secp256k1_verify(
    message_arr: &[u8],
    serialized_signature_arr: &[u8],
    pubkey_arr: &[u8],
)

https://github.com/stacks-network/stacks-blockchain/blob/26bfd5fcdc1f25106288f18ced05290b569f6abb/stacks-common/src/util/secp256k1.rs#L396
which internally calls

int secp256k1_ecdsa_verify(const secp256k1_context* ctx, const secp256k1_ecdsa_signature *sig, const unsigned char *msghash32, const secp256k1_pubkey *pubkey)

(via ffi binding )
https://github.com/bitcoin-core/secp256k1/blob/aa5d34a8fe99b1f69306be20819f337dbd3283db/src/secp256k1.c#L442

secp256k1_ecdsa_verify(self.ctx, sig.as_c_ptr(), msg.as_c_ptr(), pk.as_c_ptr()) 

Note that signECDSA returns in the signature in vrs order (via the DER format) which is also the order expected by secp256k1-verify

In the case of a blockchain like Bitcoin or Stack with restricted access to resources (space and compute) it makes sense to encode both the signature and the publicKey in a single 'recoverable signature'. But for a web app or backend service that has no such restriction I think this is unnecessary optimisation and complexity.

@markmhendrickson markmhendrickson moved this from Review to Development in Hiro Wallet (DEPRECATED) May 23, 2022
@markmhendrickson markmhendrickson moved this from Development to Backlog in Hiro Wallet (DEPRECATED) May 23, 2022
@janniks
Copy link
Contributor

janniks commented May 23, 2022

I believe Clarity's secp256k1-verify is RSV:

@aulneau
Copy link
Contributor Author

aulneau commented May 23, 2022

But for a web app or backend service that has no such restriction I think this is unnecessary optimisation and complexity.

I disagree with this statement personally -- this is a significant assumption on use case. @pradel gave a perfectly reasonable example of a real use case within 1 day of this feature being released, and @MarvinJanssen gave additional context on what the stacks-blockchain and SIP-018 expects too.

I think if the wallet does not provide a recoverable signature in RSV format, it would not be SIP-018 compliant, unless I'm misunderstanding it -- would love to hear from @MarvinJanssen on this.

@beguene I'd love to understand where the complexity is around this? Seems like a very reasonable request imo.

Note that signECDSA returns in the signature in vrs order (via the DER format) which is also the order expected by secp256k1-verify

Can you go into more detail about what you mean by this? this does not seem to be the case (see @janniks and @MarvinJanssen's comments)

@MarvinJanssen
Copy link
Contributor

@janniks and @aulneau are correct. It appears the Stacks blockchain uses vrs order for transactions and rsv order for the Clarity functions. See also this comment: hirosystems/stacks.js#1263 (comment)

I think if the wallet does not provide a recoverable signature in RSV format, it would not be SIP-018 compliant, unless I'm misunderstanding it -- would love to hear from @MarvinJanssen on this.

Not just SIP018 but it also makes it incompatible with the Clarity functions per the above comment. Additionally, without the recovered: true option, signSync drops the recovery byte:
https://github.com/paulmillr/noble-secp256k1/blob/450ebc76ef73ff375c8ce8252468d3a04e87c7f5/index.ts#L1148-L1157

@beguene
Copy link
Contributor

beguene commented May 24, 2022

My bad indeed secp256k1-verify function is rsv and takes a recoverable signature 🤦
Therefore it then makes sense then to send this format back to the dApp to be easily reused for smart contract.
Sorry for the confusion.

@jruffer
Copy link

jruffer commented May 24, 2022

Will this be fixed in all the places it is called...it's everywhere... hirowallet, stacks.js, blockstacks, stacks-wallet-web etc.

@beguene beguene moved this from Backlog to Development in Hiro Wallet (DEPRECATED) Jun 10, 2022
beguene added a commit that referenced this issue Jun 10, 2022
return all signatures in RSV format
display correct hash in the UI
closes #2443
closes #2460
closes #2419
beguene added a commit that referenced this issue Jun 10, 2022
return all signatures in RSV format
display correct hash in the UI
closes #2443
closes #2460
closes #2419
@beguene beguene moved this from Development to Review in Hiro Wallet (DEPRECATED) Jun 10, 2022
beguene added a commit that referenced this issue Jun 10, 2022
return all signatures in RSV format
display correct hash in the UI
closes #2443
closes #2460
closes #2419
beguene added a commit that referenced this issue Jun 10, 2022
return all signatures in RSV format
display correct hash in the UI
closes #2443
closes #2460
closes #2419
beguene added a commit that referenced this issue Jun 21, 2022
return all signatures in RSV format
display correct hash in the UI
closes #2443
closes #2460
closes #2419
beguene added a commit that referenced this issue Jun 21, 2022
return all signatures in RSV format
display correct hash in the UI
closes #2443
closes #2460
closes #2419
@markmhendrickson markmhendrickson moved this from Review to Development in Hiro Wallet (DEPRECATED) Jun 22, 2022
@markmhendrickson markmhendrickson moved this from Development to QA in Hiro Wallet (DEPRECATED) Jun 22, 2022
beguene added a commit that referenced this issue Jun 23, 2022
return all signatures in RSV format
display correct hash in the UI
closes #2443
closes #2460
closes #2419
beguene added a commit that referenced this issue Jun 23, 2022
return all signatures in RSV format
display correct hash in the UI
closes #2443
closes #2460
closes #2419
beguene added a commit that referenced this issue Jun 23, 2022
return all signatures in RSV format
display correct hash in the UI
closes #2443
closes #2460
closes #2419
fbwoolf pushed a commit that referenced this issue Jun 23, 2022
return all signatures in RSV format
display correct hash in the UI
closes #2443
closes #2460
closes #2419
@fbwoolf fbwoolf moved this from QA to Ready to merge in Hiro Wallet (DEPRECATED) Jun 23, 2022
blockstack-devops pushed a commit that referenced this issue Jun 23, 2022
# [3.10.0-beta.19](v3.10.0-beta.18...v3.10.0-beta.19) (2022-06-23)

### Bug Fixes

* support SIP-018 for structured data signing ([8087cd6](8087cd6)), closes [#2443](#2443) [#2460](#2460) [#2419](#2419)
blockstack-devops pushed a commit that referenced this issue Jun 29, 2022
# [3.11.0-beta.1](v3.10.0...v3.11.0-beta.1) (2022-06-29)

### Bug Fixes

* add referer headers to api calls ([4b23da1](4b23da1))
* bad ui copy practices ([828082a](828082a))
* current account name ([7b54aa4](7b54aa4))
* direct landing on send form page, closes [#2470](#2470) ([b98d570](b98d570))
* firefox white screen with referer header ([427e592](427e592))
* fund page layout shift ([9f66227](9f66227))
* header version alignment ([676b5dd](676b5dd))
* new tab send form nonce and scroll ([62e6d11](62e6d11))
* nonce with pending tx ([9ba8234](9ba8234))
* onboarding state ([40bb84b](40bb84b))
* receive page scroll ([fd363de](fd363de))
* remove double scrollbars, emotion set up ([fc0be70](fc0be70))
* send form white screen ([1ff0e70](1ff0e70))
* sign message oncancel ([f2d75f8](f2d75f8))
* sign message with line breaks ([fdc8866](fdc8866))
* suggested steps only updating with refresh ([2dea9c6](2dea9c6))
* support SIP-018 for structured data signing ([8087cd6](8087cd6)), closes [#2443](#2443) [#2460](#2460) [#2419](#2419)
* use the anchor mode from the input transaction ([92c7c6a](92c7c6a))

### Features

* add image to fungible token ([c891983](c891983)), closes [#1800](#1800)
* initial support jwt signing on ledger ([5f4696e](5f4696e))
* support Ledger hardware wallets ([8a7d0d2](8a7d0d2))
blockstack-devops pushed a commit that referenced this issue Jun 30, 2022
# [3.11.0](v3.10.0...v3.11.0) (2022-06-30)

### Bug Fixes

* add referer headers to api calls ([4b23da1](4b23da1))
* bad ui copy practices ([828082a](828082a))
* current account name ([7b54aa4](7b54aa4))
* direct landing on send form page, closes [#2470](#2470) ([b98d570](b98d570))
* firefox white screen with referer header ([427e592](427e592))
* fund page layout shift ([9f66227](9f66227))
* header version alignment ([676b5dd](676b5dd))
* loading first steps ([bbbff9c](bbbff9c))
* new tab send form nonce and scroll ([62e6d11](62e6d11))
* nonce with pending tx ([9ba8234](9ba8234))
* onboarding state ([40bb84b](40bb84b))
* receive page scroll ([fd363de](fd363de))
* remove double scrollbars, emotion set up ([fc0be70](fc0be70))
* send form white screen ([1ff0e70](1ff0e70))
* sign message oncancel ([f2d75f8](f2d75f8))
* sign message with line breaks ([fdc8866](fdc8866))
* suggested steps only updating with refresh ([2dea9c6](2dea9c6))
* support SIP-018 for structured data signing ([8087cd6](8087cd6)), closes [#2443](#2443) [#2460](#2460) [#2419](#2419)
* use the anchor mode from the input transaction ([92c7c6a](92c7c6a))

### Features

* add image to fungible token ([c891983](c891983)), closes [#1800](#1800)
* initial support jwt signing on ledger ([5f4696e](5f4696e))
* support Ledger hardware wallets ([8a7d0d2](8a7d0d2))
@fbwoolf fbwoolf moved this from Ready to merge to Merged in Hiro Wallet (DEPRECATED) Jun 30, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
8 participants