Secp256k1 support
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:
DA.Crypto.Text — Digital Asset’s platform documentation
…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:
Canton 3.3 Release Notes for Splice 0.4.0
Splice 0.4.0 includes Canton 3.3 This is the release notes for Canton 3.3.
(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-16encodings 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:
049a0e6cada7938a0fc616578aff2b11337d90ec4e99e9fee95e57e9d8371562b31698225fb73c666ba586842b913e67ad289f374f7a46fc873342f805a8683d4eas a hex public key2ca4de4ee6b5c3526edcb911382e806a8eddc037ddc2a90022bcf195d851720202ddb1ce34bba06d0f15c80d22d70f74d49093ea696c863dfb224cf2806ef61a1cas a hex signature valuedd573f5c0bebc4a46b180c446652c76e3e48867fea4e6b33f378ac99d3c09afaor45544800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003c6e38216a0197d021692000000020000001as 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
secp256k1signadd the\x19Ethereum Signed Message:\n32prefix?
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
ecrecoverworks properly with our signatures.
Does the
secp256k1signadd the\x19Ethereum Signed Message:\n32prefix?
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 ;/
Hello ![]()
Any updates in the topic?
Currently, the next release (3.4) of LF and the SDK is scheduled for the end of September. That release should contain a variant of SECP256K1 which only uses ECDSA.
Hello.
I see some changes merged to the main branch of Daml, but I don’t see the 3.4.0-snapshot.20250820.0 contains the changes - should I use an additional compilation flag? I see on the main branch it’s hidden behind the feature DAML_CRYPTO_ADDITIONS, so should we enable it in another way or just to wait for a newer snapshot?
Now, I’m using the 20250827 snapshot, but still,
Should it be enabled in any special way?
```shell
./CryptoExample.daml:12:17: error:
Variable not in scope:
secp256k1signWithEcdsaOnly : Text → Text → Script b0
File: ./CryptoExample.daml
Hidden: no
Range: 16:8-16:30
Source: typecheck
Severity: DsError
Message:
./CryptoExample.daml:16:8: error:
Variable not in scope:
secp256k1WithEcdsaOnly : Text → Text → Text → Bool
```
Your Daml project will need to target 2.dev. That should then allow you to compile using these new features.
So, building your project code with daml build --target=2.dev should be sufficient here.
Alternatively, you can add the following to your project’s daml.yaml file:
build-options:
- --target=2.dev
If you also want to disable the crypto warnings on compilation, also add the following to your project’s daml.yaml file:
build-options:
- -Wno-crypto-text-is-alpha
Thanks. I’m able to build it.
But it’s still not working properly.
Looking into the java docs,
The ECDSA signature algorithms as defined in ANSI X9.62.
Note: "ECDSA" is an ambiguous name for the "SHA1withECDSA" algorithm and should not be used. The formal name "SHA1withECDSA" should be used instead.
Java Security Standard Algorithm Names
So there probably should be NoneWithECDSA used instead of ECDSA as an algorithm passed to new MessageSignaturePrototypeUtil(“ECDSA”) for pure secp256k1 without hashing.
For your example from tests
github.com/digital-asset/damlsdk/compiler/damlc/tests/daml-test-files/CCTPMintTokenWithECDSA.daml
-- Copyright (c) 2020, Digital Asset (Switzerland) GmbH and/or its affiliates.
-- All rights reserved.
-- @SINCE-LF 2.dev
{-# OPTIONS_GHC -Wno-x-crypto #-}
module CCTPMintTokenWithECDSA where
import DA.Assert ((===))
import DA.Crypto.Text
import Daml.Script
type Bytes32Hex = Text
type UInt32Hex = Text
type UInt64Hex = Text
type UInt256Hex = Text
data BurnMessage = BurnMessage with
version : UInt32Hex
This file has been truncated. show original
it should return sth like that:
PrivKey 7308c95bf6e240ed8de37b5a7c5f453d88ece2b5e93c02ef985e8553f856474a
PubKey 043f4ae6efb79de2cf60636219110f11b695d5c1776c0b0dad1468672fba1c6f6acf79396b8403e110cbf60ccd7aefab4c541d49844a51049fcbd22dae1a51d681
PubKeyDer 3056301006072a8648ce3d020106052b8104000a034200043f4ae6efb79de2cf60636219110f11b695d5c1776c0b0dad1468672fba1c6f6acf79396b8403e110cbf60ccd7aefab4c541d49844a51049fcbd22dae1a51d681
Digest b03c694bc07762ef8f08a0260d68dd6ecc9da10a6fe1c1abfb6a21f71e88ff1c
SigEcdsa d5bbc8f7144e31b95b19d96e5631864dec0eddb84d96c0d0f6b36d6a96982d58148bf5301f6897ccd380aa6b558fc3a57e07adbf17dbb24824c57536ce1e46cb
SigDigest d5bbc8f7144e31b95b19d96e5631864dec0eddb84d96c0d0f6b36d6a96982d58148bf5301f6897ccd380aa6b558fc3a57e07adbf17dbb24824c57536ce1e46cb
SigDer 3045022100d5bbc8f7144e31b95b19d96e5631864dec0eddb84d96c0d0f6b36d6a96982d580220148bf5301f6897ccd380aa6b558fc3a57e07adbf17dbb24824c57536ce1e46cb
but
msgSig ← secp256k1signWithEcdsaOnly privateKey1 expectedDigest
debug msgSig
returns
3045022100bdbe3c37aa32885baedc4f3b6a6fdf3064ccb841e1ed7e269b8735b289743a4c0220413cbe3121071160ec7eddc81f69190930f5126fda2aa37d16b1755eb67cb299
Thanks for that @Lukasz2891
Let me patch the underlying code and lets see if things work better.
Hi @Lukasz2891, thanks again for your help on this.
A patch for this is now in place and I’ve validated my changes against your original code snippet. The next snapshot release should allow you to validate this on your side.
Thanks ![]()
Hello ![]()
Will be maybe a new snapshot available this week?
Hi @Lukasz2891. Just been checking with the release team. Apologies, but the snapshot release has unfortunately been delayed until next week.
Would the snapshot be released this week :)?
Hi @Lukasz2891. Apologies for the delay in answering you here - I was waiting for confirmation that the snapshot release had actually occurred.
Snapshot release 3.4.0-snapshot.20250911.0 contains the fixes you are after.
Thanks, I was sure I confirmed it - maybe that was on our TG ![]()
Do you have a plan to enable it by default, not by 2.dev target?
Yes, plans are currently in motion to do this.

