From 8ca167577279c2fc491600744c016baba79a6b96 Mon Sep 17 00:00:00 2001 From: Thomas Letan Date: Mon, 7 Jul 2025 17:27:25 +0200 Subject: [PATCH] EVM Node: Add SECP256K1 support for GCP KMS signing * What This patch introduces support for SECP256K1 signature algorithm within the GCP KMS integration. This enables the use of keys stored in Google Cloud Key Management Service that are configured with the SECP256K1 curve for cryptographic operations, specifically for signing. * Why The primary motivation for this change is to allow the Etherlink node to sign transactions using SECP256K1 keys managed by GCP KMS. * How The implementation involves extending the `signature_algorithm` type to include `EC_SIGN_SECP256K1_SHA256` and updating the parsing logic. A `pem_to_der` helper function was added to correctly decode PEM-encoded public keys, and a `tag` function was introduced to provide the correct prefix byte for both P256 and SECP256K1 public keys when converting them to Tezos `Signature.Public_key` format. The `key_of_pem` function was refactored to use this new common DER decoding and tagging logic. --- etherlink/CHANGES_NODE.md | 5 ++- etherlink/bin_node/lib_dev/gcp_kms.ml | 57 ++++++++++++++++++-------- etherlink/bin_node/lib_dev/gcp_kms.mli | 5 +++ etherlink/bin_node/main.ml | 15 +++++-- src/lib_crypto/secp256k1.ml | 25 +++++------ src/lib_crypto/secp256k1.mli | 4 ++ 6 files changed, 76 insertions(+), 35 deletions(-) diff --git a/etherlink/CHANGES_NODE.md b/etherlink/CHANGES_NODE.md index 4e29ffabd591..eaf752727cc1 100644 --- a/etherlink/CHANGES_NODE.md +++ b/etherlink/CHANGES_NODE.md @@ -14,9 +14,10 @@ ### Execution changes +- Add support for GCP KMS `EC_SIGN_SECP256K1_SHA256` keys. (!18618) - Add a new command `show gcp key` which can be used to retrieve Tezos-specific - (b58-encoded public key, Tezos implicit account) information about a key - stored in a GCP KMS. (!18617) + (b58-encoded public key, Tezos implicit account) and Ethereum-specific (EOA + address) information about a key stored in a GCP KMS. (!18617 !18618) ### Storage changes diff --git a/etherlink/bin_node/lib_dev/gcp_kms.ml b/etherlink/bin_node/lib_dev/gcp_kms.ml index d6a59f9f0e12..f1cca4f5b7f3 100644 --- a/etherlink/bin_node/lib_dev/gcp_kms.ml +++ b/etherlink/bin_node/lib_dev/gcp_kms.ml @@ -22,10 +22,13 @@ type partial = unit handler interpret their responses. *) type t = Signature.public_key handler -type signature_algorithm = EC_SIGN_P256_SHA256 +type signature_algorithm = EC_SIGN_P256_SHA256 | EC_SIGN_SECP256K1_SHA256 let signature_algorithm (kms : t) = - match kms.public_key with P256 _ -> EC_SIGN_P256_SHA256 | _ -> assert false + match kms.public_key with + | P256 _ -> EC_SIGN_P256_SHA256 + | Secp256k1 _ -> EC_SIGN_SECP256K1_SHA256 + | _ -> assert false type hash_algorithm = Blake2B @@ -33,6 +36,7 @@ let signature_algorithm_of_string = let open Result_syntax in function | "EC_SIGN_P256_SHA256" -> return EC_SIGN_P256_SHA256 + | "EC_SIGN_SECP256K1_SHA256" -> return EC_SIGN_SECP256K1_SHA256 | algo -> tzfail (error_of_fmt "Unsupported algorithm %s" algo) let service_name = "Gcp_kms" @@ -215,24 +219,29 @@ let extract_tail der_str n = if len < n then error_with "input is too short" else Result.return (String.sub der_str (len - n) n) +let pem_to_der pem = + let lines = String.split_on_char '\n' pem in + let b64 = + lines + |> List.filter (fun l -> not (String.starts_with ~prefix:"-----" l)) + |> String.concat "" + in + Base64.decode_exn b64 + +let tag = function + | EC_SIGN_SECP256K1_SHA256 -> '\x01' + | EC_SIGN_P256_SHA256 -> '\x02' + let key_of_pem algo pem = let open Result_syntax in - match algo with - | EC_SIGN_P256_SHA256 -> ( - let* pem = - match X509.Public_key.decode_pem pem with - | Ok pem -> return pem - | Error (`Msg err) -> - error_with "Could not decode pem public key (%s)" err - in - let key_str = X509.Public_key.encode_der pem in - let* key_str = extract_tail key_str 65 in - match - Signature.Public_key.of_bytes_without_validation - (Bytes.unsafe_of_string (Format.sprintf "\x02%s" key_str)) - with - | Some res -> return res - | _ -> error_with "Not a valid public key") + let der = pem_to_der pem in + let* key_str = extract_tail der 65 in + match + Signature.Public_key.of_bytes_without_validation + (Bytes.unsafe_of_string (Format.sprintf "%c%s" (tag algo) key_str)) + with + | Some res -> return res + | _ -> error_with "Not a valid public key" let public_key kms_handler = let open Lwt_result_syntax in @@ -310,6 +319,9 @@ let signature_of_b64encoded algo b64_sig = | EC_SIGN_P256_SHA256 -> let*? p256_sig = Signature.P256.of_string signature in return (Signature.of_p256 p256_sig) + | EC_SIGN_SECP256K1_SHA256 -> + let*? secp256k1_sig = Signature.Secp256k1.of_string signature in + return (Signature.of_secp256k1 secp256k1_sig) let sign kms_handler hash payload = let open Lwt_result_syntax in @@ -371,3 +383,12 @@ let from_gcp_key (config : Configuration.gcp_kms) gcp_key = return {kms_handler with public_key} let public_key t = t.public_key + +let ethereum_address_opt kms = + let pk = public_key (kms : t) in + match pk with + | Secp256k1 s -> + let str = Signature.Secp256k1.eth_address_of_public_key s in + let (`Hex hex) = Hex.of_bytes str in + Some Ethereum_types.(Address (Hex hex)) + | _ -> None diff --git a/etherlink/bin_node/lib_dev/gcp_kms.mli b/etherlink/bin_node/lib_dev/gcp_kms.mli index 6fbac911de86..dd76621a4230 100644 --- a/etherlink/bin_node/lib_dev/gcp_kms.mli +++ b/etherlink/bin_node/lib_dev/gcp_kms.mli @@ -17,3 +17,8 @@ val gcp_key : t -> Configuration.gcp_key val public_key : t -> Signature.Public_key.t val sign : t -> hash_algorithm -> bytes -> Signature.t tzresult Lwt.t + +(** [ethereum_address_opt kms] returns the Ethereum address of the key managed + by [kms], if said key is compatible ([EC_SIGN_SECP256K1_SHA256]). Returns + [None] otherwise. *) +val ethereum_address_opt : t -> Ethereum_types.address option diff --git a/etherlink/bin_node/main.ml b/etherlink/bin_node/main.ml index 83eb40388238..ee3a177c8a61 100644 --- a/etherlink/bin_node/main.ml +++ b/etherlink/bin_node/main.ml @@ -1427,15 +1427,24 @@ let show_kms_key_info_command = match gcp_key_from_string_opt key_str with | Some gcp_key -> let open Evm_node_lib_dev in + let open Evm_node_lib_dev_encoding in let* kms = Gcp_kms.from_gcp_key config.gcp_kms gcp_key in let pk = Gcp_kms.public_key kms in let pkh = Signature.Public_key.hash pk in - Format.printf - "@[Public key: %a@ Public key hash: %a@ @]" + let eth_opt = Gcp_kms.ethereum_address_opt kms in + let open Format in + printf + "@[Public key: %a@ Public key hash: %a%a@ @]" Signature.Public_key.pp pk Signature.Public_key_hash.pp - pkh ; + pkh + (pp_print_option (fun fmt addr -> + fprintf + fmt + "@ Ethereum address: %s" + (Ethereum_types.Address.to_string addr))) + eth_opt ; return_unit | None -> failwith "%s is not a valid URI for a key held by a GCP KMS" key_str) diff --git a/src/lib_crypto/secp256k1.ml b/src/lib_crypto/secp256k1.ml index 87e3b026f1f8..20c8c5fba62d 100644 --- a/src/lib_crypto/secp256k1.ml +++ b/src/lib_crypto/secp256k1.ml @@ -353,6 +353,18 @@ let deterministic_nonce_hash sk msg = let pop_verify _ ?msg:_ _ = false +let public_key_to_bytes_uncompressed pk = + Bigstring.to_bytes (Key.to_bytes ~compress:false context pk) + +let eth_address_of_public_key s = + let s_bytes = public_key_to_bytes_uncompressed s in + (* The public key hash is the hash of pk[1..]. *) + let s_hash = + Hacl.Hash.Keccak_256.digest Bytes.(sub s_bytes 1 (length s_bytes - 1)) + in + (* The ethereum address is pkhash[12..]. *) + Bytes.(sub s_hash 12 (length s_hash - 12)) + let recover signature msg = let open Error_monad.Result_syntax in (* Decode the signature. *) @@ -361,16 +373,5 @@ let recover signature msg = in (* Recover the public key that signed the message. *) let* public_key = Sign.recover context ~signature (Bigstring.of_bytes msg) in - let public_key_bytes = - Key.to_bytes ~compress:false context public_key |> Bigstring.to_bytes - in - (* The public key hash is the hash of pk[1..]. *) - let public_key_hash = - Hacl.Hash.Keccak_256.digest - (Bytes.sub public_key_bytes 1 (Bytes.length public_key_bytes - 1)) - in - (* The ethereum address is pkhash[12..]. *) - let address = - Bytes.sub public_key_hash 12 (Bytes.length public_key_hash - 12) - in + let address = eth_address_of_public_key public_key in return address diff --git a/src/lib_crypto/secp256k1.mli b/src/lib_crypto/secp256k1.mli index 3518a7f70960..ea0e7675070d 100644 --- a/src/lib_crypto/secp256k1.mli +++ b/src/lib_crypto/secp256k1.mli @@ -41,3 +41,7 @@ val check_keccak256 : Public_key.t -> t -> bytes -> bool the rightmost 160-bits of the Keccak-256 hash of the corresponding ECDSA public key. *) val recover : bytes -> bytes -> (bytes, string) result + +(** [eth_address_of_public_key pk] computes the Ethereum address of an + Externally Owned Account (EOA) using this public key. *) +val eth_address_of_public_key : Public_key.t -> bytes -- GitLab