Secp256k1 support

Does Canton support secp256k1?

1 Like

Short answer: Canton does not support secp256k1 at the protocol level, but does support it at the Daml-level for verifying cryptographic signatures.

The Daml support is described here:

…and is particularly useful for folks seeking to build bridges to other public blockchains because it is a de facto blockchain standard.

This is the relevant release note for this support, as well as for related tooling in Daml:

(under “Cryptographic Primitives”)

However, secp256k1 is not an accepted NIST standard so Canton does not support it at the protocol level.

3 Likes

Thanks for the reply :slight_smile: It’d help a bit, but, for us, the other important method is ecrecover, which recovers the signer by using the hash of the data and the signature. Do you maybe have a plan to add it to DA?

Hi @Lukasz2891!
Canton actually now supports Secp256k1 from Canton 3.3 also in the protocol canton/community/base/src/main/protobuf/com/digitalasset/canton/crypto/v30/crypto.proto at 2d40ff8a28381489a1461bb7c50da0961c431298 · digital-asset/canton · GitHub

The documentation is not up to date on this topic yet.

1 Like

Still, ecrecover-like would be helpful - I couldn’t find it in the repo :slight_smile:

Another question:

We’re trying to use secp256k1 (verification) instead. But the function requires the signature is generated from the sha256 of the data

For example:

let data = "123"
let hash = sha256(data)
let hashSig = privateKey.sign(hash) 

Then secp256k1 hashSig data publicKey returns true (for DER formats)

But, in our payloads, we have not the hash, but data signed. So the signature corresponds to the data (and there’s no signature of hash), so we have

let data = "123"
let dataSig = privateKey.sign(data)

and then secp256k1 dataSig data publicKey won’t work…

Is there any possibility to pass the hashSig + hash or dataSig + data instead of mixing hashSig + data? We have available data + dataSig and we can generate hash, but cannot generate hashSig as it’s generated by different part of the system and cannot be modified.

the other possibility is to have keccak256(data) supported instead of sha256(data) because in reality the verified data is the keccak256-hash of the base data

let data = "123"
let hash = keccak256(data)
let hashSig = privateKey.sign(hash) 

Then secp256k1 hashSig data publicKey would be working.

Hi @Lukasz2891 thanks for your question.

In the Daml language we do now support both secp256k1 and keccak256. Here’s some Daml Script code demonstrating the basic use case for secp256k1:

module CryptoExample where

import DA.Assert ((===))
import DA.Crypto.Text
import Daml.Script
  
main =
  let
    msg = toHex "123"
  in
    script do
      keyPair <- secp256k1generatekeypair

      msgSig <- secp256k1sign keyPair.privateKey msg

      (secp256k1 msgSig msg keyPair.publicKey) === True

Using keccak256 to calculate message digests is not too dissimilar - sdk/compiler/damlc/tests/daml-test-files/CCTPMintToken.daml provides a more comprehensive example using both of these crypto functions.

In terms of ecrecover support, there are no current plans to add in this functionality.

Is there any possibility to pass the hashSig + hash or dataSig + data instead of mixing hashSig + data? We have available data + dataSig and we can generate hash, but cannot generate hashSig as it’s generated by different part of the system and cannot be modified.

I think I need to understand your use case a little bit more? Are you attempting to extract the public key from the data and data signature (c.f. ecrecover) or are you trying to do something else here?

I should add that secp256k1 consumes base-16 encoded text/hex strings for its signature, message and key arguments - which is why the msg has a toHex instance applied to the "123" message string.

IIRC, sha256 will return base-16 encoded text/hex strings, so maybe this explains your 2nd question?

We send our data in the packages like below:

Then, we need to be sure, the data are signed by one of our 5 signers.

Instead of ecrecover we can iterate over these 5 signers to find a public key (ethereum address) of the signers (estimated value is 2.5 checks, but maybe we can optimize it during send, we’ll check - we don’t send data for all of the signers, so we basicaly don’t know which of them is included in the whole payload).

For example:

The data

