Does Canton support secp256k1?
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.
Thanks for the reply 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.
Still, ecrecover
-like would be helpful - I couldn’t find it in the repo
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 ofbase-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 key2ca4de4ee6b5c3526edcb911382e806a8eddc037ddc2a90022bcf195d851720202ddb1ce34bba06d0f15c80d22d70f74d49093ea696c863dfb224cf2806ef61a1c
as a hex signature valuedd573f5c0bebc4a46b180c446652c76e3e48867fea4e6b33f378ac99d3c09afa
or45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c6e38216a0197d021692000000020000001
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!
- 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.
- 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:
- the Engine/Speedy builtin SBSECP256K1Bool
- which in turn delegates to the library function MessageSignature.verify
- and then delegates to the BouncyCastle based new MessageSignaturePrototype(“SHA256withECDSA”).verify
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
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 ;/