diff --git a/go.mod b/go.mod index 3679ed787d8a..ac38807dbaae 100644 --- a/go.mod +++ b/go.mod @@ -88,6 +88,26 @@ require ( k8s.io/utils v0.0.0-20230726121419-3b25d923346b ) +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.36.2 // indirect + github.com/aws/aws-sdk-go-v2/config v1.29.7 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.60 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14 // indirect + github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.19 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.24.16 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.15 // indirect + github.com/aws/smithy-go v1.22.2 // indirect + github.com/cubist-labs/cubesigner-go-sdk v0.0.16 + github.com/oapi-codegen/runtime v1.1.1 // indirect +) + require ( github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect @@ -155,7 +175,7 @@ require ( github.com/kylelemons/godebug v1.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-shellwords v1.0.12 // indirect github.com/mitchellh/pointerstructure v1.2.0 // indirect diff --git a/go.sum b/go.sum index 27aaec848f0f..89adbac2f65c 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,7 @@ github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec/go.mod h1 github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKzY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/Shopify/goreferrer v0.0.0-20181106222321-ec9c9a553398/go.mod h1:a1uqRtAwp2Xwc6WNPJEufxJ7fx3npB4UV/JOLmbu5I0= github.com/StephenButtolph/canoto v0.17.2 h1:kRLJwtYk0bzdGEeEvwHaVmmDm0HFHxrS0VlVN5Hyo7U= github.com/StephenButtolph/canoto v0.17.2/go.mod h1:IcnAHC6nJUfQFVR9y60ko2ecUqqHHSB6UwI9NnBFZnE= @@ -29,6 +30,8 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/antithesishq/antithesis-sdk-go v0.3.8 h1:OvGoHxIcOXFJLyn9IJQ5DzByZ3YVAWNBc394ObzDRb8= github.com/antithesishq/antithesis-sdk-go v0.3.8/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= @@ -42,11 +45,40 @@ github.com/ava-labs/libevm v1.13.15-0.20251002164226-35926db4d661 h1:lt4yQE1HMvx github.com/ava-labs/libevm v1.13.15-0.20251002164226-35926db4d661/go.mod h1:ivRC/KojP8sai7j8WnpXIReQpcRklL2bIzoysnjpARQ= github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19 h1:S6oFasZsplNmw8B2S8cMJQMa62nT5ZKGzZRdCpd+5qQ= github.com/ava-labs/simplex v0.0.0-20250919142550-9cdfff10fd19/go.mod h1:GVzumIo3zR23/qGRN2AdnVkIPHcKMq/D89EGWZfMGQ0= +github.com/aws/aws-sdk-go-v2 v1.36.2 h1:Ub6I4lq/71+tPb/atswvToaLGVMxKZvjYDVOWEExOcU= +github.com/aws/aws-sdk-go-v2 v1.36.2/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/config v1.29.7 h1:71nqi6gUbAUiEQkypHQcNVSFJVUFANpSeUNShiwWX2M= +github.com/aws/aws-sdk-go-v2/config v1.29.7/go.mod h1:yqJQ3nh2HWw/uxd56bicyvmDW4KSc+4wN6lL8pYjynU= +github.com/aws/aws-sdk-go-v2/credentials v1.17.60 h1:1dq+ELaT5ogfmqtV1eocq8SpOK1NRsuUfmhQtD/XAh4= +github.com/aws/aws-sdk-go-v2/credentials v1.17.60/go.mod h1:HDes+fn/xo9VeszXqjBVkxOo/aUy8Mc6QqKvZk32GlE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29 h1:JO8pydejFKmGcUNiiwt75dzLHRWthkwApIvPoyUtXEg= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29/go.mod h1:adxZ9i9DRmB8zAT0pO0yGnsmu0geomp5a3uq5XpgOJ8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 h1:knLyPMw3r3JsU8MFHWctE4/e2qWbPaxDYLlohPvnY8c= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33/go.mod h1:EBp2HQ3f+XCB+5J+IoEbGhoV7CpJbnrsd4asNXmTL0A= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 h1:K0+Ne08zqti8J9jwENxZ5NoUyBnaFDTu3apwQJWrwwA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33/go.mod h1:K97stwwzaWzmqxO8yLGHhClbVW1tC6VT1pDLk1pGrq4= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14 h1:2scbY6//jy/s8+5vGrk7l1+UtHl0h9A4MjOO2k/TM2E= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14/go.mod h1:bRpZPHZpSe5YRHmPfK3h1M7UBFCn2szHzyx0rw04zro= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.19 h1:O2xbipq7k1kTct69V7mFidwTagld9c/6iyK+3yo+QNg= +github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.34.19/go.mod h1:CxTOwBy2Qs8/+yV7fkz4eZB1RB5qeWaW9SvznvFLgRA= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.16 h1:YV6xIKDJp6U7YB2bxfud9IENO1LRpGhe2Tv/OKtPrOQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.16/go.mod h1:DvbmMKgtpA6OihFJK13gHMZOZrCHttz8wPHGKXqU+3o= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15 h1:kMyK3aKotq1aTBsj1eS8ERJLjqYRRRcsmP33ozlCvlk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15/go.mod h1:5uPZU7vSNzb8Y0dm75xTikinegPYK3uJmIHQZFq5Aqo= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.15 h1:ht1jVmeeo2anR7zDiYJLSnRYnO/9NILXXu42FP3rJg0= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.15/go.mod h1:xWZ5cOiFe3czngChE4LhCBqUxNwgfwndEF7XlYP/yD8= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.0 h1:V2/ZgjfDFIygAX3ZapeigkVBoVUtOJKSwrhZdlpSvaA= @@ -124,6 +156,8 @@ github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233/go.mod h1:geZJ github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cubist-labs/cubesigner-go-sdk v0.0.16 h1:X7kPk4BhVggw/XtKXPJcT1kgnV0v70nIdqn+wAnJgqU= +github.com/cubist-labs/cubesigner-go-sdk v0.0.16/go.mod h1:aSYPLNkIt10+QQiNI4ctE+wQ1IoJdUqlUlLCmwYXk2w= github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -334,6 +368,7 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= @@ -377,8 +412,8 @@ github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2y github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -424,6 +459,8 @@ github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d/go.mod h1:o96d github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -515,6 +552,7 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -717,6 +755,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/utils/crypto/keychain/cubesigner/cubesigner_client.go b/utils/crypto/keychain/cubesigner/cubesigner_client.go new file mode 100644 index 000000000000..b40e9e9ea660 --- /dev/null +++ b/utils/crypto/keychain/cubesigner/cubesigner_client.go @@ -0,0 +1,25 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package cubesigner + +import ( + "github.com/cubist-labs/cubesigner-go-sdk/client" + "github.com/cubist-labs/cubesigner-go-sdk/models" +) + +// CubeSignerClient defines the interface for CubeSigner client operations +// needed by the keychain implementation +type CubeSignerClient interface { + // GetKeyInOrg retrieves key information for the given keyID from the CubeSigner organization. + // It returns the key metadata, including public key and key type. + GetKeyInOrg(keyID string) (*models.KeyInfo, error) + + // BlobSign signs arbitrary data using the specified keyID. + // request contains the data to be signed. + BlobSign(keyID string, request models.BlobSignRequest, receipts ...*client.MfaReceipt) (*client.CubeSignerResponse[models.SignResponse], error) + + // AvaSerializedTxSign signs Avalanche transactions using the specified chainAlias (P/X/C), and materialID (address). + // request contains the serialized transaction data to be signed. + AvaSerializedTxSign(chainAlias, materialID string, request models.AvaSerializedTxSignRequest, receipts ...*client.MfaReceipt) (*client.CubeSignerResponse[models.SignResponse], error) +} diff --git a/utils/crypto/keychain/cubesigner/cubesigner_keychain.go b/utils/crypto/keychain/cubesigner/cubesigner_keychain.go new file mode 100644 index 000000000000..9bb08de3f8f9 --- /dev/null +++ b/utils/crypto/keychain/cubesigner/cubesigner_keychain.go @@ -0,0 +1,273 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package cubesigner + +import ( + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "strings" + + "github.com/ava-labs/libevm/common" + "github.com/cubist-labs/cubesigner-go-sdk/client" + "github.com/cubist-labs/cubesigner-go-sdk/models" + "golang.org/x/exp/maps" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/constants" + "github.com/ava-labs/avalanchego/utils/crypto/keychain" + "github.com/ava-labs/avalanchego/utils/formatting/address" + "github.com/ava-labs/avalanchego/utils/set" + "github.com/ava-labs/avalanchego/wallet/chain/c" + + avasecp256k1 "github.com/ava-labs/avalanchego/utils/crypto/secp256k1" + secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +var ( + _ keychain.Keychain = (*Keychain)(nil) + _ c.EthKeychain = (*Keychain)(nil) + _ keychain.Signer = (*cubesignerSigner)(nil) + _ CubeSignerClient = (*client.ApiClient)(nil) + + ErrNoKeysProvided = errors.New("you need to provide at least one key to create a server keychain") + ErrEmptySignatureFromServer = errors.New("empty signature obtained from server") + ErrChainAliasMissing = errors.New("chainAlias must be specified in options for CubeSigner") + ErrInvalidChainAlias = errors.New("chainAlias must be 'P', 'X' or 'C' for CubeSigner") + ErrNetworkIDMissing = errors.New("network ID must be specified in options for CubeSigner") + ErrUnsupportedKeyType = errors.New("unsupported key type") + ErrInvalidPublicKey = errors.New("invalid public key format") +) + +const ( + // UncompressedPublicKeyLength is the expected length of an uncompressed secp256k1 public key in bytes. + // This includes the prefix byte (1 byte) plus the X and Y coordinates (32 bytes each). + UncompressedPublicKeyLength = 65 + // UncompressedPublicKeyPrefix is the prefix byte for uncompressed secp256k1 public keys. + // This byte indicates that the key is in uncompressed format. + UncompressedPublicKeyPrefix = 0x04 +) + +// keyInfo holds both the public key and keyID for a CubeSigner key. +type keyInfo struct { + pubKey *avasecp256k1.PublicKey // The Avalanche public key derived from CubeSigner + keyID string // The CubeSigner key identifier +} + +// Keychain provides an abstraction over CubeSigner remote signing capabilities. +type Keychain struct { + cubesignerClient CubeSignerClient // Client for CubeSigner API operations + avaAddrToKeyInfo map[ids.ShortID]*keyInfo // Maps Avalanche addresses to key info + ethAddrToKeyInfo map[common.Address]*keyInfo // Maps Ethereum addresses to key info +} + +// processKey obtains and processes key information from CubeSigner. +// It validates that the key exists in the CubeSigner organization, verifies +// that the key type is supported (secp256k1 for Avalanche/Ethereum), and +// converts the public key from hex format to an Avalanche public key. +func processKey( + cubesignerClient CubeSignerClient, + keyID string, +) (*avasecp256k1.PublicKey, error) { + // Validate key exists + keyInfo, err := cubesignerClient.GetKeyInOrg(keyID) + if err != nil { + return nil, fmt.Errorf("could not find server key %s: %w", keyID, err) + } + + // Validate key type + switch keyInfo.KeyType { + case models.SecpAvaAddr, models.SecpAvaTestAddr, models.SecpEthAddr: + // Supported key types + default: + return nil, fmt.Errorf("keytype %s of server key %s: %w", keyInfo.KeyType, keyID, ErrUnsupportedKeyType) + } + + // get public key + pubKeyHex := strings.TrimPrefix(keyInfo.PublicKey, "0x") + pubKeyBytes, err := hex.DecodeString(pubKeyHex) + if err != nil { + return nil, fmt.Errorf("%w: failed to decode public key for server key %s: %w", ErrInvalidPublicKey, keyID, err) + } + if len(pubKeyBytes) != UncompressedPublicKeyLength { + return nil, fmt.Errorf("invalid public key length for server key %s: expected %d bytes, got %d", keyID, UncompressedPublicKeyLength, len(pubKeyBytes)) + } + if pubKeyBytes[0] != UncompressedPublicKeyPrefix { + return nil, fmt.Errorf("invalid public key format for server key %s: expected uncompressed format (0x%02x prefix), got 0x%02x", keyID, UncompressedPublicKeyPrefix, pubKeyBytes[0]) + } + pubKey, err := secp256k1.ParsePubKey(pubKeyBytes) + if err != nil { + return nil, fmt.Errorf("invalid public key format for server key %s: %w", keyID, err) + } + avaPubKey, err := avasecp256k1.ToPublicKey(pubKey.SerializeCompressed()) + if err != nil { + return nil, fmt.Errorf("invalid public key format for server key %s: %w", keyID, err) + } + + return avaPubKey, nil +} + +// NewKeychain creates a new keychain abstraction over a CubeSigner connection. +// It validates that all provided keyIDs exist in the CubeSigner organization and returns +// a keychain that can be used to sign transactions using those keys. +func NewKeychain( + cubesignerClient CubeSignerClient, + keyIDs []string, +) (*Keychain, error) { + if len(keyIDs) == 0 { + return nil, ErrNoKeysProvided + } + + avaAddrToKeyInfo := map[ids.ShortID]*keyInfo{} + ethAddrToKeyInfo := map[common.Address]*keyInfo{} + + for _, keyID := range keyIDs { + avaPubKey, err := processKey(cubesignerClient, keyID) + if err != nil { + return nil, err + } + + keyInf := &keyInfo{ + pubKey: avaPubKey, + keyID: keyID, + } + + avaAddrToKeyInfo[avaPubKey.Address()] = keyInf + ethAddrToKeyInfo[avaPubKey.EthAddress()] = keyInf + } + + return &Keychain{ + cubesignerClient: cubesignerClient, + avaAddrToKeyInfo: avaAddrToKeyInfo, + ethAddrToKeyInfo: ethAddrToKeyInfo, + }, nil +} + +// Addresses returns the set of Avalanche addresses that this keychain can sign for. +func (kc *Keychain) Addresses() set.Set[ids.ShortID] { + return set.Of(maps.Keys(kc.avaAddrToKeyInfo)...) +} + +// Get returns a signer for the given Avalanche address, if it exists in this keychain. +func (kc *Keychain) Get(addr ids.ShortID) (keychain.Signer, bool) { + keyInf, found := kc.avaAddrToKeyInfo[addr] + if !found { + return nil, false + } + return &cubesignerSigner{ + cubesignerClient: kc.cubesignerClient, + pubKey: keyInf.pubKey, + keyID: keyInf.keyID, + }, true +} + +// EthAddresses returns the set of Ethereum addresses that this keychain can sign for. +func (kc *Keychain) EthAddresses() set.Set[common.Address] { + return set.Of(maps.Keys(kc.ethAddrToKeyInfo)...) +} + +// GetEth returns a signer for the given Ethereum address, if it exists in this keychain. +func (kc *Keychain) GetEth(addr common.Address) (keychain.Signer, bool) { + keyInf, found := kc.ethAddrToKeyInfo[addr] + if !found { + return nil, false + } + return &cubesignerSigner{ + cubesignerClient: kc.cubesignerClient, + pubKey: keyInf.pubKey, + keyID: keyInf.keyID, + }, true +} + +// cubesignerAvagoSigner is an abstraction of the underlying cubesigner connection, +// to be able sign for a specific address +type cubesignerSigner struct { + cubesignerClient CubeSignerClient + pubKey *avasecp256k1.PublicKey + keyID string +} + +// processSignatureResponse is a helper function that processes the common response +// pattern from CubeSigner signing operations. It decodes the hex signature and validates its length. +func processSignatureResponse(signatureHex string) ([]byte, error) { + signatureBytes, err := hex.DecodeString(strings.TrimPrefix(signatureHex, "0x")) + if err != nil { + return nil, fmt.Errorf("failed to decode server's signature: %w", err) + } + if len(signatureBytes) != avasecp256k1.SignatureLen { + return nil, fmt.Errorf("invalid server's signature length: expected %d bytes, got %d", avasecp256k1.SignatureLen, len(signatureBytes)) + } + return signatureBytes, nil +} + +// SignHash signs the given hash using CubeSigner's BlobSign API. +// It expects to receive a hash of the unsigned transaction bytes. +func (s *cubesignerSigner) SignHash(b []byte) ([]byte, error) { + response, err := s.cubesignerClient.BlobSign( + s.keyID, + models.BlobSignRequest{ + MessageBase64: base64.StdEncoding.EncodeToString(b), + }, + ) + if err != nil { + return nil, fmt.Errorf("server signing err: %w", err) + } + if response.ResponseData == nil { + return nil, ErrEmptySignatureFromServer + } + return processSignatureResponse(response.ResponseData.Signature) +} + +// Sign signs the given payload according to the given signing options. +// It expects to receive the unsigned transaction bytes and requires ChainAlias and NetworkID +// to be specified in the signing options for CubeSigner's AvaSerializedTxSign API. +func (s *cubesignerSigner) Sign(b []byte, opts ...keychain.SigningOption) ([]byte, error) { + options := &keychain.SigningOptions{} + for _, opt := range opts { + opt(options) + } + // Require chainAlias and network from options + if options.ChainAlias == "" { + return nil, ErrChainAliasMissing + } + if options.ChainAlias != "P" && options.ChainAlias != "X" && options.ChainAlias != "C" { + return nil, fmt.Errorf("%w, got %q", ErrInvalidChainAlias, options.ChainAlias) + } + if options.NetworkID == 0 { + return nil, ErrNetworkIDMissing + } + var materialID string + if options.ChainAlias == "C" { + materialID = s.pubKey.EthAddress().Hex() + } else { + hrp := constants.GetHRP(options.NetworkID) + addr := s.pubKey.Address() + var err error + materialID, err = address.FormatBech32(hrp, addr.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to format %s address %v as Bech32: %w", hrp, addr, err) + } + } + + response, err := s.cubesignerClient.AvaSerializedTxSign( + options.ChainAlias, + materialID, + models.AvaSerializedTxSignRequest{ + Tx: "0x" + hex.EncodeToString(b), + }, + ) + if err != nil { + return nil, fmt.Errorf("server signing err: %w", err) + } + if response.ResponseData == nil { + return nil, ErrEmptySignatureFromServer + } + return processSignatureResponse(response.ResponseData.Signature) +} + +// Address returns the Avalanche address associated with this signer. +func (s *cubesignerSigner) Address() ids.ShortID { + return s.pubKey.Address() +} diff --git a/utils/crypto/keychain/cubesigner/cubesigner_keychain_test.go b/utils/crypto/keychain/cubesigner/cubesigner_keychain_test.go new file mode 100644 index 000000000000..5b21b60ccdb2 --- /dev/null +++ b/utils/crypto/keychain/cubesigner/cubesigner_keychain_test.go @@ -0,0 +1,334 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package cubesigner + +import ( + "errors" + "testing" + + "github.com/ava-labs/libevm/common" + "github.com/cubist-labs/cubesigner-go-sdk/client" + "github.com/cubist-labs/cubesigner-go-sdk/models" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/utils/crypto/keychain" + "github.com/ava-labs/avalanchego/utils/crypto/keychain/cubesigner/cubesignermock" +) + +var errTest = errors.New("test") + +const testKeyID = "test-key-id" + +func TestNewKeychain(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + // user provides no keys + mockClient := cubesignermock.NewCubeSignerClient(ctrl) + _, err := NewKeychain(mockClient, []string{}) + require.ErrorIs(err, ErrNoKeysProvided) + + // client returns error when getting key info + mockClient = cubesignermock.NewCubeSignerClient(ctrl) + mockClient.EXPECT().GetKeyInOrg(testKeyID).Return(nil, errTest).Times(1) + _, err = NewKeychain(mockClient, []string{testKeyID}) + require.ErrorIs(err, errTest) + + // client returns unsupported key type + mockClient = cubesignermock.NewCubeSignerClient(ctrl) + keyInfo := &models.KeyInfo{ + KeyType: "UnsupportedType", + PublicKey: "0x04" + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + } + mockClient.EXPECT().GetKeyInOrg(testKeyID).Return(keyInfo, nil).Times(1) + _, err = NewKeychain(mockClient, []string{testKeyID}) + require.ErrorIs(err, ErrUnsupportedKeyType) + + // client returns invalid public key format + mockClient = cubesignermock.NewCubeSignerClient(ctrl) + keyInfo = &models.KeyInfo{ + KeyType: models.SecpAvaAddr, + PublicKey: "invalid-hex", + } + mockClient.EXPECT().GetKeyInOrg(testKeyID).Return(keyInfo, nil).Times(1) + _, err = NewKeychain(mockClient, []string{testKeyID}) + require.ErrorIs(err, ErrInvalidPublicKey) + + // good path - Avalanche address + mockClient = cubesignermock.NewCubeSignerClient(ctrl) + keyInfo = &models.KeyInfo{ + KeyType: models.SecpAvaAddr, + PublicKey: "0x04" + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + } + mockClient.EXPECT().GetKeyInOrg(testKeyID).Return(keyInfo, nil).Times(1) + kc, err := NewKeychain(mockClient, []string{testKeyID}) + require.NoError(err) + require.NotNil(kc) + + // good path - Ethereum address + mockClient = cubesignermock.NewCubeSignerClient(ctrl) + keyInfo = &models.KeyInfo{ + KeyType: models.SecpEthAddr, + PublicKey: "0x04" + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + } + mockClient.EXPECT().GetKeyInOrg(testKeyID).Return(keyInfo, nil).Times(1) + kc, err = NewKeychain(mockClient, []string{testKeyID}) + require.NoError(err) + require.NotNil(kc) +} + +func TestKeychain_Addresses(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + mockClient := cubesignermock.NewCubeSignerClient(ctrl) + + keyInfo := &models.KeyInfo{ + KeyType: models.SecpAvaAddr, + PublicKey: "0x04" + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + } + mockClient.EXPECT().GetKeyInOrg(testKeyID).Return(keyInfo, nil).Times(1) + + kc, err := NewKeychain(mockClient, []string{testKeyID}) + require.NoError(err) + + addresses := kc.Addresses() + require.Len(addresses, 1) +} + +func TestKeychain_Get(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + mockClient := cubesignermock.NewCubeSignerClient(ctrl) + + keyInfo := &models.KeyInfo{ + KeyType: models.SecpAvaAddr, + PublicKey: "0x04" + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + } + mockClient.EXPECT().GetKeyInOrg(testKeyID).Return(keyInfo, nil).Times(1) + + kc, err := NewKeychain(mockClient, []string{testKeyID}) + require.NoError(err) + + addresses := kc.Addresses() + require.Len(addresses, 1) + + addr := addresses.List()[0] + signer, found := kc.Get(addr) + require.True(found) + require.NotNil(signer) + require.Equal(addr, signer.Address()) + + // test with non-existent address + randomAddr := ids.GenerateTestShortID() + signer, found = kc.Get(randomAddr) + require.False(found) + require.Nil(signer) +} + +func TestKeychain_EthAddresses(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + mockClient := cubesignermock.NewCubeSignerClient(ctrl) + + keyInfo := &models.KeyInfo{ + KeyType: models.SecpEthAddr, + PublicKey: "0x04" + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + } + mockClient.EXPECT().GetKeyInOrg(testKeyID).Return(keyInfo, nil).Times(1) + + kc, err := NewKeychain(mockClient, []string{testKeyID}) + require.NoError(err) + + ethAddresses := kc.EthAddresses() + require.Len(ethAddresses, 1) +} + +func TestKeychain_GetEth(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + mockClient := cubesignermock.NewCubeSignerClient(ctrl) + + keyInfo := &models.KeyInfo{ + KeyType: models.SecpEthAddr, + PublicKey: "0x04" + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + } + mockClient.EXPECT().GetKeyInOrg(testKeyID).Return(keyInfo, nil).Times(1) + + kc, err := NewKeychain(mockClient, []string{testKeyID}) + require.NoError(err) + + ethAddresses := kc.EthAddresses() + require.Len(ethAddresses, 1) + + ethAddr := ethAddresses.List()[0] + signer, found := kc.GetEth(ethAddr) + require.True(found) + require.NotNil(signer) + + // Test non-existent address + nonExistentAddr := common.HexToAddress("0x0000000000000000000000000000000000000000") + signer, found = kc.GetEth(nonExistentAddr) + require.False(found) + require.Nil(signer) +} + +func TestCubesignerSigner_SignHash(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + mockClient := cubesignermock.NewCubeSignerClient(ctrl) + + keyInfo := &models.KeyInfo{ + KeyType: models.SecpAvaAddr, + PublicKey: "0x04" + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + } + mockClient.EXPECT().GetKeyInOrg(testKeyID).Return(keyInfo, nil).Times(1) + + kc, err := NewKeychain(mockClient, []string{testKeyID}) + require.NoError(err) + + addresses := kc.Addresses() + addr := addresses.List()[0] + signer, found := kc.Get(addr) + require.True(found) + + hash := []byte("test-hash") + + // test successful signing + response := &client.CubeSignerResponse[models.SignResponse]{ + ResponseData: &models.SignResponse{ + Signature: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01", + }, + } + mockClient.EXPECT().BlobSign(testKeyID, gomock.Any()).Return(response, nil).Times(1) + + signature, err := signer.SignHash(hash) + require.NoError(err) + require.NotNil(signature) + + // test client error + mockClient.EXPECT().BlobSign(testKeyID, gomock.Any()).Return(nil, errTest).Times(1) + _, err = signer.SignHash(hash) + require.ErrorIs(err, errTest) + + // test empty response + emptyResponse := &client.CubeSignerResponse[models.SignResponse]{ + ResponseData: nil, + } + mockClient.EXPECT().BlobSign(testKeyID, gomock.Any()).Return(emptyResponse, nil).Times(1) + _, err = signer.SignHash(hash) + require.ErrorIs(err, ErrEmptySignatureFromServer) +} + +func TestCubesignerSigner_Sign(t *testing.T) { + require := require.New(t) + ctrl := gomock.NewController(t) + + mockClient := cubesignermock.NewCubeSignerClient(ctrl) + + keyInfo := &models.KeyInfo{ + KeyType: models.SecpAvaAddr, + PublicKey: "0x04" + "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8", + } + mockClient.EXPECT().GetKeyInOrg(testKeyID).Return(keyInfo, nil).Times(1) + + kc, err := NewKeychain(mockClient, []string{testKeyID}) + require.NoError(err) + + addresses := kc.Addresses() + addr := addresses.List()[0] + signer, found := kc.Get(addr) + require.True(found) + + txBytes := []byte("test-transaction-bytes") + + // Test missing chain alias + _, err = signer.Sign(txBytes) + require.ErrorIs(err, ErrChainAliasMissing) + + // Test invalid chain alias + _, err = signer.Sign(txBytes, keychain.WithChainAlias("invalid")) + require.ErrorIs(err, ErrInvalidChainAlias) + + // Test missing network ID + _, err = signer.Sign(txBytes, keychain.WithChainAlias("P")) + require.ErrorIs(err, ErrNetworkIDMissing) + + // Test successful P-chain signing + response := &client.CubeSignerResponse[models.SignResponse]{ + ResponseData: &models.SignResponse{ + Signature: "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef01", + }, + } + mockClient.EXPECT().AvaSerializedTxSign( + "P", + gomock.Any(), // materialID (Bech32 address) + gomock.Any(), // request with hex-encoded tx + ).Return(response, nil).Times(1) + + signature, err := signer.Sign(txBytes, + keychain.WithChainAlias("P"), + keychain.WithNetworkID(1)) + require.NoError(err) + require.NotNil(signature) + + // Test successful X-chain signing + mockClient.EXPECT().AvaSerializedTxSign( + "X", + gomock.Any(), // materialID (Bech32 address) + gomock.Any(), // request with hex-encoded tx + ).Return(response, nil).Times(1) + + signature, err = signer.Sign(txBytes, + keychain.WithChainAlias("X"), + keychain.WithNetworkID(1)) + require.NoError(err) + require.NotNil(signature) + + // Test successful C-chain signing + mockClient.EXPECT().AvaSerializedTxSign( + "C", + gomock.Any(), // materialID (Eth address) + gomock.Any(), // request with hex-encoded tx + ).Return(response, nil).Times(1) + + signature, err = signer.Sign(txBytes, + keychain.WithChainAlias("C"), + keychain.WithNetworkID(1)) + require.NoError(err) + require.NotNil(signature) + + // Test client error + mockClient.EXPECT().AvaSerializedTxSign( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(nil, errTest).Times(1) + + _, err = signer.Sign(txBytes, + keychain.WithChainAlias("P"), + keychain.WithNetworkID(1)) + require.ErrorIs(err, errTest) + + // Test empty response + emptyResponse := &client.CubeSignerResponse[models.SignResponse]{ + ResponseData: nil, + } + mockClient.EXPECT().AvaSerializedTxSign( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ).Return(emptyResponse, nil).Times(1) + + _, err = signer.Sign(txBytes, + keychain.WithChainAlias("P"), + keychain.WithNetworkID(1)) + require.ErrorIs(err, ErrEmptySignatureFromServer) +} diff --git a/utils/crypto/keychain/cubesigner/cubesignermock/cubesigner_client.go b/utils/crypto/keychain/cubesigner/cubesignermock/cubesigner_client.go new file mode 100644 index 000000000000..b2ce1b184945 --- /dev/null +++ b/utils/crypto/keychain/cubesigner/cubesignermock/cubesigner_client.go @@ -0,0 +1,97 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/ava-labs/avalanchego/utils/crypto/keychain/cubesigner (interfaces: CubeSignerClient) +// +// Generated by this command: +// +// mockgen -package=cubesignermock -destination=cubesignermock/cubesigner_client.go -mock_names=CubeSignerClient=CubeSignerClient . CubeSignerClient +// + +// Package cubesignermock is a generated GoMock package. +package cubesignermock + +import ( + reflect "reflect" + + client "github.com/cubist-labs/cubesigner-go-sdk/client" + models "github.com/cubist-labs/cubesigner-go-sdk/models" + gomock "go.uber.org/mock/gomock" +) + +// CubeSignerClient is a mock of CubeSignerClient interface. +type CubeSignerClient struct { + ctrl *gomock.Controller + recorder *CubeSignerClientMockRecorder + isgomock struct{} +} + +// CubeSignerClientMockRecorder is the mock recorder for CubeSignerClient. +type CubeSignerClientMockRecorder struct { + mock *CubeSignerClient +} + +// NewCubeSignerClient creates a new mock instance. +func NewCubeSignerClient(ctrl *gomock.Controller) *CubeSignerClient { + mock := &CubeSignerClient{ctrl: ctrl} + mock.recorder = &CubeSignerClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *CubeSignerClient) EXPECT() *CubeSignerClientMockRecorder { + return m.recorder +} + +// AvaSerializedTxSign mocks base method. +func (m *CubeSignerClient) AvaSerializedTxSign(chainAlias, materialID string, request models.AvaSerializedTxSignRequest, receipts ...*client.MfaReceipt) (*client.CubeSignerResponse[models.SignResponse], error) { + m.ctrl.T.Helper() + varargs := []any{chainAlias, materialID, request} + for _, a := range receipts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "AvaSerializedTxSign", varargs...) + ret0, _ := ret[0].(*client.CubeSignerResponse[models.SignResponse]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AvaSerializedTxSign indicates an expected call of AvaSerializedTxSign. +func (mr *CubeSignerClientMockRecorder) AvaSerializedTxSign(chainAlias, materialID, request any, receipts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{chainAlias, materialID, request}, receipts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AvaSerializedTxSign", reflect.TypeOf((*CubeSignerClient)(nil).AvaSerializedTxSign), varargs...) +} + +// BlobSign mocks base method. +func (m *CubeSignerClient) BlobSign(keyID string, request models.BlobSignRequest, receipts ...*client.MfaReceipt) (*client.CubeSignerResponse[models.SignResponse], error) { + m.ctrl.T.Helper() + varargs := []any{keyID, request} + for _, a := range receipts { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "BlobSign", varargs...) + ret0, _ := ret[0].(*client.CubeSignerResponse[models.SignResponse]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// BlobSign indicates an expected call of BlobSign. +func (mr *CubeSignerClientMockRecorder) BlobSign(keyID, request any, receipts ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{keyID, request}, receipts...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlobSign", reflect.TypeOf((*CubeSignerClient)(nil).BlobSign), varargs...) +} + +// GetKeyInOrg mocks base method. +func (m *CubeSignerClient) GetKeyInOrg(keyID string) (*models.KeyInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetKeyInOrg", keyID) + ret0, _ := ret[0].(*models.KeyInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetKeyInOrg indicates an expected call of GetKeyInOrg. +func (mr *CubeSignerClientMockRecorder) GetKeyInOrg(keyID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetKeyInOrg", reflect.TypeOf((*CubeSignerClient)(nil).GetKeyInOrg), keyID) +} diff --git a/utils/crypto/keychain/cubesigner/mocks_generate_test.go b/utils/crypto/keychain/cubesigner/mocks_generate_test.go new file mode 100644 index 000000000000..dbe08ac881d7 --- /dev/null +++ b/utils/crypto/keychain/cubesigner/mocks_generate_test.go @@ -0,0 +1,6 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package cubesigner + +//go:generate go run go.uber.org/mock/mockgen -package=${GOPACKAGE}mock -destination=${GOPACKAGE}mock/cubesigner_client.go -mock_names=CubeSignerClient=CubeSignerClient . CubeSignerClient diff --git a/utils/crypto/keychain/keychain.go b/utils/crypto/keychain/keychain.go index a27f8be24baf..5223b33ef881 100644 --- a/utils/crypto/keychain/keychain.go +++ b/utils/crypto/keychain/keychain.go @@ -22,10 +22,10 @@ var ( ) // Signer implements functions for a keychain to return its main address and -// to sign a hash +// to sign a hash or transaction type Signer interface { SignHash([]byte) ([]byte, error) - Sign([]byte) ([]byte, error) + Sign([]byte, ...SigningOption) ([]byte, error) Address() ids.ShortID } @@ -141,7 +141,7 @@ func (l *ledgerSigner) SignHash(b []byte) ([]byte, error) { } // expects to receive the unsigned tx bytes -func (l *ledgerSigner) Sign(b []byte) ([]byte, error) { +func (l *ledgerSigner) Sign(b []byte, _ ...SigningOption) ([]byte, error) { // Sign using the address with index l.idx on the ledger device. The number // of returned signatures should be the same length as the provided indices. sigs, err := l.ledger.Sign(b, []uint32{l.idx}) diff --git a/utils/crypto/keychain/signing_options.go b/utils/crypto/keychain/signing_options.go new file mode 100644 index 000000000000..15086f6a9b6d --- /dev/null +++ b/utils/crypto/keychain/signing_options.go @@ -0,0 +1,27 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package keychain + +// SigningOptions contains context information for signing operations +type SigningOptions struct { + ChainAlias string + NetworkID uint32 +} + +// SigningOption is a function that modifies SigningOptions +type SigningOption func(*SigningOptions) + +// WithChainAlias sets the chain alias for signing context +func WithChainAlias(chainAlias string) SigningOption { + return func(opts *SigningOptions) { + opts.ChainAlias = chainAlias + } +} + +// WithNetworkID sets the network ID for signing context +func WithNetworkID(networkID uint32) SigningOption { + return func(opts *SigningOptions) { + opts.NetworkID = networkID + } +} diff --git a/utils/crypto/secp256k1/secp256k1.go b/utils/crypto/secp256k1/secp256k1.go index 41c3cdaa057a..7d9368531e76 100644 --- a/utils/crypto/secp256k1/secp256k1.go +++ b/utils/crypto/secp256k1/secp256k1.go @@ -16,6 +16,7 @@ import ( "github.com/ava-labs/avalanchego/cache/lru" "github.com/ava-labs/avalanchego/ids" "github.com/ava-labs/avalanchego/utils/cb58" + "github.com/ava-labs/avalanchego/utils/crypto/keychain" "github.com/ava-labs/avalanchego/utils/hashing" stdecdsa "crypto/ecdsa" @@ -212,7 +213,8 @@ func (k *PrivateKey) EthAddress() common.Address { return crypto.PubkeyToAddress(*(k.PublicKey().ToECDSA())) } -func (k *PrivateKey) Sign(msg []byte) ([]byte, error) { +func (k *PrivateKey) Sign(msg []byte, _ ...keychain.SigningOption) ([]byte, error) { + // Ignore options - secp256k1 signing doesn't need chain/network context return k.SignHash(hashing.ComputeHash256(msg)) } diff --git a/vms/avm/txs/txstest/builder.go b/vms/avm/txs/txstest/builder.go index beae84f2eb72..e6b2a45db786 100644 --- a/vms/avm/txs/txstest/builder.go +++ b/vms/avm/txs/txstest/builder.go @@ -225,7 +225,7 @@ func (b *Builder) builders(kc *secp256k1fx.Keychain) (builder.Builder, signer.Si addrs: addrs, } builder = builder.New(addrs, b.ctx, wa) - signer = signer.New(kc, wa) + signer = signer.New(kc, wa, b.ctx.NetworkID) ) return builder, signer } diff --git a/vms/platformvm/txs/txstest/wallet.go b/vms/platformvm/txs/txstest/wallet.go index 4a9e74b60aac..74db93fb8671 100644 --- a/vms/platformvm/txs/txstest/wallet.go +++ b/vms/platformvm/txs/txstest/wallet.go @@ -112,6 +112,7 @@ func NewWallet( signer.New( kc, backend, + ctx.NetworkID, ), ) } diff --git a/wallet/chain/p/signer/signer.go b/wallet/chain/p/signer/signer.go index 4d8a4e2f6635..b00916ccd7e3 100644 --- a/wallet/chain/p/signer/signer.go +++ b/wallet/chain/p/signer/signer.go @@ -33,23 +33,26 @@ type Backend interface { } type txSigner struct { - kc keychain.Keychain - backend Backend + kc keychain.Keychain + backend Backend + networkID uint32 } -func New(kc keychain.Keychain, backend Backend) Signer { +func New(kc keychain.Keychain, backend Backend, networkID uint32) Signer { return &txSigner{ - kc: kc, - backend: backend, + kc: kc, + backend: backend, + networkID: networkID, } } func (s *txSigner) Sign(ctx stdcontext.Context, tx *txs.Tx) error { return tx.Unsigned.Visit(&visitor{ - kc: s.kc, - backend: s.backend, - ctx: ctx, - tx: tx, + kc: s.kc, + backend: s.backend, + ctx: ctx, + tx: tx, + networkID: s.networkID, }) } diff --git a/wallet/chain/p/signer/visitor.go b/wallet/chain/p/signer/visitor.go index d2162d08e207..a881249ac825 100644 --- a/wallet/chain/p/signer/visitor.go +++ b/wallet/chain/p/signer/visitor.go @@ -19,6 +19,7 @@ import ( "github.com/ava-labs/avalanchego/vms/platformvm/stakeable" "github.com/ava-labs/avalanchego/vms/platformvm/txs" "github.com/ava-labs/avalanchego/vms/secp256k1fx" + "github.com/ava-labs/avalanchego/wallet/chain/p/builder" ) var ( @@ -37,10 +38,11 @@ var ( // visitor handles signing transactions for the signer type visitor struct { - kc keychain.Keychain - backend Backend - ctx context.Context - tx *txs.Tx + kc keychain.Keychain + backend Backend + ctx context.Context + tx *txs.Tx + networkID uint32 } func (*visitor) AdvanceTimeTx(*txs.AdvanceTimeTx) error { @@ -56,7 +58,7 @@ func (s *visitor) AddValidatorTx(tx *txs.AddValidatorTx) error { if err != nil { return err } - return sign(s.tx, false, txSigners) + return sign(s.tx, false, txSigners, s.networkID) } func (s *visitor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { @@ -69,7 +71,7 @@ func (s *visitor) AddSubnetValidatorTx(tx *txs.AddSubnetValidatorTx) error { return err } txSigners = append(txSigners, subnetAuthSigners) - return sign(s.tx, false, txSigners) + return sign(s.tx, false, txSigners, s.networkID) } func (s *visitor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { @@ -77,7 +79,7 @@ func (s *visitor) AddDelegatorTx(tx *txs.AddDelegatorTx) error { if err != nil { return err } - return sign(s.tx, false, txSigners) + return sign(s.tx, false, txSigners, s.networkID) } func (s *visitor) CreateChainTx(tx *txs.CreateChainTx) error { @@ -90,7 +92,7 @@ func (s *visitor) CreateChainTx(tx *txs.CreateChainTx) error { return err } txSigners = append(txSigners, subnetAuthSigners) - return sign(s.tx, false, txSigners) + return sign(s.tx, false, txSigners, s.networkID) } func (s *visitor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { @@ -98,7 +100,7 @@ func (s *visitor) CreateSubnetTx(tx *txs.CreateSubnetTx) error { if err != nil { return err } - return sign(s.tx, false, txSigners) + return sign(s.tx, false, txSigners, s.networkID) } func (s *visitor) ImportTx(tx *txs.ImportTx) error { @@ -111,7 +113,7 @@ func (s *visitor) ImportTx(tx *txs.ImportTx) error { return err } txSigners = append(txSigners, txImportSigners...) - return sign(s.tx, false, txSigners) + return sign(s.tx, false, txSigners, s.networkID) } func (s *visitor) ExportTx(tx *txs.ExportTx) error { @@ -119,7 +121,7 @@ func (s *visitor) ExportTx(tx *txs.ExportTx) error { if err != nil { return err } - return sign(s.tx, false, txSigners) + return sign(s.tx, false, txSigners, s.networkID) } func (s *visitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error { @@ -132,7 +134,7 @@ func (s *visitor) RemoveSubnetValidatorTx(tx *txs.RemoveSubnetValidatorTx) error return err } txSigners = append(txSigners, subnetAuthSigners) - return sign(s.tx, true, txSigners) + return sign(s.tx, true, txSigners, s.networkID) } func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { @@ -145,7 +147,7 @@ func (s *visitor) TransformSubnetTx(tx *txs.TransformSubnetTx) error { return err } txSigners = append(txSigners, subnetAuthSigners) - return sign(s.tx, true, txSigners) + return sign(s.tx, true, txSigners, s.networkID) } func (s *visitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidatorTx) error { @@ -153,7 +155,7 @@ func (s *visitor) AddPermissionlessValidatorTx(tx *txs.AddPermissionlessValidato if err != nil { return err } - return sign(s.tx, true, txSigners) + return sign(s.tx, true, txSigners, s.networkID) } func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegatorTx) error { @@ -161,7 +163,7 @@ func (s *visitor) AddPermissionlessDelegatorTx(tx *txs.AddPermissionlessDelegato if err != nil { return err } - return sign(s.tx, true, txSigners) + return sign(s.tx, true, txSigners, s.networkID) } func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) error { @@ -174,7 +176,7 @@ func (s *visitor) TransferSubnetOwnershipTx(tx *txs.TransferSubnetOwnershipTx) e return err } txSigners = append(txSigners, subnetAuthSigners) - return sign(s.tx, true, txSigners) + return sign(s.tx, true, txSigners, s.networkID) } func (s *visitor) BaseTx(tx *txs.BaseTx) error { @@ -182,7 +184,7 @@ func (s *visitor) BaseTx(tx *txs.BaseTx) error { if err != nil { return err } - return sign(s.tx, false, txSigners) + return sign(s.tx, false, txSigners, s.networkID) } func (s *visitor) ConvertSubnetToL1Tx(tx *txs.ConvertSubnetToL1Tx) error { @@ -195,7 +197,7 @@ func (s *visitor) ConvertSubnetToL1Tx(tx *txs.ConvertSubnetToL1Tx) error { return err } txSigners = append(txSigners, subnetAuthSigners) - return sign(s.tx, true, txSigners) + return sign(s.tx, true, txSigners, s.networkID) } func (s *visitor) RegisterL1ValidatorTx(tx *txs.RegisterL1ValidatorTx) error { @@ -203,7 +205,7 @@ func (s *visitor) RegisterL1ValidatorTx(tx *txs.RegisterL1ValidatorTx) error { if err != nil { return err } - return sign(s.tx, true, txSigners) + return sign(s.tx, true, txSigners, s.networkID) } func (s *visitor) SetL1ValidatorWeightTx(tx *txs.SetL1ValidatorWeightTx) error { @@ -211,7 +213,7 @@ func (s *visitor) SetL1ValidatorWeightTx(tx *txs.SetL1ValidatorWeightTx) error { if err != nil { return err } - return sign(s.tx, true, txSigners) + return sign(s.tx, true, txSigners, s.networkID) } func (s *visitor) IncreaseL1ValidatorBalanceTx(tx *txs.IncreaseL1ValidatorBalanceTx) error { @@ -219,7 +221,7 @@ func (s *visitor) IncreaseL1ValidatorBalanceTx(tx *txs.IncreaseL1ValidatorBalanc if err != nil { return err } - return sign(s.tx, true, txSigners) + return sign(s.tx, true, txSigners, s.networkID) } func (s *visitor) DisableL1ValidatorTx(tx *txs.DisableL1ValidatorTx) error { @@ -232,7 +234,7 @@ func (s *visitor) DisableL1ValidatorTx(tx *txs.DisableL1ValidatorTx) error { return err } txSigners = append(txSigners, disableAuthSigners) - return sign(s.tx, true, txSigners) + return sign(s.tx, true, txSigners, s.networkID) } func (s *visitor) getSigners(sourceChainID ids.ID, ins []*avax.TransferableInput) ([][]keychain.Signer, error) { @@ -328,7 +330,7 @@ func (s *visitor) getAuthSigners(ownerID ids.ID, auth verify.Verifiable) ([]keyc } // TODO: remove [signHash] after the ledger supports signing all transactions. -func sign(tx *txs.Tx, signHash bool, txSigners [][]keychain.Signer) error { +func sign(tx *txs.Tx, signHash bool, txSigners [][]keychain.Signer, networkID uint32) error { unsignedBytes, err := txs.Codec.Marshal(txs.CodecVersion, &tx.Unsigned) if err != nil { return fmt.Errorf("couldn't marshal unsigned tx: %w", err) @@ -380,7 +382,9 @@ func sign(tx *txs.Tx, signHash bool, txSigners [][]keychain.Signer) error { if signHash { sig, err = signer.SignHash(unsignedHash) } else { - sig, err = signer.Sign(unsignedBytes) + sig, err = signer.Sign(unsignedBytes, + keychain.WithChainAlias(builder.Alias), + keychain.WithNetworkID(networkID)) } if err != nil { return fmt.Errorf("problem signing tx: %w", err) diff --git a/wallet/chain/x/signer/signer.go b/wallet/chain/x/signer/signer.go index 5e22f523d5aa..02b3ec9c6017 100644 --- a/wallet/chain/x/signer/signer.go +++ b/wallet/chain/x/signer/signer.go @@ -31,23 +31,26 @@ type Backend interface { } type signer struct { - kc keychain.Keychain - backend Backend + kc keychain.Keychain + backend Backend + networkID uint32 } -func New(kc keychain.Keychain, backend Backend) Signer { +func New(kc keychain.Keychain, backend Backend, networkID uint32) Signer { return &signer{ - kc: kc, - backend: backend, + kc: kc, + backend: backend, + networkID: networkID, } } func (s *signer) Sign(ctx context.Context, tx *txs.Tx) error { return tx.Unsigned.Visit(&visitor{ - kc: s.kc, - backend: s.backend, - ctx: ctx, - tx: tx, + kc: s.kc, + backend: s.backend, + ctx: ctx, + tx: tx, + networkID: s.networkID, }) } diff --git a/wallet/chain/x/signer/visitor.go b/wallet/chain/x/signer/visitor.go index e9f1ce64a7bc..e1f2af8e8516 100644 --- a/wallet/chain/x/signer/visitor.go +++ b/wallet/chain/x/signer/visitor.go @@ -37,10 +37,11 @@ var ( // visitor handles signing transactions for the signer type visitor struct { - kc keychain.Keychain - backend Backend - ctx context.Context - tx *txs.Tx + kc keychain.Keychain + backend Backend + ctx context.Context + tx *txs.Tx + networkID uint32 } func (s *visitor) BaseTx(tx *txs.BaseTx) error { @@ -48,7 +49,7 @@ func (s *visitor) BaseTx(tx *txs.BaseTx) error { if err != nil { return err } - return sign(s.tx, txCreds, txSigners) + return sign(s.tx, txCreds, txSigners, s.networkID) } func (s *visitor) CreateAssetTx(tx *txs.CreateAssetTx) error { @@ -56,7 +57,7 @@ func (s *visitor) CreateAssetTx(tx *txs.CreateAssetTx) error { if err != nil { return err } - return sign(s.tx, txCreds, txSigners) + return sign(s.tx, txCreds, txSigners, s.networkID) } func (s *visitor) OperationTx(tx *txs.OperationTx) error { @@ -70,7 +71,7 @@ func (s *visitor) OperationTx(tx *txs.OperationTx) error { } txCreds = append(txCreds, txOpsCreds...) txSigners = append(txSigners, txOpsSigners...) - return sign(s.tx, txCreds, txSigners) + return sign(s.tx, txCreds, txSigners, s.networkID) } func (s *visitor) ImportTx(tx *txs.ImportTx) error { @@ -84,7 +85,7 @@ func (s *visitor) ImportTx(tx *txs.ImportTx) error { } txCreds = append(txCreds, txImportCreds...) txSigners = append(txSigners, txImportSigners...) - return sign(s.tx, txCreds, txSigners) + return sign(s.tx, txCreds, txSigners, s.networkID) } func (s *visitor) ExportTx(tx *txs.ExportTx) error { @@ -92,7 +93,7 @@ func (s *visitor) ExportTx(tx *txs.ExportTx) error { if err != nil { return err } - return sign(s.tx, txCreds, txSigners) + return sign(s.tx, txCreds, txSigners, s.networkID) } func (s *visitor) getSigners(ctx context.Context, sourceChainID ids.ID, ins []*avax.TransferableInput) ([]verify.Verifiable, [][]keychain.Signer, error) { @@ -218,7 +219,7 @@ func (s *visitor) getOpsSigners(ctx context.Context, sourceChainID ids.ID, ops [ return txCreds, txSigners, nil } -func sign(tx *txs.Tx, creds []verify.Verifiable, txSigners [][]keychain.Signer) error { +func sign(tx *txs.Tx, creds []verify.Verifiable, txSigners [][]keychain.Signer, networkID uint32) error { codec := builder.Parser.Codec() unsignedBytes, err := codec.Marshal(txs.CodecVersion, &tx.Unsigned) if err != nil { @@ -282,7 +283,9 @@ func sign(tx *txs.Tx, creds []verify.Verifiable, txSigners [][]keychain.Signer) continue } - sig, err := signer.Sign(unsignedBytes) + sig, err := signer.Sign(unsignedBytes, + keychain.WithChainAlias(builder.Alias), + keychain.WithNetworkID(networkID)) if err != nil { return fmt.Errorf("problem signing tx: %w", err) } diff --git a/wallet/subnet/primary/wallet.go b/wallet/subnet/primary/wallet.go index 52951121c328..a51fbd5af879 100644 --- a/wallet/subnet/primary/wallet.go +++ b/wallet/subnet/primary/wallet.go @@ -106,13 +106,13 @@ func MakeWallet( pBackend := pwallet.NewBackend(pUTXOs, owners) pClient := p.NewClient(avaxState.PClient, pBackend) pBuilder := pbuilder.New(avaxAddrs, avaxState.PCTX, pBackend) - pSigner := psigner.New(avaxKeychain, pBackend) + pSigner := psigner.New(avaxKeychain, pBackend, avaxState.PCTX.NetworkID) xChainID := avaxState.XCTX.BlockchainID xUTXOs := common.NewChainUTXOs(xChainID, avaxState.UTXOs) xBackend := x.NewBackend(avaxState.XCTX, xUTXOs) xBuilder := xbuilder.New(avaxAddrs, avaxState.XCTX, xBackend) - xSigner := xsigner.New(avaxKeychain, xBackend) + xSigner := xsigner.New(avaxKeychain, xBackend, avaxState.XCTX.NetworkID) cChainID := avaxState.CCTX.BlockchainID cUTXOs := common.NewChainUTXOs(cChainID, avaxState.UTXOs) @@ -157,6 +157,6 @@ func MakePWallet( pBackend := pwallet.NewBackend(pUTXOs, owners) pClient := p.NewClient(client, pBackend) pBuilder := pbuilder.New(addrs, context, pBackend) - pSigner := psigner.New(keychain, pBackend) + pSigner := psigner.New(keychain, pBackend, context.NetworkID) return pwallet.New(pClient, pBuilder, pSigner), nil }