45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c6e38216a0197d021692000000020000001 and the signature 2ca4de4ee6b5c3526edcb911382e806a8eddc037ddc2a90022bcf195d851720202ddb1ce34bba06d0f15c80d22d70f74d49093ea696c863dfb224cf2806ef61a1c are recovered to:
public key 049a0e6cada7938a0fc616578aff2b11337d90ec4e99e9fee95e57e9d8371562b31698225fb73c666ba586842b913e67ad289f374f7a46fc873342f805a8683d4e which is one of these signer public keys.

The signature is a signature of the keccak(data) which is dd573f5c0bebc4a46b180c446652c76e3e48867fea4e6b33f378ac99d3c09afa in the case.

(You can use ABDK Toolkit to check)

Generally, we’re expecting the function

verify (toDer signature) (someTransform (keccak data)) (toDerPk publicKey)

will return true.

In our case,

verify (toDer "2ca4de4ee6b5c3526edcb911382e806a8eddc037ddc2a90022bcf195d851720202ddb1ce34bba06d0f15c80d22d70f74d49093ea696c863dfb224cf2806ef61a") (someTransform "dd573f5c0bebc4a46b180c446652c76e3e48867fea4e6b33f378ac99d3c09afa"
) (toDerPk "049a0e6cada7938a0fc616578aff2b11337d90ec4e99e9fee95e57e9d8371562b31698225fb73c666ba586842b913e67ad289f374f7a46fc873342f805a8683d4e") 

should return true.

Problems

The missing step is to find the function someTransform. Maybe one of base-16 encodings would help, I have a plan to check it deeper once again today.

And one more thing: why is it so important - because of the compatibility with all of our connectors and the whole system.

Hi @Lukasz2891 thanks for that extra detail, its very helpful.

The missing step is to find the function someTransform . Maybe one of base-16 encodings would help, I have a plan to check it deeper once again today.

The Daml crypto function keccak256 returns a hex string, which you should be able to use directly (i.e. your someTransform should be an identity) as an argument to the Daml secp256k1 function. So, using your example data (and assuming the function toDer is defined somewhere) this should work:

module CryptoExample where

import DA.Assert ((===))
import DA.Crypto.Text
import Daml.Script
  
main =
  let
    derSig = toDer "2ca4de4ee6b5c3526edcb911382e806a8eddc037ddc2a90022bcf195d851720202ddb1ce34bba06d0f15c80d22d70f74d49093ea696c863dfb224cf2806ef61a"
    hash = "dd573f5c0bebc4a46b180c446652c76e3e48867fea4e6b33f378ac99d3c09afa"
    derPublicKey = toDer "049a0e6cada7938a0fc616578aff2b11337d90ec4e99e9fee95e57e9d8371562b31698225fb73c666ba586842b913e67ad289f374f7a46fc873342f805a8683d4e"
  in
    script do
      (secp256k1 derSig hash derPublicKey) === True

Thanks for the reply.

main =
  let
    derSig = "304402202ca4de4ee6b5c3526edcb911382e806a8eddc037ddc2a90022bcf195d8517202022002ddb1ce34bba06d0f15c80d22d70f74d49093ea696c863dfb224cf2806ef61a" -- toDer "2ca4de4ee6b5c3526edcb911382e806a8eddc037ddc2a90022bcf195d851720202ddb1ce34bba06d0f15c80d22d70f74d49093ea696c863dfb224cf2806ef61a"
    hash = "dd573f5c0bebc4a46b180c446652c76e3e48867fea4e6b33f378ac99d3c09afa"
    derPublicKey = "3056301006072a8648ce3d020106052b8104000a034200049a0e6cada7938a0fc616578aff2b11337d90ec4e99e9fee95e57e9d8371562b31698225fb73c666ba586842b913e67ad289f374f7a46fc873342f805a8683d4e" -- toDer "049a0e6cada7938a0fc616578aff2b11337d90ec4e99e9fee95e57e9d8371562b31698225fb73c666ba586842b913e67ad289f374f7a46fc873342f805a8683d4e"
  in
    script do
      (secp256k1 derSig hash derPublicKey) === True

throws (so secp256k1 returns false). Maybe I’m doing sth wrong or the DER is wrong (but I checked it, also with AI and other libraries)…

Is there any other way to debug it? Maybe could you help ;/

Likewise, when I check your supplied DER hex strings, I’m not observing anything obviously untoward.

If, instead of using the Daml code, I use an online tool (e.g. ECDSA Verify Signature - Online Tools) I observe that if I use:

  • 049a0e6cada7938a0fc616578aff2b11337d90ec4e99e9fee95e57e9d8371562b31698225fb73c666ba586842b913e67ad289f374f7a46fc873342f805a8683d4e as a hex public key
  • 2ca4de4ee6b5c3526edcb911382e806a8eddc037ddc2a90022bcf195d851720202ddb1ce34bba06d0f15c80d22d70f74d49093ea696c863dfb224cf2806ef61a1c as a hex signature value
  • dd573f5c0bebc4a46b180c446652c76e3e48867fea4e6b33f378ac99d3c09afa or 45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c6e38216a0197d021692000000020000001 as hex message values

Then the online tool complains that the signature is invalid - which is consistent with our failing Daml implementation observations.

Which leads me to wonder if there’s something else going wrong here (especially as 2 independent tools are displaying a failure here)?

My understanding of ecrecover is that there’s a possibility to extract 0, 1 or 2 public keys. To aid extracting a single key, I’d thought that an extended signature encoding was used to encode the 2 integers r and s, along with an extra integer v (sometimes called recid in implementations). When I look at the DER decoded signature, I’m observing the r and s integers, but no v integer - as a result (and I could well be wrong here), could we be extracting the wrong public key?

OK, I’ll try to check it with the v added to the signature.

Thanks!

  1. Does the secp256k1sign add the \x19Ethereum Signed Message:\n32 prefix?
    Maybe that’s the case. Can I see the implementation somewhere?

We use signDigest directly (as it’s already keccak-hashed) without adding the prefix.

  1. I see the online tools report the signature as invalid, but all of our signatures are reported as invalid in these tools. I don’t know why, but in the every implementation on all chains ecrecover works properly with our signatures.

Does the secp256k1sign add the \x19Ethereum Signed Message:\n32 prefix?
Maybe that’s the case. Can I see the implementation somewhere?

Absolutely. After several layers of language/compiler related translation, the Daml call to secp256k1 delegates to:

Along the way, arguments to the Daml secp256k1 function have their data values transformed, but not in any significant way (e.g. no additional hashing is performed, etc.) - all transformations are data format changes. So, by and large, the Daml call to secp256k1 is an almost direct delegation to a BouncyCastle Secp256k1 verification implementation.

So, it (SHA256withECDSA) signs SHA-256 of the data, not the data, as in my investigation. So it cannot be used for our purposes, because we sign the keccak-256 of the data directly, not the SHA-256 of keccak-256 of data :confused:

We also have a question: are you planning to implement a secp256k1 implementation with “clean data” signing? This would facilitate integration with any decentralized application (dapp) in the Ethereum ecosystem.

Hi @Lukasz2891, thanks for that - much appreciated.

Our plan is to implement a variant of SECP256K1 which (under the hood) uses ECDSA only. So a future release of LF and the SDK should resolve this issue for you (sorry, I have no release schedule dates at the moment).

One thing I did note, when I locally built a version of the Daml SDK and compiler that only uses ECDSA, it still didn’t appear to be able to validate the test data from above. Would you be able to check this observation?

Hard to say why ;/ we’re using the data in multiple systems on different platforms - and they are verified, and even recovered properly ;/

Also, the https://toolkit.abdk.consulting/ethereum#recover-address properly recovers it.

It’s important to have the full compatibility with other systems.

Maybe the implementation uses secp256r1 by default, not the secp256k1 curve?

And… I’m starting my vacation tommorrow - so my responses will be limited for the next 2 weeks ;/