├── .flake8 ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── app.py ├── applications └── ethereum-signer │ ├── cmd │ ├── key-generator_enclave │ │ ├── main.go │ │ ├── run.sh │ │ └── server.go │ ├── key-generator_pod │ │ ├── functions.go │ │ ├── main.go │ │ └── run.sh │ ├── kmstool-cli │ │ ├── README.md │ │ ├── cmd │ │ │ ├── cli.go │ │ │ ├── cli_test.go │ │ │ └── testdata │ │ │ │ ├── basic_decrypt_ephemeral-key.golden │ │ │ │ ├── basic_decrypt_missing_nsm.golden │ │ │ │ └── missing_region.golden │ │ └── main.go │ ├── lambda │ │ ├── handler.go │ │ └── main.go │ ├── signing_enclave │ │ ├── main.go │ │ ├── run.sh │ │ └── server.go │ └── signing_pod │ │ ├── functions.go │ │ ├── main.go │ │ └── run.sh │ ├── go.mod │ ├── go.sum │ ├── images │ ├── key-generator_enclave │ │ └── Dockerfile │ ├── key-generator_pod │ │ └── Dockerfile │ ├── lambda │ │ └── Dockerfile │ ├── signing_enclave │ │ └── Dockerfile │ └── signing_pod │ │ └── Dockerfile │ └── internal │ ├── attestation │ └── attestation.go │ ├── aws │ └── util.go │ ├── enclave │ ├── config.go │ ├── util.go │ └── util_test.go │ ├── ethereum │ ├── ethereum_test.go │ ├── transaction.go │ ├── units.go │ └── userop.go │ ├── hmac │ ├── hmac.go │ └── hmac_test.go │ ├── keymanagement │ ├── kms.go │ ├── kms_test.go │ ├── storage.go │ ├── storage_test.go │ ├── util.go │ └── util_test.go │ ├── metrics │ ├── client.go │ └── server.go │ ├── pod │ ├── config.go │ └── credentials.go │ └── types │ ├── kms.go │ ├── metrics.go │ └── signing.go ├── assets ├── deployment_confirmation.png ├── deployment_output.png ├── eks_enclaves.png ├── eks_enclaves_key_generation.png ├── eks_overview.png ├── eks_overview_v2-EKS_Cluster.png ├── eks_overview_v2-Web3_app.png ├── eks_overview_v2.png ├── key_generation_flow.png ├── metrics.png └── signing_flow.png ├── cdk.json ├── eks_nitro_wallet ├── __init__.py ├── eks_nitro_cluster_stack.py ├── eks_utils.py └── eth_key_management_app_stack.py ├── lib ├── docker │ ├── Dockerfile_device_plugin │ ├── Dockerfile_device_plugin_amazon_linux_2023 │ ├── Dockerfile_go_base │ ├── Dockerfile_nitro-cli_build │ └── Dockerfile_pod ├── k8s_templates │ ├── deployment_spec_template.yaml │ ├── external_dns.yaml │ ├── load_balancer_service_template.yaml │ └── metrics_server.yaml ├── policy_templates │ ├── alb_policy.json │ └── enclave_key_policy.json └── user_data │ └── user_data.sh ├── requirements-dev.txt ├── requirements.txt ├── scripts ├── apply_deployment_spec.sh ├── build_docker_base.sh ├── build_enclave_image.sh ├── build_kmstool_enclave_cli.sh ├── build_nitro_cli.sh ├── build_vsock_proxy.sh ├── configure_environment.sh ├── delete_deployment.sh ├── eks_aws_nitro_enclaves_plugin.sh ├── generate_key_policy.sh ├── get_pcr0.sh └── update_deployment.sh └── tests ├── __init__.py ├── e2e ├── .env ├── cleanup.sh ├── ethereum-apps_lambda_invoke.sh ├── postman │ ├── eks_nitro_wallet.postman_collection.json │ └── eks_nitro_wallet_performance.postman_collection.json ├── run_test_pipeline.sh ├── setup.sh └── signing_load_test.sh └── unit ├── __init__.py └── test_eks_nitro_wallet_stack.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 140 3 | exclude = tests/* 4 | max-complexity = 80 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | package-lock.json 3 | __pycache__ 4 | .pytest_cache 5 | .venv 6 | *.egg-info 7 | 8 | # CDK asset staging directory 9 | .cdk.staging 10 | cdk.out 11 | .idea 12 | AppOutput* 13 | EksOutput* 14 | *.eif 15 | ./lib/enclave/kms/* 16 | ./lib/enclave/proxy/* 17 | lib/enclave/ethereum-signer/kms/* 18 | lib/enclave/web3signer/kms/* 19 | lib/enclave/web3signer/proxy/* 20 | applications/ethereum-signer/enclave/kms/* 21 | applications/web3signer/enclave/kms/* 22 | applications/web3signer/enclave/proxy/* 23 | applications/ethereum-signer/third_party/* 24 | .DS_Store 25 | cdk.out.* 26 | *.json 27 | .vsock_base_port_assignments.tmp 28 | applications/ethereum-signer/cmd/kmstool-cli/kmstool-cli 29 | applications/ethereum-signer/vendor 30 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | SHELL := env PATH=$(PATH) /bin/bash 5 | PIP := pip3 6 | PYTHON := python3 7 | 8 | clean: 9 | find . -name "*.pyc" -o -name "__pycache__" | xargs rm -rf 10 | rm -rf .venv cdk.out 11 | 12 | install_dev_dependencies: 13 | pip3 install -r requirements-dev.txt -q 14 | 15 | format: install_dev_dependencies 16 | black lib eks_nitro_wallet app.py 17 | 18 | lint: install_dev_dependencies 19 | -flake8 lib eks_nitro_wallet app.py 20 | 21 | static_security_check: install_dev_dependencies 22 | -bandit -r lib eks_nitro_wallet app.py 23 | find cdk.out/*template.json -exec cfn_nag_scan -i {} \; 24 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | import os 6 | import cdk_nag 7 | from aws_cdk import App, Environment, Aspects 8 | 9 | from eks_nitro_wallet.eks_nitro_cluster_stack import EksNitroWalletStack 10 | from eks_nitro_wallet.eth_key_management_app_stack import NitroWalletAppStack 11 | 12 | prefix = os.getenv("CDK_PREFIX", "") 13 | 14 | app = App() 15 | EksNitroWalletStack( 16 | app, 17 | "{}EksNitroCluster".format(prefix), 18 | env=Environment( 19 | region=os.environ.get("CDK_DEPLOY_REGION"), 20 | account=os.environ.get("CDK_DEPLOY_ACCOUNT"), 21 | ), 22 | ) 23 | 24 | NitroWalletAppStack( 25 | app, 26 | "{}EthKeyManagementApp".format(prefix), 27 | params={"log_level": os.environ.get("CDK_APP_LOG_LEVEL", "WARNING")}, 28 | env=Environment( 29 | region=os.environ.get("CDK_DEPLOY_REGION"), 30 | account=os.environ.get("CDK_DEPLOY_ACCOUNT"), 31 | ), 32 | ) 33 | 34 | Aspects.of(app).add(cdk_nag.AwsSolutionsChecks(verbose=False)) 35 | app.synth() 36 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/key-generator_enclave/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "aws/ethereum-signer/internal/enclave" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var version = "undefined" 15 | 16 | func main() { 17 | log.Infof("starting key generation enclave (%s)", version) 18 | 19 | config, err := enclave.LoadConfig() 20 | if err != nil { 21 | log.Fatalf("failed loading config: %s", err) 22 | } 23 | 24 | server := NewServer(config) 25 | if err := server.Initialize(); err != nil { 26 | log.Fatalf("failed initializing server: %s", err) 27 | } 28 | 29 | server.Run() 30 | } 31 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/key-generator_enclave/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | set -e 6 | set -x 7 | 8 | ip addr add 127.0.0.1/32 dev lo:0 9 | ip addr add 127.0.0.2/32 dev lo:0 10 | ip link set dev lo:0 up 11 | 12 | echo "127.0.0.1 kms.${REGION}.amazonaws.com" >>/etc/hosts 13 | echo "127.0.0.2 dynamodb.${REGION}.amazonaws.com" >>/etc/hosts 14 | 15 | # tcp and kernel tuning would go here 16 | #sysctl -w net.core.rmem_max=16777216 # 4MB 17 | #sysctl -w net.core.wmem_max=16777216 # 4MB 18 | #sysctl -w net.ipv4.tcp_max_syn_backlog=65536 19 | #sysctl -w net.core.somaxconn=65535 20 | #sysctl -w net.core.netdev_max_backlog=65536 21 | 22 | # start outbound proxy for kms 23 | IN_ADDRS=127.0.0.1:443 OUT_ADDRS=3:"$(($PORT))" /app/proxy & 24 | 25 | # start outbound proxy for dynamodb 26 | IN_ADDRS=127.0.0.2:443 OUT_ADDRS=3:"$(($PORT + 1))" /app/proxy & 27 | 28 | /app/key-generator_enclave 29 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/key-generator_enclave/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | package main 7 | 8 | import ( 9 | aws2 "aws/ethereum-signer/internal/aws" 10 | "aws/ethereum-signer/internal/enclave" 11 | "aws/ethereum-signer/internal/keymanagement" 12 | "aws/ethereum-signer/internal/metrics" 13 | signerTypes "aws/ethereum-signer/internal/types" 14 | "crypto/ecdsa" 15 | "encoding/hex" 16 | "encoding/json" 17 | "fmt" 18 | "github.com/ethereum/go-ethereum/crypto" 19 | "github.com/go-playground/validator/v10" 20 | "github.com/mdlayher/vsock" 21 | log "github.com/sirupsen/logrus" 22 | "net" 23 | "sync" 24 | "time" 25 | ) 26 | 27 | const ( 28 | bufferSize = 4096 29 | maxWorkers = 10 30 | metricsCID = 3 31 | metricsFreq = 10 * time.Second 32 | ) 33 | 34 | type Server struct { 35 | config *enclave.Config 36 | validate *validator.Validate 37 | listener *vsock.Listener 38 | metricsClient *metrics.Client 39 | connPool chan struct{} 40 | bufferPool *sync.Pool 41 | } 42 | 43 | func NewServer(config *enclave.Config) *Server { 44 | return &Server{ 45 | config: config, 46 | validate: validator.New(), 47 | connPool: make(chan struct{}, maxWorkers), 48 | bufferPool: &sync.Pool{ 49 | New: func() interface{} { 50 | b := make([]byte, bufferSize) 51 | return &b 52 | }, 53 | }, 54 | } 55 | } 56 | 57 | func (s *Server) Initialize() error { 58 | if err := s.setupLogging(); err != nil { 59 | return fmt.Errorf("failed to setup logging: %w", err) 60 | } 61 | 62 | if err := s.setupVsockListener(); err != nil { 63 | return fmt.Errorf("failed to setup vsock listener: %w", err) 64 | } 65 | 66 | s.setupMetrics() 67 | return nil 68 | } 69 | 70 | func (s *Server) setupLogging() error { 71 | logLevel, err := log.ParseLevel(s.config.LogLevel) 72 | if err != nil { 73 | return fmt.Errorf("invalid log level %s: %w", s.config.LogLevel, err) 74 | } 75 | log.SetLevel(logLevel) 76 | log.Infof("LOG_LEVEL=%s", logLevel) 77 | return nil 78 | } 79 | 80 | func (s *Server) setupVsockListener() error { 81 | contextID, err := vsock.ContextID() 82 | if err != nil { 83 | return fmt.Errorf("failed to get contextID: %w", err) 84 | } 85 | 86 | listener, err := vsock.ListenContextID(contextID, s.config.Port, nil) 87 | if err != nil { 88 | return fmt.Errorf("failed to create listener on port %v and contextID %v: %w", 89 | s.config.Port, contextID, err) 90 | } 91 | s.listener = listener 92 | return nil 93 | } 94 | 95 | func (s *Server) setupMetrics() { 96 | s.metricsClient = metrics.NewMetricsClient(metricsCID, 97 | s.config.Port+metrics.PortOffset, metricsFreq) 98 | s.metricsClient.Start() 99 | log.Infof("metrics client started with target cid: %d, port: %d", 100 | metricsCID, s.config.Port+metrics.PortOffset) 101 | } 102 | 103 | func (s *Server) Run() { 104 | log.Info("starting listener for key generation requests") 105 | for { 106 | conn, err := s.listener.Accept() 107 | if err != nil { 108 | log.Errorf("failed accepting connection: %v", err) 109 | continue 110 | } 111 | 112 | s.connPool <- struct{}{} // acquire connection slot 113 | go s.handleConnection(conn) 114 | } 115 | } 116 | 117 | func (s *Server) handleConnection(conn net.Conn) { 118 | defer func() { 119 | err := conn.Close() 120 | if err != nil { 121 | log.Errorf("failed closing connection: %v", err) 122 | return 123 | } 124 | <-s.connPool // release connection slot 125 | }() 126 | 127 | payload, err := s.readAndValidatePayload(conn) 128 | if err != nil { 129 | enclave.HandleError(conn, err.Error(), 400) 130 | return 131 | } 132 | 133 | keyData, err := s.generateKeyPair() 134 | if err != nil { 135 | enclave.HandleError(conn, err.Error(), 500) 136 | return 137 | } 138 | 139 | if err := s.processAndStoreKey(conn, keyData, payload); err != nil { 140 | enclave.HandleError(conn, err.Error(), 500) 141 | return 142 | } 143 | } 144 | 145 | func (s *Server) readAndValidatePayload(conn net.Conn) (*signerTypes.EnclaveKeyGenerationPayload, error) { 146 | buf := *(s.bufferPool.Get().(*[]byte)) 147 | defer s.bufferPool.Put(&buf) 148 | 149 | n, err := conn.Read(buf) 150 | if err != nil { 151 | return nil, fmt.Errorf("failed reading from connection: %w", err) 152 | } 153 | 154 | var payload signerTypes.EnclaveKeyGenerationPayload 155 | if err := json.Unmarshal(buf[:n], &payload); err != nil { 156 | return nil, fmt.Errorf("failed unmarshalling payload: %w", err) 157 | } 158 | 159 | if err := s.validate.Struct(payload); err != nil { 160 | return nil, fmt.Errorf("payload validation failed: %w", err) 161 | } 162 | 163 | return &payload, nil 164 | } 165 | 166 | func (s *Server) generateKeyPair() (*keyData, error) { 167 | ethPrivateKey, err := crypto.GenerateKey() 168 | if err != nil { 169 | return nil, fmt.Errorf("failed generating Ethereum private key: %w", err) 170 | } 171 | 172 | publicKey, ok := ethPrivateKey.Public().(*ecdsa.PublicKey) 173 | if !ok || publicKey == nil { 174 | return nil, fmt.Errorf("invalid public key generated") 175 | } 176 | 177 | return &keyData{ 178 | privateKey: ethPrivateKey, 179 | address: crypto.PubkeyToAddress(*publicKey).Hex(), 180 | }, nil 181 | } 182 | 183 | type keyData struct { 184 | privateKey *ecdsa.PrivateKey 185 | address string 186 | } 187 | 188 | func (s *Server) processAndStoreKey(conn net.Conn, kd *keyData, payload *signerTypes.EnclaveKeyGenerationPayload) error { 189 | plainKey := signerTypes.PlainKey{ 190 | EthKey: hex.EncodeToString(kd.privateKey.D.Bytes()), 191 | Secret: payload.Secret, 192 | } 193 | 194 | kmsProvider, err := keymanagement.NewAWSKMSProvider(payload.Credential, s.config.Region, aws2.TCP, 0, 0) 195 | if err != nil { 196 | return fmt.Errorf("failed creating KMS provider: %w", err) 197 | } 198 | 199 | ddbProvider, err := keymanagement.NewAWSDDBProvider(payload.Credential, s.config.Region, aws2.TCP, 0, 0) 200 | if err != nil { 201 | return fmt.Errorf("failed creating DDB provider: %w", err) 202 | } 203 | 204 | keyID, err := keymanagement.EncryptAndSaveKey(kmsProvider, ddbProvider, 205 | payload.KeyARN, payload.SecretsTable, plainKey, kd.address) 206 | if err != nil { 207 | return fmt.Errorf("failed encrypting and saving key: %w", err) 208 | } 209 | 210 | return s.sendResponse(conn, keyID, kd.address) 211 | } 212 | 213 | func (s *Server) sendResponse(conn net.Conn, keyID, address string) error { 214 | response := signerTypes.EnclaveResult{ 215 | Status: 200, 216 | Body: signerTypes.Ciphertext{ 217 | KeyID: keyID, 218 | Address: address, 219 | }, 220 | } 221 | 222 | responseData, err := json.Marshal(response) 223 | if err != nil { 224 | return fmt.Errorf("failed serializing response: %w", err) 225 | } 226 | 227 | if _, err := conn.Write(responseData); err != nil { 228 | return fmt.Errorf("failed writing response: %w", err) 229 | } 230 | 231 | return nil 232 | } 233 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/key-generator_pod/functions.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "aws/ethereum-signer/internal/metrics" 11 | "aws/ethereum-signer/internal/pod" 12 | signerTypes "aws/ethereum-signer/internal/types" 13 | "encoding/json" 14 | "fmt" 15 | "github.com/aws/aws-sdk-go-v2/aws" 16 | "github.com/aws/aws-sdk-go-v2/service/sts/types" 17 | "github.com/gin-gonic/gin" 18 | "github.com/go-playground/validator/v10" 19 | "github.com/mdlayher/vsock" 20 | log "github.com/sirupsen/logrus" 21 | "net" 22 | "net/http" 23 | "os" 24 | ) 25 | 26 | type KeyGenerator struct { 27 | config *aws.Config 28 | enclaveCID uint32 29 | enclavePort uint32 30 | validator *validator.Validate 31 | } 32 | 33 | func setupLogging(logLevel string) error { 34 | level, err := log.ParseLevel(logLevel) 35 | if err != nil { 36 | return fmt.Errorf("invalid LOG_LEVEL value (%s): %w", logLevel, err) 37 | } 38 | log.SetLevel(level) 39 | log.Infof("LOG_LEVEL=%s", level) 40 | return nil 41 | } 42 | 43 | func setupMetricsServer(enclavePort uint64) error { 44 | listenerPort := uint32(enclavePort + metrics.PortOffset) 45 | metricsServer := metrics.NewMetricsServer(3, listenerPort) 46 | if err := metricsServer.Start(); err != nil { 47 | return fmt.Errorf("failed to start metrics server on CID: %v, port: %v: %w", 3, listenerPort, err) 48 | } 49 | return nil 50 | } 51 | 52 | func setupRouter(config *aws.Config, enclaveID, enclavePort uint64) *gin.Engine { 53 | router := gin.New() 54 | router.Use(gin.Recovery()) 55 | 56 | keyGen := NewKeyGenerator(config, enclaveID, enclavePort) 57 | router.POST("/", keyGen.GenerateKey) 58 | 59 | return router 60 | } 61 | 62 | func NewKeyGenerator(config *aws.Config, enclaveID, enclavePort uint64) *KeyGenerator { 63 | return &KeyGenerator{ 64 | config: config, 65 | enclaveCID: uint32(enclaveID), 66 | enclavePort: uint32(enclavePort), 67 | validator: validator.New(), 68 | } 69 | } 70 | 71 | type EnvVars struct { 72 | KeyARN string 73 | SecretsTable string 74 | } 75 | 76 | func (kg *KeyGenerator) GenerateKey(c *gin.Context) { 77 | request, err := kg.validateRequest(c) 78 | if err != nil { 79 | kg.handleError(c, http.StatusBadRequest, "invalid request", err) 80 | return 81 | } 82 | 83 | envVars, err := kg.getEnvironmentVariables() 84 | if err != nil { 85 | kg.handleError(c, http.StatusInternalServerError, "environment configuration error", err) 86 | return 87 | } 88 | 89 | result, err := kg.processKeyGeneration(request, envVars) 90 | if err != nil { 91 | kg.handleError(c, http.StatusInternalServerError, "key generation failed", err) 92 | return 93 | } 94 | 95 | c.IndentedJSON(result.Status, result.Body) 96 | } 97 | 98 | func (kg *KeyGenerator) validateRequest(c *gin.Context) (*signerTypes.PlainKey, error) { 99 | var request signerTypes.PlainKey 100 | if err := c.BindJSON(&request); err != nil { 101 | return nil, fmt.Errorf("failed to parse request: %w", err) 102 | } 103 | log.Debugf("incoming request: %v", request) 104 | 105 | if err := kg.validator.Struct(request); err != nil { 106 | validationErrors := err.(validator.ValidationErrors) 107 | log.Warnf("request validation failed: %s", validationErrors) 108 | return nil, validationErrors 109 | } 110 | return &request, nil 111 | } 112 | 113 | func (kg *KeyGenerator) getEnvironmentVariables() (*EnvVars, error) { 114 | keyARN := os.Getenv("KEY_ARN") 115 | secretsTable := os.Getenv("SECRETS_TABLE") 116 | 117 | if keyARN == "" { 118 | return nil, fmt.Errorf("KEY_ARN environment variable cannot be empty") 119 | } 120 | if secretsTable == "" { 121 | return nil, fmt.Errorf("SECRETS_TABLE environment variable cannot be empty") 122 | } 123 | 124 | return &EnvVars{ 125 | KeyARN: keyARN, 126 | SecretsTable: secretsTable, 127 | }, nil 128 | } 129 | 130 | func (kg *KeyGenerator) processKeyGeneration(request *signerTypes.PlainKey, env *EnvVars) (*signerTypes.EnclaveResult, error) { 131 | credentials, err := kg.getAWSCredentials() 132 | if err != nil { 133 | return nil, fmt.Errorf("failed to get AWS credentials: %w", err) 134 | } 135 | 136 | payload, err := kg.createPayload(request, credentials, env) 137 | if err != nil { 138 | return nil, fmt.Errorf("failed to create payload: %w", err) 139 | } 140 | 141 | return kg.communicateWithEnclave(payload) 142 | } 143 | 144 | func (kg *KeyGenerator) getAWSCredentials() (*types.Credentials, error) { 145 | credentials, err := pod.GetAWSWebIdentityCredentials(kg.config, "key-generator_session") 146 | if err != nil { 147 | return nil, fmt.Errorf("failed to get AWS credentials: %w", err) 148 | } 149 | log.Debugf("gathered AWS credentials: %v", credentials) 150 | return credentials, nil 151 | } 152 | 153 | func (kg *KeyGenerator) createPayload(request *signerTypes.PlainKey, creds *types.Credentials, env *EnvVars) ([]byte, error) { 154 | payload := signerTypes.EnclaveKeyGenerationPayload{ 155 | Credential: signerTypes.AWSCredentials{ 156 | AccessKeyID: *creds.AccessKeyId, 157 | SecretAccessKey: *creds.SecretAccessKey, 158 | Token: *creds.SessionToken, 159 | }, 160 | KeyARN: env.KeyARN, 161 | Secret: request.Secret, 162 | SecretsTable: env.SecretsTable, 163 | } 164 | log.Debugf("assembled signing payload: %v", payload) 165 | 166 | serialized, err := json.Marshal(payload) 167 | if err != nil { 168 | return nil, fmt.Errorf("failed to serialize payload: %w", err) 169 | } 170 | log.Debugf("serialized key generation payload: %q", serialized) 171 | return serialized, nil 172 | } 173 | 174 | func (kg *KeyGenerator) communicateWithEnclave(payload []byte) (*signerTypes.EnclaveResult, error) { 175 | conn, err := vsock.Dial(kg.enclaveCID, kg.enclavePort, nil) //#nosec G115 176 | if err != nil { 177 | return nil, fmt.Errorf("failed to connect to enclave: %w", err) 178 | } 179 | defer func() { 180 | if err := conn.Close(); err != nil { 181 | log.Errorf("failed to close connection: %v", err) 182 | } 183 | }() 184 | 185 | if _, err := conn.Write(payload); err != nil { 186 | return nil, fmt.Errorf("failed to write to enclave: %w", err) 187 | } 188 | 189 | response, err := kg.readEnclaveResponse(conn) 190 | if err != nil { 191 | return nil, fmt.Errorf("failed to read enclave response: %w", err) 192 | } 193 | 194 | return response, nil 195 | } 196 | 197 | func (kg *KeyGenerator) readEnclaveResponse(conn net.Conn) (*signerTypes.EnclaveResult, error) { 198 | buf := make([]byte, 512) 199 | n, err := conn.Read(buf) 200 | if err != nil { 201 | return nil, fmt.Errorf("failed to read from connection: %w", err) 202 | } 203 | log.Debugf("raw enclave key generation result: %s", buf[:n]) 204 | 205 | var result signerTypes.EnclaveResult 206 | if err := json.Unmarshal(buf[:n], &result); err != nil { 207 | return nil, fmt.Errorf("failed to parse enclave response: %w", err) 208 | } 209 | log.Debugf("unmarshaled enclave result: %v", result) 210 | 211 | return &result, nil 212 | } 213 | 214 | func (kg *KeyGenerator) handleError(c *gin.Context, status int, message string, err error) { 215 | log.Errorf("%s: %v", message, err) 216 | c.IndentedJSON(status, gin.H{"error": err.Error()}) 217 | } 218 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/key-generator_pod/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "aws/ethereum-signer/internal/pod" 11 | awsConfig "github.com/aws/aws-sdk-go-v2/config" 12 | log "github.com/sirupsen/logrus" 13 | "golang.org/x/net/context" 14 | ) 15 | 16 | var version = "undefined" 17 | 18 | func main() { 19 | log.Infof("starting key generator pod (%s)", version) 20 | 21 | // Load configuration 22 | config, err := pod.LoadConfig() 23 | if err != nil { 24 | log.Fatalf("Failed to load configuration: %v", err) 25 | } 26 | 27 | // Setup logging 28 | if err := setupLogging(config.LogLevel); err != nil { 29 | log.Fatalf("Failed to setup logging: %v", err) 30 | } 31 | 32 | // Load AWS configuration 33 | awsCfg, err := awsConfig.LoadDefaultConfig(context.TODO()) 34 | if err != nil { 35 | log.Fatalf("Failed to load SDK configuration: %v", err) 36 | } 37 | 38 | // Setup router 39 | router := setupRouter(&awsCfg, config.EnclaveCID, config.EnclavePort) 40 | 41 | // Setup metrics server 42 | log.Infof("starting enclave metrics agent") 43 | if err := setupMetricsServer(config.EnclavePort); err != nil { 44 | log.Fatalf("Failed to setup metrics server: %v", err) 45 | } 46 | 47 | // Start TLS server 48 | log.Infof("starting server on %s", config.ListenAddress) 49 | if err := router.RunTLS(config.ListenAddress, config.CertFile, config.KeyFile); err != nil { 50 | log.Fatalf("Failed to start server on %s: %v", config.ListenAddress, err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/key-generator_pod/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set +e 4 | set -x 5 | 6 | EIF_PATH="/usr/src/app/enclave.eif" 7 | ENCLAVE_CPU_COUNT=2 8 | # based on eif file size 9 | ENCLAVE_MEMORY_SIZE=1500 10 | 11 | # increment base port for every single application on pod (vsock_1, vsock_n, metrics) 12 | vsock_port_1=$((VSOCK_BASE_PORT)) 13 | vsock_port_2=$((vsock_port_1 + 1)) 14 | 15 | if [[ "${LOG_LEVEL}" == "DEBUG" ]]; then 16 | debug="--debug-mode" 17 | else 18 | debug= 19 | fi 20 | 21 | generate_tls_artifact() { 22 | local fqdn=${1} 23 | 24 | openssl11 ecparam -name prime256v1 -genkey -noout -out private-key.pem 25 | 26 | # generate associated public key 27 | openssl11 ec -in private-key.pem -pubout -out public-key.pem 28 | 29 | # generate self-signed x509 certificate for EC2 instance 30 | host=$(echo "${fqdn}" | tr "." "\n" | head -n 1) 31 | 32 | # requires openssl > 1.1.1 / is 1.0.2k 33 | openssl11 req -new -x509 -key private-key.pem -out cert.pem -days 360 -subj "/C=US/O=AWS/OU=Blockchain Compute/CN=${host}" --addext "subjectAltName=DNS:${fqdn}" 34 | } 35 | 36 | enclave_image_uri=$(aws ssm get-parameter --region "${AWS_REGION}" --name "${ENCLAVE_IMAGE_URI_SSM}" | jq -r '.Parameter.Value') 37 | enclave=$(nitro-cli describe-enclaves | jq -r '.[0].EnclaveName') 38 | 39 | # have pod panic so that enclave status and pod status are synced and k8s can restart if required 40 | if [[ ${enclave} == "null" ]]; then 41 | aws s3 cp "${enclave_image_uri}" "${EIF_PATH}" 42 | nitro-cli run-enclave --cpu-count "${ENCLAVE_CPU_COUNT}" --memory "${ENCLAVE_MEMORY_SIZE}" --eif-path "${EIF_PATH}" ${debug} 43 | sleep 5 44 | fi 45 | 46 | # second call for pod logs to see if enclave is still up and running 47 | enclave_cid=$(nitro-cli describe-enclaves | jq -r '.[0].EnclaveCID') 48 | 49 | # ensure that proper cid has been assigned 50 | [ "${enclave_cid}" == "null" ] && exit 1 51 | 52 | export ENCLAVE_CID=${enclave_cid} 53 | 54 | VSOCK_PROXY_YAML=./vsock-proxy.yaml 55 | cat <<'EOF' | envsubst >$VSOCK_PROXY_YAML 56 | allowlist: 57 | - {address: kms.${AWS_REGION}.amazonaws.com, port: 443} 58 | - {address: dynamodb.${AWS_REGION}.amazonaws.com, port: 443} 59 | EOF 60 | 61 | # start KMS and DynamoDB outbound proxies in background 62 | # https://github.com/aws/aws-nitro-enclaves-cli/blob/main/vsock_proxy/README.md 63 | vsock-proxy "${vsock_port_1}" kms."${AWS_REGION}".amazonaws.com 443 --config ./vsock-proxy.yaml -w 20 & 64 | vsock-proxy "${vsock_port_2}" dynamodb."${AWS_REGION}".amazonaws.com 443 --config ./vsock-proxy.yaml -w 20 & 65 | 66 | generate_tls_artifact "${FQDN}" 67 | 68 | ./key-generator_pod 69 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/kmstool-cli/README.md: -------------------------------------------------------------------------------- 1 | # kmstool-cli 2 | 3 | `kmstool-cli` is a cli tool written in Go(lang) that supports sending decrypt 4 | requests from inside AWS Nitro Enclaves leveraging cryptographic attestation. 5 | 6 | ## Build 7 | 8 | Change with your terminal into this folder and execute the `go build` command: 9 | 10 | ```shell 11 | cd applications/ethereum-signer/cmd/kmstool-cli 12 | GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-X main.Version=v0.0.1 -X main.BuildTime=$(date -u +'%Y-%m-%d_%H:%M:%S')" -o kmstool-cli 13 | ``` 14 | 15 | ## Usage 16 | 17 | **decrypt request** 18 | 19 | ```shell 20 | ./kmstool-cli decrypt \ 21 | --region us-west-2 \ 22 | --proxy-port 8000 \ 23 | --aws-access-key-id ACCESS_KEY \ 24 | --aws-secret-access-key SECRET_KEY \ 25 | --aws-session-token SESSION_TOKEN \ 26 | --ciphertext BASE64_ENCODED_TEXT \ 27 | --encryption-context key1=value1,key2=value2 \ 28 | --key-id KEY_ID \ 29 | --encryption-algorithm ALGORITHM 30 | ```` 31 | 32 | If `decryption` worked successfully, the base64 encoded plaintext will be printed on `stdoout` without any 33 | leading or trailing characters, thus the based64 encoded string can directly be processed. 34 | 35 | **show version information that has been specified during build time** 36 | 37 | ```shell 38 | ./kmstool-cli --version 39 | ``` 40 | 41 | **use verbose mode with decrypt command** 42 | 43 | ```shell 44 | ./kmstool-cli decrypt --region us-west-2 --ciphertext BASE64_TEXT -v 45 | ``` 46 | 47 | If `-v/--verbose` mode has been selected, all input parameters will be printed to `stdout` on the terminal in additon 48 | to the base64 encoded plaintext. 49 | 50 | **deactivate ephemeral key for decrypt command** 51 | ```shell 52 | ./kmstool-cli decrypt --region us-west-2 --ciphertext BASE64_TEXT -v --ephemeral-key false 53 | ``` 54 | 55 | Key will be generated the first time the cli is run and stored in env var. Subsequent calls will 56 | load the RSA private key from env instead of regenerating it for every run. 57 | 58 | Using the [aws-nitro-enclave-blockchain-wallet](https://github.com/aws-samples/aws-nitro-enclave-blockchain-wallet?tab=readme-ov-file) as 59 | a standard testing harness, setting the `--ephemeral-key` flag to `false`, on average saves avg. `~50/60ms` on the RSA key generation step. 60 | 61 | **get help** 62 | 63 | ```shell 64 | ./kmstool-cli --help 65 | ``` 66 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/kmstool-cli/cmd/cli.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "aws/ethereum-signer/internal/attestation" 5 | aws2 "aws/ethereum-signer/internal/aws" 6 | "aws/ethereum-signer/internal/keymanagement" 7 | signerTypes "aws/ethereum-signer/internal/types" 8 | "encoding/base64" 9 | "fmt" 10 | kmstypes "github.com/aws/aws-sdk-go-v2/service/kms/types" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var ( 15 | Version = "dev" 16 | BuildTime = "unknown" 17 | 18 | // global flags 19 | verbose bool 20 | ) 21 | 22 | type DecryptConfig struct { 23 | region string 24 | proxyPort int 25 | awsAccessKeyID string 26 | awsSecretAccessKey string 27 | awsSessionToken string 28 | ciphertext string 29 | keyID string 30 | encryptionAlgorithm string 31 | encryptionContext map[string]string 32 | ephemeralKeySwitch bool 33 | } 34 | 35 | var decryptCfg DecryptConfig 36 | 37 | func NewRootCmd() *cobra.Command { 38 | var rootCmd = &cobra.Command{ 39 | Use: "kmstool-cli", 40 | Short: "AWS KMS decryption tool written in Go(lang)", 41 | Version: Version, 42 | Run: func(cmd *cobra.Command, args []string) { 43 | err := cmd.Help() 44 | if err != nil { 45 | return 46 | } 47 | }, 48 | } 49 | 50 | rootCmd.SetVersionTemplate(`Version: {{.Version}} 51 | Build Time: ` + BuildTime) 52 | 53 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "enable verbose output") 54 | 55 | var decryptCmd = &cobra.Command{ 56 | Use: "decrypt", 57 | Short: "Decrypt ciphertext using AWS KMS", 58 | RunE: runDecrypt, 59 | PreRunE: func(cmd *cobra.Command, args []string) error { 60 | // validate required flags 61 | if decryptCfg.region == "" { 62 | return fmt.Errorf("--region is required") 63 | } 64 | if decryptCfg.ciphertext == "" { 65 | return fmt.Errorf("--ciphertext is required") 66 | } 67 | if decryptCfg.keyID != "" && decryptCfg.encryptionAlgorithm == "" { 68 | return fmt.Errorf("--encryption-algorithm is required when --key-id is set") 69 | } 70 | 71 | if decryptCfg.encryptionAlgorithm != "" { 72 | _, err := supportedEncryptionAlgorithms(decryptCfg.encryptionAlgorithm) 73 | if err != nil { 74 | return fmt.Errorf("invalid encryption algorithm: %v", err) 75 | } 76 | } 77 | 78 | if decryptCfg.awsSessionToken == "" || decryptCfg.awsAccessKeyID == "" || decryptCfg.awsSecretAccessKey == "" { 79 | return fmt.Errorf("AWS credentials are required, --aws-access-key-id, --aws-secret-access-key, --aws-session-token have to be set") 80 | } 81 | 82 | // validate base64 ciphertext 83 | _, err := base64.StdEncoding.DecodeString(decryptCfg.ciphertext) 84 | if err != nil { 85 | return fmt.Errorf("invalid base64 ciphertext: %v", err) 86 | } 87 | 88 | return nil 89 | }, 90 | } 91 | 92 | // add flags to decrypt command 93 | decryptCmd.Flags().StringVar(&decryptCfg.region, "region", "", "AWS region to use for KMS") 94 | decryptCmd.Flags().IntVar(&decryptCfg.proxyPort, "proxy-port", 8000, "Connect to KMS proxy on PORT") 95 | decryptCmd.Flags().StringVar(&decryptCfg.awsAccessKeyID, "aws-access-key-id", "", "AWS access key ID") 96 | decryptCmd.Flags().StringVar(&decryptCfg.awsSecretAccessKey, "aws-secret-access-key", "", "AWS secret access key") 97 | decryptCmd.Flags().StringVar(&decryptCfg.awsSessionToken, "aws-session-token", "", "Session token associated with the access key ID") 98 | decryptCmd.Flags().StringVar(&decryptCfg.ciphertext, "ciphertext", "", "Base64-encoded ciphertext that need to decrypt") 99 | decryptCmd.Flags().StringVar(&decryptCfg.keyID, "key-id", "", "Decrypt key id (for symmetric keys)") 100 | decryptCmd.Flags().StringVar(&decryptCfg.encryptionAlgorithm, "encryption-algorithm", "SYMMETRIC_DEFAULT", "Encryption algorithm for ciphertext, defaults to SYMMETRIC_DEFAULT (only option for symmetric keys") 101 | decryptCmd.Flags().StringToStringVar(&decryptCfg.encryptionContext, "encryption-context", nil, "Encryption context key-value pairs") 102 | decryptCmd.Flags().BoolVar(&decryptCfg.ephemeralKeySwitch, "ephemeral-key", true, 103 | "If true, the RSA key used in the Recipient field is regenerated with every run. "+ 104 | "If switch is set to false, key will be sourced from RSA_PRIVATE_KEY env variable. "+ 105 | "If variable is empty, key will just be generated once and stored in RSA_PRIVATE_KEY. "+ 106 | "RSA key is used by KMS in the CiphertextForRecipient structure which is a RecipientInfo structure, "+ 107 | "as described in RFC5652 Section 6") 108 | 109 | rootCmd.AddCommand(decryptCmd) 110 | 111 | return rootCmd 112 | } 113 | 114 | func runDecrypt(cmd *cobra.Command, args []string) error { 115 | if verbose { 116 | fmt.Printf("Decrypting with the following configuration:\n") 117 | fmt.Printf("Region: %s\n", decryptCfg.region) 118 | fmt.Printf("Proxy Port: %d\n", decryptCfg.proxyPort) 119 | fmt.Printf("AWS Access Key ID: %s\n", maskString(decryptCfg.awsAccessKeyID)) 120 | fmt.Printf("AWS Secret Access Key: %s\n", maskString(decryptCfg.awsSecretAccessKey)) 121 | fmt.Printf("AWS Session Token: %s\n", maskString(decryptCfg.awsSessionToken)) 122 | fmt.Printf("Ciphertext: %s\n", decryptCfg.ciphertext) 123 | fmt.Printf("Key ID: %s\n", decryptCfg.keyID) 124 | fmt.Printf("Encryption Algorithm: %s\n", decryptCfg.encryptionAlgorithm) 125 | fmt.Printf("Encryption Context: %v\n", decryptCfg.encryptionContext) 126 | fmt.Printf("Ephemeral Key: %v\n", decryptCfg.ephemeralKeySwitch) 127 | } 128 | 129 | // credentials 130 | credentials := signerTypes.AWSCredentials{ 131 | AccessKeyID: decryptCfg.awsAccessKeyID, 132 | SecretAccessKey: decryptCfg.awsSecretAccessKey, 133 | Token: decryptCfg.awsSessionToken, 134 | } 135 | 136 | // advanced decrypt options 137 | encALgo, err := supportedEncryptionAlgorithms(decryptCfg.encryptionAlgorithm) 138 | if err != nil { 139 | return fmt.Errorf("invalid encryption algorithm: %v", err) 140 | } 141 | advDecOpts := keymanagement.AdvancedDecOpts{ 142 | EncryptionAlgorithm: encALgo, 143 | EncryptionContext: decryptCfg.encryptionContext, 144 | KeyId: decryptCfg.keyID, 145 | EphemeralRSAKey: decryptCfg.ephemeralKeySwitch, 146 | } 147 | 148 | kmsProvider, err := keymanagement.NewAWSKMSProvider(credentials, decryptCfg.region, aws2.VSOCK, 3, uint32(decryptCfg.proxyPort)) 149 | if err != nil { 150 | return fmt.Errorf("failed to create kms provider: %v", err) 151 | } 152 | plaintextB64, err := keymanagement.DecryptCiphertextWithAttestation(decryptCfg.ciphertext, &advDecOpts, &attestation.NitroAttestationProvider{}, kmsProvider) 153 | if err != nil { 154 | return fmt.Errorf("failed to decrypt ciphertext: %v", err) 155 | } 156 | 157 | fmt.Printf("%v", plaintextB64) 158 | 159 | return nil 160 | } 161 | 162 | // supported encryption algorithms 163 | func supportedEncryptionAlgorithms(encryptionAlgorithm string) (kmstypes.EncryptionAlgorithmSpec, error) { 164 | switch encryptionAlgorithm { 165 | case "SYMMETRIC_DEFAULT": 166 | return kmstypes.EncryptionAlgorithmSpecSymmetricDefault, nil 167 | case "RSAES_OAEP_SHA_1": 168 | return kmstypes.EncryptionAlgorithmSpecRsaesOaepSha1, nil 169 | case "RSAES_OAEP_SHA_256": 170 | return kmstypes.EncryptionAlgorithmSpecRsaesOaepSha256, nil 171 | default: 172 | return "", fmt.Errorf("only SYMMETRIC_DEFAULT, RSAES_OAEP_SHA_1 and RSAES_OAEP_SHA_256 are supported. Please see https://github.com/aws/aws-nitro-enclaves-sdk-c/blob/main/docs/kms-apis/Decrypt.md#encryptionalgorithm for more information: %s", encryptionAlgorithm) 173 | } 174 | } 175 | 176 | // helper function to mask sensitive information 177 | func maskString(s string) string { 178 | if s == "" { 179 | return "" 180 | } 181 | if len(s) <= 4 { 182 | return "****" 183 | } 184 | return s[:4] + "****" 185 | } 186 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/kmstool-cli/cmd/cli_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | ) 11 | 12 | var update = flag.Bool("update", false, "update golden files") 13 | 14 | func TestDecryptCommand(t *testing.T) { 15 | tests := []struct { 16 | name string 17 | args []string 18 | env map[string]string 19 | input string 20 | golden string 21 | wantErr bool 22 | }{ 23 | { 24 | // golden path 25 | // todo extend mocking to cli - just ensuring correct parameter combination right now 26 | name: "basic_decrypt", 27 | args: []string{ 28 | "decrypt", 29 | "--region", "us-west-2", 30 | "--ciphertext", "SGVsbG8gV29ybGQ=", // base64 encoded 31 | "--proxy-port", "8000", 32 | "--aws-access-key-id", "AKIAIOSFODNN7EXAMPLE", 33 | "--aws-secret-access-key", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 34 | "--aws-session-token", "SESSION_TOKEN", 35 | }, 36 | env: map[string]string{ 37 | "RSA_PRIVATE_KEY": "test-key", 38 | }, 39 | wantErr: true, 40 | golden: "testdata/basic_decrypt_missing_nsm.golden", 41 | }, 42 | { 43 | name: "missing_region", 44 | args: []string{ 45 | "decrypt", 46 | "--key-id", "test-key-id", 47 | "--ciphertext", "SGVsbG8gV29ybGQ=", 48 | }, 49 | wantErr: true, 50 | golden: "testdata/missing_region.golden", 51 | }, 52 | { 53 | name: "invalid_encryption_algorithm", 54 | args: []string{ 55 | "decrypt", 56 | "--region", "us-west-2", 57 | "--proxy-port", "8000", 58 | "--aws-access-key-id", "AKIAIOSFODNN7EXAMPLE", 59 | "--aws-secret-access-key", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", 60 | "--aws-session-token", "SESSION_TOKEN", 61 | "--ciphertext", "SGVsbG8gV29ybGQ=", // base64 encoded 62 | "--ephemeral-key", "false", 63 | }, 64 | wantErr: true, 65 | golden: "testdata/basic_decrypt_ephemeral-key.golden", 66 | }, 67 | } 68 | 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | // 72 | rootCmd := NewRootCmd() 73 | 74 | // Setup environment 75 | for k, v := range tt.env { 76 | os.Setenv(k, v) 77 | defer os.Unsetenv(k) 78 | } 79 | 80 | // Capture stdout and err out 81 | oldStdout := os.Stdout 82 | oldStderr := os.Stderr 83 | 84 | rOut, wOut, _ := os.Pipe() 85 | rErr, wErr, _ := os.Pipe() 86 | 87 | os.Stdout = wOut 88 | os.Stderr = wErr 89 | 90 | // Execute command 91 | rootCmd.SetArgs(tt.args) 92 | err := rootCmd.Execute() 93 | 94 | // Restore stdout 95 | wOut.Close() 96 | wErr.Close() 97 | os.Stdout = oldStdout 98 | os.Stderr = oldStderr 99 | 100 | // Read captured output 101 | var stdoutBuf, stderrBuf bytes.Buffer 102 | io.Copy(&stdoutBuf, rOut) 103 | io.Copy(&stderrBuf, rErr) 104 | output := stdoutBuf.String() 105 | errorOutput := stderrBuf.String() 106 | 107 | // Combine output if needed 108 | combinedOutput := output 109 | if errorOutput != "" { 110 | combinedOutput = output + errorOutput 111 | } 112 | 113 | // Handle error cases 114 | if (err != nil) != tt.wantErr { 115 | t.Errorf("Execute() error = %v, wantErr %v", err, tt.wantErr) 116 | return 117 | } 118 | 119 | // Update golden files if flag is set 120 | if *update { 121 | if err := os.MkdirAll(filepath.Dir(tt.golden), 0755); err != nil { 122 | t.Fatal(err) 123 | } 124 | if err := os.WriteFile(tt.golden, []byte(combinedOutput), 0644); err != nil { 125 | t.Fatal(err) 126 | } 127 | } 128 | 129 | // Compare with golden file 130 | expected, err := os.ReadFile(tt.golden) 131 | if err != nil { 132 | t.Fatal(err) 133 | } 134 | 135 | if combinedOutput != string(expected) { 136 | t.Errorf("output differs from golden file:\ngot:\n%s\nwant:\n%s", 137 | combinedOutput, string(expected)) 138 | } 139 | }) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/kmstool-cli/cmd/testdata/basic_decrypt_ephemeral-key.golden: -------------------------------------------------------------------------------- 1 | Error: failed to decrypt ciphertext: open /dev/nsm: no such file or directory 2 | Usage: 3 | kmstool-cli decrypt [flags] 4 | 5 | Flags: 6 | --aws-access-key-id string AWS access key ID 7 | --aws-secret-access-key string AWS secret access key 8 | --aws-session-token string Session token associated with the access key ID 9 | --ciphertext string Base64-encoded ciphertext that need to decrypt 10 | --encryption-algorithm string Encryption algorithm for ciphertext, defaults to SYMMETRIC_DEFAULT (only option for symmetric keys (default "SYMMETRIC_DEFAULT") 11 | --encryption-context stringToString Encryption context key-value pairs (default []) 12 | --ephemeral-key If true, the RSA key used in the Recipient field is regenerated with every run. If switch is set to false, key will be sourced from RSA_PRIVATE_KEY env variable. If variable is empty, key will just be generated once and stored in RSA_PRIVATE_KEY. RSA key is used by KMS in the CiphertextForRecipient structure which is a RecipientInfo structure, as described in RFC5652 Section 6 (default true) 13 | -h, --help help for decrypt 14 | --key-id string Decrypt key id (for symmetric keys) 15 | --proxy-port int Connect to KMS proxy on PORT (default 8000) 16 | --region string AWS region to use for KMS 17 | 18 | Global Flags: 19 | -v, --verbose enable verbose output 20 | 21 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/kmstool-cli/cmd/testdata/basic_decrypt_missing_nsm.golden: -------------------------------------------------------------------------------- 1 | Error: failed to decrypt ciphertext: open /dev/nsm: no such file or directory 2 | Usage: 3 | kmstool-cli decrypt [flags] 4 | 5 | Flags: 6 | --aws-access-key-id string AWS access key ID 7 | --aws-secret-access-key string AWS secret access key 8 | --aws-session-token string Session token associated with the access key ID 9 | --ciphertext string Base64-encoded ciphertext that need to decrypt 10 | --encryption-algorithm string Encryption algorithm for ciphertext, defaults to SYMMETRIC_DEFAULT (only option for symmetric keys (default "SYMMETRIC_DEFAULT") 11 | --encryption-context stringToString Encryption context key-value pairs (default []) 12 | --ephemeral-key If true, the RSA key used in the Recipient field is regenerated with every run. If switch is set to false, key will be sourced from RSA_PRIVATE_KEY env variable. If variable is empty, key will just be generated once and stored in RSA_PRIVATE_KEY. RSA key is used by KMS in the CiphertextForRecipient structure which is a RecipientInfo structure, as described in RFC5652 Section 6 (default true) 13 | -h, --help help for decrypt 14 | --key-id string Decrypt key id (for symmetric keys) 15 | --proxy-port int Connect to KMS proxy on PORT (default 8000) 16 | --region string AWS region to use for KMS 17 | 18 | Global Flags: 19 | -v, --verbose enable verbose output 20 | 21 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/kmstool-cli/cmd/testdata/missing_region.golden: -------------------------------------------------------------------------------- 1 | Error: --region is required 2 | Usage: 3 | kmstool-cli decrypt [flags] 4 | 5 | Flags: 6 | --aws-access-key-id string AWS access key ID 7 | --aws-secret-access-key string AWS secret access key 8 | --aws-session-token string Session token associated with the access key ID 9 | --ciphertext string Base64-encoded ciphertext that need to decrypt 10 | --encryption-algorithm string Encryption algorithm for ciphertext, defaults to SYMMETRIC_DEFAULT (only option for symmetric keys (default "SYMMETRIC_DEFAULT") 11 | --encryption-context stringToString Encryption context key-value pairs (default []) 12 | --ephemeral-key If true, the RSA key used in the Recipient field is regenerated with every run. If switch is set to false, key will be sourced from RSA_PRIVATE_KEY env variable. If variable is empty, key will just be generated once and stored in RSA_PRIVATE_KEY. RSA key is used by KMS in the CiphertextForRecipient structure which is a RecipientInfo structure, as described in RFC5652 Section 6 (default true) 13 | -h, --help help for decrypt 14 | --key-id string Decrypt key id (for symmetric keys) 15 | --proxy-port int Connect to KMS proxy on PORT (default 8000) 16 | --region string AWS region to use for KMS 17 | 18 | Global Flags: 19 | -v, --verbose enable verbose output 20 | 21 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/kmstool-cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "aws/ethereum-signer/cmd/kmstool-cli/cmd" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | if err := cmd.NewRootCmd().Execute(); err != nil { 10 | os.Exit(1) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/lambda/handler.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | signerHMAC "aws/ethereum-signer/internal/hmac" 11 | signerTypes "aws/ethereum-signer/internal/types" 12 | "bytes" 13 | "crypto/tls" 14 | "encoding/json" 15 | "fmt" 16 | "github.com/go-playground/validator/v10" 17 | "github.com/google/uuid" 18 | log "github.com/sirupsen/logrus" 19 | "io" 20 | "net/http" 21 | "time" 22 | ) 23 | 24 | var validate *validator.Validate 25 | 26 | func handleSigningRequest(nitroInstancePrivateDNS string, userRequestPayload signerTypes.UserSigningRequest) (signerTypes.UserResponse, error) { 27 | 28 | timestamp := int(time.Now().Unix()) 29 | 30 | transactionPayloadSerialized, err := json.Marshal(userRequestPayload.TransactionPayload) 31 | if err != nil { 32 | return signerTypes.UserResponse{}, err 33 | } 34 | 35 | // user password (secret) -> sha256 -> + salt -> rehash 36 | // user passes tx payload along with sha256 hash of secret - deterministic private key 37 | hmacHex := signerHMAC.CalculateHMAC(transactionPayloadSerialized, userRequestPayload.Secret, timestamp) 38 | if err != nil { 39 | return signerTypes.UserResponse{}, fmt.Errorf("exception happened calculating HMAC: %s", err) 40 | } 41 | log.Debugf("calculated HMAC: %s", hmacHex) 42 | 43 | transactionSigningRequest := signerTypes.SigningRequest{ 44 | TransactionPayload: userRequestPayload.TransactionPayload, 45 | KeyID: userRequestPayload.KeyID, 46 | Timestamp: timestamp, 47 | HMAC: hmacHex, 48 | } 49 | 50 | transactionSigningRequestSerialized, err := json.Marshal(transactionSigningRequest) 51 | if err != nil { 52 | return signerTypes.UserResponse{}, err 53 | } 54 | 55 | hostName := fmt.Sprintf("%s.%s", "ethereum-signer", nitroInstancePrivateDNS) 56 | enclaveResponseRaw, statusCode, err := handleEnclaveRequest(hostName, transactionSigningRequestSerialized) 57 | if err != nil { 58 | return signerTypes.UserResponse{}, nil 59 | } 60 | 61 | enclaveResponse := signerTypes.SignedTransaction{} 62 | err = json.Unmarshal(enclaveResponseRaw, &enclaveResponse) 63 | if err != nil { 64 | return signerTypes.UserResponse{}, err 65 | } 66 | 67 | userResponse := signerTypes.UserResponse{ 68 | EnclaveStatus: statusCode, 69 | EnclaveResult: enclaveResponse, 70 | } 71 | return userResponse, nil 72 | } 73 | 74 | func handleGenerateKeyRequest(nitroInstancePrivateDNS string, userRequestPayload signerTypes.PlainKey) (signerTypes.UserResponse, error) { 75 | 76 | userRequestPayloadSerialized, err := json.Marshal(userRequestPayload) 77 | if err != nil { 78 | return signerTypes.UserResponse{}, err 79 | } 80 | hostName := fmt.Sprintf("%s.%s", "ethereum-key-generator", nitroInstancePrivateDNS) 81 | enclaveRespnseRaw, statusCode, err := handleEnclaveRequest(hostName, userRequestPayloadSerialized) 82 | if err != nil { 83 | return signerTypes.UserResponse{}, err 84 | } 85 | 86 | enclaveResponse := signerTypes.Ciphertext{} 87 | err = json.Unmarshal(enclaveRespnseRaw, &enclaveResponse) 88 | if err != nil { 89 | return signerTypes.UserResponse{}, err 90 | } 91 | 92 | userResponse := signerTypes.UserResponse{ 93 | EnclaveStatus: statusCode, 94 | EnclaveResult: enclaveResponse, 95 | } 96 | return userResponse, nil 97 | } 98 | 99 | func handleEnclaveRequest(hostName string, enclavePayload []byte) ([]byte, int, error) { 100 | 101 | tlsTransport := &http.Transport{ 102 | TLSClientConfig: &tls.Config{ 103 | MinVersion: tls.VersionTLS13, 104 | InsecureSkipVerify: true, // #nosec G402 105 | // todo to enable TLS certificate validation even with self-signed cert pass custom x509 cert 106 | //RootCAs: 107 | }, 108 | } 109 | 110 | tlsClient := &http.Client{Transport: tlsTransport} 111 | 112 | res, err := tlsClient.Post(fmt.Sprintf("https://%s:%d", hostName, 8080), "application/json", bytes.NewBuffer(enclavePayload)) 113 | if err != nil { 114 | return nil, 0, err 115 | } 116 | defer func() { 117 | err := res.Body.Close() 118 | if err != nil { 119 | log.Errorf("error happened closing response body: %s", err) 120 | } 121 | }() 122 | 123 | enclaveResponseRaw, err := io.ReadAll(res.Body) 124 | if err != nil { 125 | return nil, 0, err 126 | } 127 | 128 | return enclaveResponseRaw, res.StatusCode, nil 129 | } 130 | 131 | func isValidUUID(u string) bool { 132 | _, err := uuid.Parse(u) 133 | return err == nil 134 | } 135 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/lambda/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | signerTypes "aws/ethereum-signer/internal/types" 11 | "context" 12 | "encoding/json" 13 | "github.com/aws/aws-lambda-go/events" 14 | "github.com/aws/aws-lambda-go/lambda" 15 | "github.com/go-playground/validator/v10" 16 | log "github.com/sirupsen/logrus" 17 | "net/url" 18 | "os" 19 | ) 20 | 21 | var version = "undefined" 22 | 23 | func HandleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 24 | requestJSON, err := json.Marshal(request) 25 | if err != nil { 26 | return events.APIGatewayProxyResponse{Body: err.Error(), StatusCode: 500}, nil 27 | } 28 | log.Debugf("starting ethereum key handler lambda (%s)", version) 29 | log.Debugf("event:\n%s", requestJSON) 30 | 31 | operation := "" 32 | 33 | // todo finish refactoring - get rid of double switch 34 | switch { 35 | case request.HTTPMethod == "POST" && request.Path == "/key": 36 | operation = "ethKeyGenerator" 37 | 38 | // change how handler is called -> pass key_id as param 39 | case request.HTTPMethod == "POST" && request.Resource == "/key/{key_id}/tx_signature": 40 | operation = "ethTxSignature" 41 | 42 | case request.HTTPMethod == "POST" && request.Resource == "/key/{key_id}/userop_signature": 43 | operation = "ethUserOpSignature" 44 | } 45 | 46 | // environment variable required for generate key, sign tx and sign user op 47 | nitroInstancePrivateDNS := os.Getenv("NITRO_INSTANCE_PRIVATE_DNS") 48 | validate = validator.New() 49 | 50 | switch operation { 51 | case "ethKeyGenerator": 52 | var keyGenerationRequest signerTypes.PlainKey 53 | err = json.Unmarshal([]byte(request.Body), &keyGenerationRequest) 54 | if err != nil { 55 | return events.APIGatewayProxyResponse{Body: err.Error(), StatusCode: 500}, nil 56 | } 57 | 58 | keyGenerationRequestJSON, _ := json.Marshal(keyGenerationRequest) 59 | log.Debugf("key generation request: %s", keyGenerationRequestJSON) 60 | 61 | // trigger struct validation to ensure that the external key conforms with the required length 62 | err = validate.Struct(keyGenerationRequest) 63 | if err != nil { 64 | validationErrors := err.(validator.ValidationErrors) 65 | return events.APIGatewayProxyResponse{Body: validationErrors.Error(), StatusCode: 400}, nil 66 | } 67 | 68 | enclaveResult, err := handleGenerateKeyRequest(nitroInstancePrivateDNS, keyGenerationRequest) 69 | if err != nil { 70 | return events.APIGatewayProxyResponse{Body: err.Error(), StatusCode: 500}, nil 71 | } 72 | 73 | enclaveResultJSON, err := json.Marshal(enclaveResult) 74 | if err != nil { 75 | return events.APIGatewayProxyResponse{Body: err.Error(), StatusCode: 500}, nil 76 | } 77 | 78 | return events.APIGatewayProxyResponse{Body: string(enclaveResultJSON), StatusCode: 200}, nil 79 | 80 | // enclave determines tx or user op based on passed parameters so no need to handle requests differently 81 | case "ethTxSignature", "ethUserOpSignature": 82 | 83 | keyIDParam, found := request.PathParameters["key_id"] 84 | if found { 85 | keyIDParam, err = url.QueryUnescape(keyIDParam) 86 | if err != nil { 87 | return events.APIGatewayProxyResponse{Body: err.Error(), StatusCode: 400}, nil 88 | } 89 | if !isValidUUID(keyIDParam) { 90 | return events.APIGatewayProxyResponse{Body: "invalid keyID submitted", StatusCode: 400}, nil 91 | } 92 | } 93 | 94 | var signingRequestPayload signerTypes.UserSigningRequest 95 | 96 | err = json.Unmarshal([]byte(request.Body), &signingRequestPayload) 97 | if err != nil { 98 | return events.APIGatewayProxyResponse{Body: err.Error(), StatusCode: 400}, nil 99 | } 100 | 101 | err := validate.Struct(signingRequestPayload) 102 | if err != nil { 103 | validationErrors := err.(validator.ValidationErrors) 104 | return events.APIGatewayProxyResponse{Body: validationErrors.Error(), StatusCode: 400}, nil 105 | } 106 | 107 | signedTX, err := handleSigningRequest(nitroInstancePrivateDNS, signingRequestPayload) 108 | if err != nil { 109 | return events.APIGatewayProxyResponse{Body: err.Error(), StatusCode: 500}, nil 110 | } 111 | 112 | //return signedTX, nil 113 | signedTXJSON, err := json.Marshal(signedTX) 114 | if err != nil { 115 | return events.APIGatewayProxyResponse{Body: err.Error(), StatusCode: 500}, nil 116 | } 117 | 118 | return events.APIGatewayProxyResponse{Body: string(signedTXJSON), StatusCode: 200}, nil 119 | 120 | default: 121 | return events.APIGatewayProxyResponse{Body: "operation not supported", StatusCode: 400}, nil 122 | 123 | } 124 | } 125 | 126 | func main() { 127 | 128 | logLevel, err := log.ParseLevel(os.Getenv("LOG_LEVEL")) 129 | if err != nil { 130 | log.Fatalf("LOG_LEVEL value (%s) could not be parsed: %s", os.Getenv("LOG_LEVEL"), err) 131 | } 132 | 133 | log.SetLevel(logLevel) 134 | 135 | nitroInstancePrivateDNS := os.Getenv("NITRO_INSTANCE_PRIVATE_DNS") 136 | secretTable := os.Getenv("SECRETS_TABLE") 137 | keyARN := os.Getenv("KEY_ARN") 138 | 139 | if nitroInstancePrivateDNS == "" || secretTable == "" || keyARN == "" { 140 | panic("NITRO_INSTANCE_PRIVATE_DNS, SECRETS_TABLE or KEY_ARN cannot be empty") 141 | } 142 | 143 | lambda.Start(HandleRequest) 144 | } 145 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/signing_enclave/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "aws/ethereum-signer/internal/enclave" 11 | "github.com/go-playground/validator/v10" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | var version = "undefined" 16 | var validate = validator.New() 17 | 18 | func main() { 19 | config, err := enclave.LoadConfig() 20 | if err != nil { 21 | log.Fatalf("failed loading config: %v", err) 22 | } 23 | 24 | server := NewSigningServer(config) 25 | if err := server.Initialize(); err != nil { 26 | log.Fatalf("server initialization failed: %v", err) 27 | } 28 | 29 | server.Run() 30 | } 31 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/signing_enclave/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # SPDX-License-Identifier: MIT-0 4 | 5 | set -e 6 | set -x 7 | 8 | # tcp and kernel tuning would go here 9 | #sysctl -w net.core.rmem_max=16777216 # 4MB 10 | #sysctl -w net.core.wmem_max=16777216 # 4MB 11 | #sysctl -w net.ipv4.tcp_max_syn_backlog=65536 12 | #sysctl -w net.core.somaxconn=65535 13 | #sysctl -w net.core.netdev_max_backlog=65536 14 | 15 | /app/signing_enclave 16 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/signing_pod/functions.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "aws/ethereum-signer/internal/metrics" 11 | "aws/ethereum-signer/internal/pod" 12 | signerTypes "aws/ethereum-signer/internal/types" 13 | "context" 14 | "encoding/json" 15 | "fmt" 16 | "github.com/aws/aws-sdk-go-v2/aws" 17 | "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" 18 | "github.com/aws/aws-sdk-go-v2/service/dynamodb" 19 | dbTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" 20 | "github.com/aws/aws-sdk-go-v2/service/sts/types" 21 | "github.com/gin-gonic/gin" 22 | "github.com/go-playground/validator/v10" 23 | "github.com/mdlayher/vsock" 24 | log "github.com/sirupsen/logrus" 25 | "net" 26 | "net/http" 27 | "os" 28 | ) 29 | 30 | type TxSigner struct { 31 | config *aws.Config 32 | enclaveCID uint32 33 | enclavePort uint32 34 | validator *validator.Validate 35 | } 36 | 37 | func NewSignerInstance(config *aws.Config, enclaveCID, enclavePort uint64) *TxSigner { 38 | return &TxSigner{ 39 | config: config, 40 | enclaveCID: uint32(enclaveCID), 41 | enclavePort: uint32(enclavePort), 42 | validator: validator.New(), 43 | } 44 | } 45 | 46 | func setupLogging(logLevel string) error { 47 | level, err := log.ParseLevel(logLevel) 48 | if err != nil { 49 | return fmt.Errorf("invalid LOG_LEVEL value (%s): %w", logLevel, err) 50 | } 51 | log.SetLevel(level) 52 | log.Infof("LOG_LEVEL=%s", level) 53 | return nil 54 | } 55 | 56 | func setupMetricsServer(enclavePort uint64) error { 57 | listenerPort := uint32(enclavePort + metrics.PortOffset) 58 | metricsServer := metrics.NewMetricsServer(3, listenerPort) 59 | if err := metricsServer.Start(); err != nil { 60 | return fmt.Errorf("failed to start metrics server on CID: %v, port: %v: %w", 3, listenerPort, err) 61 | } 62 | return nil 63 | } 64 | 65 | func setupRouter(config *aws.Config, enclaveID, enclavePort uint64) *gin.Engine { 66 | router := gin.New() 67 | router.Use(gin.Recovery()) 68 | signer := NewSignerInstance(config, enclaveID, enclavePort) 69 | router.POST("/", signer.SignTransaction) 70 | 71 | return router 72 | } 73 | 74 | type EnvVars struct { 75 | SecretsTable string 76 | } 77 | 78 | func (txs *TxSigner) getEnvironmentVariables() (*EnvVars, error) { 79 | secretsTable := os.Getenv("SECRETS_TABLE") 80 | 81 | if secretsTable == "" { 82 | return nil, fmt.Errorf("SECRETS_TABLE environment variable cannot be empty") 83 | } 84 | 85 | return &EnvVars{ 86 | SecretsTable: secretsTable, 87 | }, nil 88 | } 89 | 90 | func (txs *TxSigner) getEncryptedKey(keyID string) (signerTypes.Ciphertext, error) { 91 | 92 | dynamoDBClient := dynamodb.NewFromConfig(*txs.config) 93 | 94 | keyIDValue, err := attributevalue.Marshal(keyID) 95 | if err != nil { 96 | return signerTypes.Ciphertext{}, fmt.Errorf("exception happened marshalling keyID into DynamoDB compatible query attribute:%s", err) 97 | } 98 | 99 | envVars, err := txs.getEnvironmentVariables() 100 | if err != nil { 101 | return signerTypes.Ciphertext{}, fmt.Errorf("exception happened getting environment variables:%s", err) 102 | } 103 | 104 | result, err := dynamoDBClient.GetItem(context.TODO(), &dynamodb.GetItemInput{ 105 | TableName: &envVars.SecretsTable, 106 | Key: map[string]dbTypes.AttributeValue{"key_id": keyIDValue}, 107 | }, 108 | ) 109 | if err != nil { 110 | return signerTypes.Ciphertext{}, fmt.Errorf("exception happened sending GetItem request to DynamoDB:%s", err) 111 | } 112 | 113 | if len(result.Item) == 0 { 114 | return signerTypes.Ciphertext{}, &signerTypes.SecretNotFoundError{ 115 | Err: fmt.Errorf("KeyID (%s) was not found", keyID), 116 | } 117 | } 118 | 119 | var encryptedKey signerTypes.Ciphertext 120 | err = attributevalue.UnmarshalMap(result.Item, &encryptedKey) 121 | if err != nil { 122 | return signerTypes.Ciphertext{}, fmt.Errorf("exception happened unmarshalling DynamoDB result into type singerTypes.Ciphertext:%s", err) 123 | } 124 | 125 | return encryptedKey, nil 126 | } 127 | 128 | func (txs *TxSigner) communicateWithEnclave(payload []byte) (*signerTypes.EnclaveResult, error) { 129 | conn, err := vsock.Dial(txs.enclaveCID, txs.enclavePort, nil) //#nosec G115 130 | if err != nil { 131 | return nil, fmt.Errorf("failed to connect to enclave: %w", err) 132 | } 133 | defer func() { 134 | if err := conn.Close(); err != nil { 135 | log.Errorf("failed to close connection: %v", err) 136 | } 137 | }() 138 | 139 | if _, err := conn.Write(payload); err != nil { 140 | return nil, fmt.Errorf("failed to write to enclave: %w", err) 141 | } 142 | 143 | response, err := txs.readEnclaveResponse(conn) 144 | if err != nil { 145 | return nil, fmt.Errorf("failed to read enclave response: %w", err) 146 | } 147 | 148 | return response, nil 149 | } 150 | 151 | func (txs *TxSigner) readEnclaveResponse(conn net.Conn) (*signerTypes.EnclaveResult, error) { 152 | buf := make([]byte, 512) 153 | n, err := conn.Read(buf) 154 | if err != nil { 155 | return nil, fmt.Errorf("failed to read from connection: %w", err) 156 | } 157 | log.Debugf("raw enclave key generation result: %s", buf[:n]) 158 | 159 | var result signerTypes.EnclaveResult 160 | if err := json.Unmarshal(buf[:n], &result); err != nil { 161 | return nil, fmt.Errorf("failed to parse enclave response: %w", err) 162 | } 163 | log.Debugf("unmarshaled enclave result: %v", result) 164 | 165 | return &result, nil 166 | } 167 | 168 | func (txs *TxSigner) handleError(c *gin.Context, status int, message string, err error) { 169 | log.Errorf("%s: %v", message, err) 170 | c.IndentedJSON(status, gin.H{"error": err.Error()}) 171 | } 172 | 173 | func (txs *TxSigner) generatePayload(signingRequest signerTypes.SigningRequest, encryptedKey signerTypes.Ciphertext, creds *types.Credentials) ([]byte, error) { 174 | 175 | // assemble enclave payload 176 | payload := signerTypes.EnclaveSigningPayload{ 177 | Credential: signerTypes.AWSCredentials{ 178 | AccessKeyID: *creds.AccessKeyId, 179 | SecretAccessKey: *creds.SecretAccessKey, 180 | Token: *creds.SessionToken, 181 | }, 182 | TransactionPayload: signingRequest.TransactionPayload, 183 | EncryptedKey: encryptedKey.Ciphertext, 184 | Timestamp: signingRequest.Timestamp, 185 | HMAC: signingRequest.HMAC, 186 | } 187 | log.Debugf("assembled signing payload: %v", payload) 188 | 189 | serialized, err := json.Marshal(payload) 190 | if err != nil { 191 | return nil, fmt.Errorf("failed to serialize payload: %w", err) 192 | } 193 | log.Debugf("serialized key generation payload: %q", serialized) 194 | return serialized, nil 195 | 196 | } 197 | 198 | func (txs *TxSigner) SignTransaction(c *gin.Context) { 199 | var newSigningRequest signerTypes.SigningRequest 200 | 201 | if err := c.BindJSON(&newSigningRequest); err != nil { 202 | return 203 | } 204 | log.Debugf("incomming request: %v", newSigningRequest) 205 | 206 | //validate = validator.New() 207 | err := txs.validator.Struct(newSigningRequest) 208 | if err != nil { 209 | validationErrors := err.(validator.ValidationErrors) 210 | txs.handleError(c, http.StatusBadRequest, "incoming request could not be verified", validationErrors) 211 | return 212 | } 213 | 214 | stsCredentials, err := pod.GetAWSWebIdentityCredentials(txs.config, "ethereum-signer_session") 215 | if err != nil { 216 | txs.handleError(c, http.StatusInternalServerError, "exception happened gathering sts pod", err) 217 | return 218 | } 219 | log.Debugf("gathered sts pod: %v", stsCredentials) 220 | 221 | encryptedKey, err := txs.getEncryptedKey(newSigningRequest.KeyID) 222 | if err != nil { 223 | re, ok := err.(*signerTypes.SecretNotFoundError) 224 | if ok { 225 | txs.handleError(c, http.StatusNotFound, "Requested secret could not be found in DynamoDB", re.Err) 226 | return 227 | } 228 | txs.handleError(c, http.StatusInternalServerError, "exception happened downloading encrypted key from DynamoDB", err) 229 | return 230 | } 231 | log.Debugf("encrypted key: %v", encryptedKey) 232 | 233 | payload, err := txs.generatePayload(newSigningRequest, encryptedKey, stsCredentials) 234 | if err != nil { 235 | txs.handleError(c, http.StatusInternalServerError, "exception happened generating payload", err) 236 | return 237 | } 238 | 239 | result, err := txs.communicateWithEnclave(payload) 240 | if err != nil { 241 | txs.handleError(c, http.StatusInternalServerError, "exception happened communicating with enclave", err) 242 | return 243 | } 244 | c.IndentedJSON(result.Status, result.Body) 245 | } 246 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/signing_pod/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | 7 | package main 8 | 9 | import ( 10 | "aws/ethereum-signer/internal/pod" 11 | "context" 12 | awsConfig "github.com/aws/aws-sdk-go-v2/config" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // todo add build time and version tag 17 | var version = "undefined" 18 | 19 | func main() { 20 | log.Infof("starting signing pod (%s)", version) 21 | 22 | // Load configuration 23 | config, err := pod.LoadConfig() 24 | if err != nil { 25 | log.Fatalf("Failed to load configuration: %v", err) 26 | } 27 | 28 | // Setup logging 29 | if err := setupLogging(config.LogLevel); err != nil { 30 | log.Fatalf("Failed to setup logging: %v", err) 31 | } 32 | 33 | // Load AWS configuration 34 | awsCfg, err := awsConfig.LoadDefaultConfig(context.TODO()) 35 | if err != nil { 36 | log.Fatalf("Failed to load SDK configuration: %v", err) 37 | } 38 | 39 | router := setupRouter(&awsCfg, config.EnclaveCID, config.EnclavePort) 40 | 41 | // Setup metrics server 42 | log.Infof("starting enclave metrics agent") 43 | if err := setupMetricsServer(config.EnclavePort); err != nil { 44 | log.Fatalf("Failed to setup metrics server: %v", err) 45 | } 46 | 47 | // Start TLS server 48 | log.Infof("starting server on %s", config.ListenAddress) 49 | if err := router.RunTLS(config.ListenAddress, config.CertFile, config.KeyFile); err != nil { 50 | log.Fatalf("Failed to start server on %s: %v", config.ListenAddress, err) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /applications/ethereum-signer/cmd/signing_pod/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set +e 4 | set -x 5 | 6 | EIF_PATH="/usr/src/app/enclave.eif" 7 | ENCLAVE_CPU_COUNT=2 8 | # based on eif file size 9 | ENCLAVE_MEMORY_SIZE=1500 10 | 11 | if [[ "${LOG_LEVEL}" == "DEBUG" ]]; then 12 | debug="--debug-mode" 13 | else 14 | debug= 15 | fi 16 | 17 | # todo old arithmetic 18 | vsock_port_1=$((VSOCK_BASE_PORT)) 19 | 20 | generate_tls_artifact() { 21 | local fqdn=${1} 22 | 23 | openssl11 ecparam -name prime256v1 -genkey -noout -out private-key.pem 24 | 25 | # generate associated public key 26 | openssl11 ec -in private-key.pem -pubout -out public-key.pem 27 | 28 | # generate self-signed x509 certificate for EC2 instance 29 | host=$(echo "${fqdn}" | tr "." "\n" | head -n 1) 30 | 31 | # requires openssl > 1.1.1 / is 1.0.2k 32 | openssl11 req -new -x509 -key private-key.pem -out cert.pem -days 360 -subj "/C=US/O=AWS/OU=Blockchain Compute/CN=${host}" --addext "subjectAltName=DNS:${fqdn}" 33 | } 34 | 35 | enclave_image_uri=$(aws ssm get-parameter --region "${AWS_REGION}" --name "${ENCLAVE_IMAGE_URI_SSM}" | jq -r '.Parameter.Value') 36 | enclave=$(nitro-cli describe-enclaves | jq -r '.[0].EnclaveName') 37 | 38 | if [[ ${enclave} == "null" ]]; then 39 | aws s3 cp "${enclave_image_uri}" "${EIF_PATH}" 40 | nitro-cli run-enclave --cpu-count "${ENCLAVE_CPU_COUNT}" --memory "${ENCLAVE_MEMORY_SIZE}" --eif-path "${EIF_PATH}" ${debug} 41 | sleep 5 42 | fi 43 | 44 | # second call for pod logs to see if enclave is still up and running 45 | enclave_cid=$(nitro-cli describe-enclaves | jq -r '.[0].EnclaveCID') 46 | 47 | # ensure that proper cid has been assigned 48 | [ "${enclave_cid}" == "null" ] && exit 1 49 | 50 | export ENCLAVE_CID=${enclave_cid} 51 | 52 | # start outbound vsock proxy in background 53 | vsock-proxy "${vsock_port_1}" kms."${AWS_REGION}".amazonaws.com 443 -w 20 & 54 | 55 | # todo tls inject private key and cert -> secrets manager download 56 | # todo save in ssm config store and point lambda to x509 cert for validation 57 | generate_tls_artifact "${FQDN}" 58 | 59 | ./signing_pod 60 | -------------------------------------------------------------------------------- /applications/ethereum-signer/go.mod: -------------------------------------------------------------------------------- 1 | module aws/ethereum-signer 2 | 3 | go 1.21.0 4 | 5 | toolchain go1.22.4 6 | 7 | require ( 8 | github.com/aws/aws-lambda-go v1.37.0 9 | github.com/aws/aws-sdk-go-v2/config v1.18.45 10 | github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.10 11 | github.com/aws/aws-sdk-go-v2/service/kms v1.21.0 12 | github.com/ethereum/go-ethereum v1.13.15 13 | github.com/gin-gonic/gin v1.10.0 14 | github.com/google/uuid v1.3.0 15 | github.com/mdlayher/vsock v1.2.0 16 | github.com/sirupsen/logrus v1.9.0 17 | golang.org/x/net v0.33.0 18 | ) 19 | 20 | require ( 21 | github.com/hf/nsm v0.0.0-20220930140112-cd181bd646b9 22 | github.com/mackerelio/go-osstat v0.2.4 23 | github.com/prozz/aws-embedded-metrics-golang v1.2.0 24 | github.com/spf13/cobra v1.8.1 25 | github.com/stretchr/testify v1.10.0 26 | go.mozilla.org/pkcs7 v0.9.0 27 | ) 28 | 29 | require ( 30 | github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.14.1 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.7.21 // indirect 33 | github.com/bits-and-blooms/bitset v1.13.0 // indirect 34 | github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect 35 | github.com/bytedance/sonic v1.12.3 // indirect 36 | github.com/bytedance/sonic/loader v0.2.0 // indirect 37 | github.com/cloudwego/base64x v0.1.4 // indirect 38 | github.com/cloudwego/iasm v0.2.0 // indirect 39 | github.com/consensys/bavard v0.1.13 // indirect 40 | github.com/consensys/gnark-crypto v0.12.1 // indirect 41 | github.com/crate-crypto/go-kzg-4844 v0.7.0 // indirect 42 | github.com/davecgh/go-spew v1.1.1 // indirect 43 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect 44 | github.com/ethereum/c-kzg-4844 v0.4.0 // indirect 45 | github.com/fxamacker/cbor/v2 v2.2.0 // indirect 46 | github.com/gabriel-vasile/mimetype v1.4.5 // indirect 47 | github.com/holiman/uint256 v1.2.4 // indirect 48 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 49 | github.com/jmespath/go-jmespath v0.4.0 // indirect 50 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 51 | github.com/mdlayher/socket v0.4.0 // indirect 52 | github.com/mmcloughlin/addchain v0.4.0 // indirect 53 | github.com/pmezard/go-difflib v1.0.0 // indirect 54 | github.com/spf13/pflag v1.0.5 // indirect 55 | github.com/stretchr/objx v0.5.2 // indirect 56 | github.com/supranational/blst v0.3.11 // indirect 57 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 58 | github.com/x448/float16 v0.8.4 // indirect 59 | golang.org/x/arch v0.10.0 // indirect 60 | golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect 61 | golang.org/x/sync v0.10.0 // indirect 62 | gopkg.in/yaml.v3 v3.0.1 // indirect 63 | rsc.io/tmplfunc v0.0.3 // indirect 64 | ) 65 | 66 | require ( 67 | github.com/aws/aws-sdk-go-v2 v1.21.2 68 | github.com/aws/aws-sdk-go-v2/credentials v1.13.43 69 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect 70 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect 71 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 // indirect 72 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 // indirect 73 | github.com/aws/aws-sdk-go-v2/service/dynamodb v1.18.1 74 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 // indirect 75 | github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 // indirect 76 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 // indirect 77 | github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 78 | github.com/aws/smithy-go v1.15.0 // indirect 79 | github.com/gin-contrib/sse v0.1.0 // indirect 80 | github.com/go-playground/locales v0.14.1 // indirect 81 | github.com/go-playground/universal-translator v0.18.1 // indirect 82 | github.com/go-playground/validator/v10 v10.22.1 83 | github.com/goccy/go-json v0.10.3 // indirect 84 | github.com/json-iterator/go v1.1.12 // indirect 85 | github.com/leodido/go-urn v1.4.0 // indirect 86 | github.com/mattn/go-isatty v0.0.20 // indirect 87 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 88 | github.com/modern-go/reflect2 v1.0.2 // indirect 89 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 90 | github.com/ugorji/go/codec v1.2.12 // indirect 91 | golang.org/x/crypto v0.31.0 // indirect 92 | golang.org/x/sys v0.28.0 // indirect 93 | golang.org/x/text v0.21.0 // indirect 94 | google.golang.org/protobuf v1.34.2 // indirect 95 | ) 96 | 97 | replace go.mozilla.org/pkcs7 v0.9.0 => github.com/dpdornseifer/pkcs7 v0.9.1 98 | -------------------------------------------------------------------------------- /applications/ethereum-signer/images/key-generator_enclave/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | ## build 5 | FROM go_eks_base as build 6 | COPY . . 7 | ARG generator_enclave_folders='./cmd/key-generator_enclave/... ./internal/...' 8 | 9 | ARG SKIP_TEST_ARG 10 | 11 | RUN if [ "$SKIP_TEST_ARG" != "true" ]; then staticcheck $generator_enclave_folders && \ 12 | gosec $generator_enclave_folders && \ 13 | go test $generator_enclave_folders -test.v; fi 14 | 15 | RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -ldflags="-X main.version=v0.0.1" ./cmd/key-generator_enclave 16 | 17 | ## deploy image 18 | FROM alpine:latest AS prod 19 | RUN apk update && \ 20 | apk add ca-certificates iproute2 && \ 21 | rm -rf /var/cache/apk/* 22 | 23 | ARG REGION_ARG 24 | ENV REGION=$REGION_ARG 25 | 26 | ARG LOG_LEVEL_ARG 27 | ENV LOG_LEVEL=$LOG_LEVEL_ARG 28 | 29 | ARG VSOCK_BASE_PORT_ARG 30 | ENV PORT=$VSOCK_BASE_PORT_ARG 31 | 32 | WORKDIR /app 33 | 34 | # viproxy for outbound communication 35 | COPY ./third_party/proxy/proxy /app 36 | COPY ./cmd/key-generator_enclave/run.sh /app 37 | COPY --from=build /app/key-generator_enclave /app/key-generator_enclave 38 | 39 | CMD ["sh", "/app/run.sh"] 40 | -------------------------------------------------------------------------------- /applications/ethereum-signer/images/key-generator_pod/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | ## build 5 | FROM go_eks_base as build 6 | COPY . . 7 | ARG SKIP_TEST_ARG 8 | 9 | ARG generator_pod_folders="./cmd/key-generator_pod/... ./internal/..." 10 | # excluded G304, since that pod is running in protected EKS environment and 11 | # AWS_WEB_IDENTITY_TOKEN_FILE is being injected by EKS directly 12 | RUN if [ "$SKIP_TEST_ARG" != "true" ]; then staticcheck $generator_pod_folders && \ 13 | gosec $generator_pod_folders && \ 14 | go test $generator_pod_folders -test.v; fi 15 | 16 | RUN CGO_ENABLED=0 go build -ldflags="-X main.version=v0.0.1" ./cmd/key-generator_pod 17 | 18 | ## deploy image including required nitro-cli binaries 19 | FROM nitro_eks_pod_base_image:latest AS prod 20 | RUN yum update -y && \ 21 | yum install openssl openssl11 gettext -y && \ 22 | yum clean all && \ 23 | rm -rf /var/cache/yum 24 | 25 | RUN touch ~/.rnd 26 | 27 | WORKDIR /app 28 | 29 | COPY cmd/key-generator_pod/run.sh ./ 30 | COPY --from=build /app/key-generator_pod /app/key-generator_pod 31 | 32 | CMD ["/app/run.sh"] 33 | -------------------------------------------------------------------------------- /applications/ethereum-signer/images/lambda/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | ## build 5 | FROM go_lambda_base as build 6 | COPY . . 7 | ARG SKIP_TEST_ARG 8 | 9 | ARG lambda_folders="./cmd/lambda/... ./internal/..." 10 | 11 | # exclude TLS InsecureSkipVerify set true warning - pod is generating self signed certificate which needs to be 12 | # imported to Lambda for validation - no transport in place right now 13 | RUN if [ "$SKIP_TEST_ARG" != "true" ]; then staticcheck $lambda_folders && \ 14 | gosec $lambda_folders && \ 15 | go test $lambda_folders -test.v; fi 16 | 17 | RUN go build -ldflags="-X main.version=v0.0.1" -o /main ./cmd/lambda 18 | 19 | # copy artifacts to a clean image 20 | FROM public.ecr.aws/lambda/provided:al2 AS prod 21 | COPY --from=build /main /main 22 | ENTRYPOINT [ "/main" ] -------------------------------------------------------------------------------- /applications/ethereum-signer/images/signing_enclave/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | ## build 5 | FROM go_eks_base as build 6 | COPY . . 7 | ARG SKIP_TEST_ARG 8 | 9 | ARG signing_enclave_folders="./cmd/signing_enclave/... ./internal/..." 10 | 11 | RUN if [ "$SKIP_TEST_ARG" != "true" ]; then staticcheck $signing_enclave_folders && \ 12 | gosec $signing_enclave_folders && \ 13 | go test $signing_enclave_folders -test.v; fi 14 | 15 | RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -ldflags="-X main.version=v0.0.1" ./cmd/signing_enclave 16 | 17 | ## deploy image 18 | FROM debian:bookworm-slim AS prod 19 | RUN apt-get update && \ 20 | apt-get install -y ca-certificates procps && \ 21 | apt-get clean && \ 22 | apt-get autoremove --yes && \ 23 | rm -rf /var/lib/apt/lists/* 24 | 25 | ARG REGION_ARG 26 | ENV REGION=$REGION_ARG 27 | 28 | ARG LOG_LEVEL_ARG 29 | ENV LOG_LEVEL=$LOG_LEVEL_ARG 30 | 31 | ARG VSOCK_BASE_PORT_ARG 32 | ENV PORT=$VSOCK_BASE_PORT_ARG 33 | 34 | WORKDIR /app 35 | 36 | 37 | COPY ./cmd/signing_enclave/run.sh /app 38 | COPY --from=build /app/signing_enclave /app/signing_enclave 39 | 40 | CMD ["sh", "/app/run.sh"] 41 | -------------------------------------------------------------------------------- /applications/ethereum-signer/images/signing_pod/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | ## build 5 | FROM go_eks_base as build 6 | COPY . . 7 | ARG SKIP_TEST_ARG 8 | 9 | # make tests skipable for cross platform compile tests to save significant time 10 | # excluded G304, since that pod is running in protected EKS environment and AWS_WEB_IDENTITY_TOKEN_FILE \ 11 | # is being injected by EKS directly 12 | ARG signing_pod_folders="./cmd/signing_pod/... ./internal/..." 13 | RUN if [ "$SKIP_TEST_ARG" != "true" ]; then staticcheck $signing_pod_folders && \ 14 | gosec $signing_pod_folders && \ 15 | go test $signing_pod_folders -test.v; fi 16 | 17 | RUN CGO_ENABLED=0 go build -ldflags="-X main.version=v0.0.1" ./cmd/signing_pod 18 | 19 | ## deploy image including required nitro-cli binaries 20 | FROM nitro_eks_pod_base_image:latest as prod 21 | RUN yum update -y && \ 22 | yum install openssl openssl11 -y && \ 23 | yum clean all && \ 24 | rm -rf /var/cache/yum 25 | RUN touch ~/.rnd 26 | 27 | WORKDIR /app 28 | 29 | COPY cmd/signing_pod/run.sh ./ 30 | COPY --from=build /app/signing_pod /app/signing_pod 31 | 32 | 33 | CMD ["/app/run.sh"] 34 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/attestation/attestation.go: -------------------------------------------------------------------------------- 1 | package attestation 2 | 3 | import ( 4 | "errors" 5 | "github.com/hf/nsm" 6 | "github.com/hf/nsm/request" 7 | ) 8 | 9 | type AttestationProvider interface { 10 | GetAttestationDoc(nonce []byte, userData []byte, publicKey []byte) ([]byte, error) 11 | } 12 | 13 | type NitroAttestationProvider struct{} 14 | 15 | func (p *NitroAttestationProvider) GetAttestationDoc(nonce []byte, userData []byte, publicKey []byte) ([]byte, error) { 16 | return GetAttestationDoc(nonce, userData, publicKey) 17 | } 18 | 19 | func GetAttestationDoc(nonce, userData, publicKey []byte) ([]byte, error) { 20 | sess, err := nsm.OpenDefaultSession() 21 | defer func(sess *nsm.Session) { 22 | err := sess.Close() 23 | if err != nil { 24 | 25 | } 26 | }(sess) 27 | 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | res, err := sess.Send(&request.Attestation{ 33 | Nonce: nonce, 34 | UserData: userData, 35 | PublicKey: publicKey, 36 | }) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | if res.Error != "" { 42 | return nil, errors.New(string(res.Error)) 43 | } 44 | 45 | if nil == res.Attestation || nil == res.Attestation.Document { 46 | return nil, errors.New("NSM device did not return an attestation") 47 | } 48 | 49 | return res.Attestation.Document, nil 50 | } 51 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/aws/util.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | signerTypes "aws/ethereum-signer/internal/types" 5 | "github.com/aws/aws-sdk-go-v2/aws" 6 | "github.com/aws/aws-sdk-go-v2/config" 7 | "github.com/aws/aws-sdk-go-v2/credentials" 8 | "github.com/mdlayher/vsock" 9 | "golang.org/x/net/context" 10 | "net" 11 | "net/http" 12 | "time" 13 | ) 14 | 15 | type ConnectionType int 16 | 17 | const ( 18 | VSOCK ConnectionType = iota 19 | TCP 20 | ) 21 | 22 | type ConnectionConfig struct { 23 | connectionType ConnectionType 24 | contextID uint32 25 | port uint32 26 | } 27 | 28 | func NewConnectionConfig(connectionType ConnectionType, contextID uint32, port uint32) ConnectionConfig { 29 | if connectionType != VSOCK { 30 | return ConnectionConfig{ 31 | connectionType: connectionType, 32 | } 33 | } 34 | 35 | return ConnectionConfig{ 36 | connectionType: connectionType, 37 | contextID: contextID, 38 | port: port, 39 | } 40 | } 41 | 42 | func EnclaveSDKConfig(ephemeralCredentials signerTypes.AWSCredentials, region string, connectionConfig ConnectionConfig) (aws.Config, error) { 43 | cfg, err := config.LoadDefaultConfig(context.TODO(), 44 | config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(ephemeralCredentials.AccessKeyID, ephemeralCredentials.SecretAccessKey, ephemeralCredentials.Token))) 45 | if err != nil { 46 | //enclave.HandleError(conn, fmt.Sprintf("configuration error: %s", err), 500) 47 | return aws.Config{}, err 48 | } 49 | cfg.Region = region 50 | cfg.HTTPClient = &http.Client{Timeout: 5 * time.Second} 51 | // replace dialer with vsock based approach if required, else rely on standard http dial out 52 | if connectionConfig.connectionType == VSOCK { 53 | cfg.HTTPClient = &http.Client{ 54 | Transport: &http.Transport{ 55 | DialContext: func(ctx context.Context, network string, address string) (conn net.Conn, e error) { 56 | return vsock.Dial(connectionConfig.contextID, connectionConfig.port, nil) 57 | }, 58 | }, 59 | } 60 | } 61 | 62 | return cfg, nil 63 | } 64 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/enclave/config.go: -------------------------------------------------------------------------------- 1 | package enclave 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | type Config struct { 10 | LogLevel string 11 | Region string 12 | Port uint32 13 | } 14 | 15 | func LoadConfig() (*Config, error) { 16 | port := os.Getenv("PORT") 17 | if port == "" { 18 | return nil, fmt.Errorf("PORT cannot be empty") 19 | } 20 | 21 | portInt, err := strconv.ParseUint(port, 10, 32) 22 | if err != nil { 23 | return nil, fmt.Errorf("invalid port number: %v", err) 24 | } 25 | 26 | region := os.Getenv("REGION") 27 | if region == "" { 28 | return nil, fmt.Errorf("REGION cannot be empty") 29 | } 30 | 31 | return &Config{ 32 | LogLevel: os.Getenv("LOG_LEVEL"), 33 | Region: region, 34 | Port: uint32(portInt), 35 | }, nil 36 | } 37 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/enclave/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | 7 | package enclave 8 | 9 | import ( 10 | signerTypes "aws/ethereum-signer/internal/types" 11 | "encoding/json" 12 | log "github.com/sirupsen/logrus" 13 | "net" 14 | ) 15 | 16 | func HandleError(conn net.Conn, msg string, status int) { 17 | defer func(conn net.Conn) { 18 | err := conn.Close() 19 | if err != nil { 20 | log.Errorf("error happened closing vsock connection: %s", err) 21 | } 22 | }(conn) 23 | switch status { 24 | case 500: 25 | log.Errorf(msg) 26 | case 403: 27 | log.Warnf(msg) 28 | } 29 | 30 | response, _ := json.Marshal(signerTypes.EnclaveResult{ 31 | Status: status, 32 | Body: signerTypes.SignedTransaction{ 33 | Error: msg, 34 | }, 35 | }) 36 | _, err := conn.Write(response) 37 | if err != nil { 38 | log.Errorf("error happened writing back result via vsock connect: %s", err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/enclave/util_test.go: -------------------------------------------------------------------------------- 1 | package enclave 2 | 3 | import ( 4 | signerTypes "aws/ethereum-signer/internal/types" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/stretchr/testify/mock" 8 | "net" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | // MockConn is a mock implementation of net.Conn 14 | type MockConn struct { 15 | mock.Mock 16 | } 17 | 18 | func (m *MockConn) Read(b []byte) (n int, err error) { 19 | args := m.Called(b) 20 | return args.Int(0), args.Error(1) 21 | } 22 | 23 | func (m *MockConn) Write(b []byte) (n int, err error) { 24 | args := m.Called(b) 25 | return args.Int(0), args.Error(1) 26 | } 27 | 28 | func (m *MockConn) Close() error { 29 | args := m.Called() 30 | return args.Error(0) 31 | } 32 | 33 | // Other required net.Conn interface methods... 34 | // You'll need to implement these but they won't be used in our tests 35 | func (m *MockConn) LocalAddr() net.Addr { return nil } 36 | func (m *MockConn) RemoteAddr() net.Addr { return nil } 37 | func (m *MockConn) SetDeadline(t time.Time) error { return nil } 38 | func (m *MockConn) SetReadDeadline(t time.Time) error { return nil } 39 | func (m *MockConn) SetWriteDeadline(t time.Time) error { return nil } 40 | 41 | func TestHandleError(t *testing.T) { 42 | testCases := []struct { 43 | name string 44 | message string 45 | status int 46 | writeError error 47 | closeError error 48 | }{ 49 | { 50 | name: "500 error handling", 51 | message: "internal server error", 52 | status: 500, 53 | writeError: nil, 54 | closeError: nil, 55 | }, 56 | { 57 | name: "403 error handling", 58 | message: "forbidden access", 59 | status: 403, 60 | writeError: nil, 61 | closeError: nil, 62 | }, 63 | { 64 | name: "Write error handling", 65 | message: "test message", 66 | status: 500, 67 | writeError: fmt.Errorf("write error"), 68 | closeError: nil, 69 | }, 70 | { 71 | name: "Close error handling", 72 | message: "test message", 73 | status: 500, 74 | writeError: nil, 75 | closeError: fmt.Errorf("close error"), 76 | }, 77 | } 78 | 79 | for _, tc := range testCases { 80 | t.Run(tc.name, func(t *testing.T) { 81 | mockConn := new(MockConn) 82 | 83 | // Setup expected response 84 | expectedResponse, _ := json.Marshal(signerTypes.EnclaveResult{ 85 | Status: tc.status, 86 | Body: signerTypes.SignedTransaction{ 87 | Error: tc.message, 88 | }, 89 | }) 90 | 91 | // Setup expectations 92 | mockConn.On("Write", expectedResponse).Return(len(expectedResponse), tc.writeError) 93 | mockConn.On("Close").Return(tc.closeError) 94 | 95 | // Execute function 96 | HandleError(mockConn, tc.message, tc.status) 97 | 98 | // Verify all expectations were met 99 | mockConn.AssertExpectations(t) 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/ethereum/ethereum_test.go: -------------------------------------------------------------------------------- 1 | package ethereum 2 | 3 | import ( 4 | "aws/ethereum-signer/internal/types" 5 | "github.com/ethereum/go-ethereum/common/hexutil" 6 | types2 "github.com/ethereum/go-ethereum/core/types" 7 | "math/big" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func Test_assembleEthereumTransaction(t *testing.T) { 13 | type args struct { 14 | transactionPayload types.TransactionPayload 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | want string 20 | }{ 21 | { 22 | name: "ok", 23 | args: args{ 24 | transactionPayload: types.TransactionPayload{ 25 | Value: 0.01, 26 | To: "0xa5D3241A1591061F2a4bB69CA0215F66520E67cf", 27 | Nonce: 0, 28 | Type: 2, 29 | ChainID: 5, 30 | Gas: 100000, 31 | MaxFeePerGas: 100000000000, 32 | MaxPriorityFeePerGas: 3000000000, 33 | Data: "", 34 | }}, 35 | want: "0xd7e35d4a5f46548a2469cebf6d5a9c4749829b7477b8f9355a1e40f329575ba6", 36 | }, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | got := AssembleTransaction(tt.args.transactionPayload) 41 | if !reflect.DeepEqual(got.Hash().Hex(), tt.want) { 42 | t.Errorf("assembleEthereumTransaction() got = %v, want %v", got, tt.want) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func Test_etherToWei(t *testing.T) { 49 | type args struct { 50 | eth *big.Float 51 | } 52 | tests := []struct { 53 | name string 54 | args args 55 | want *big.Int 56 | }{ 57 | {name: "ok", 58 | args: args{eth: big.NewFloat(0.001)}, 59 | want: big.NewInt(1000000000000000), 60 | }, 61 | } 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | if got := etherToWei(tt.args.eth); !reflect.DeepEqual(got, tt.want) { 65 | t.Errorf("etherToWei() = %v, want %v", got, tt.want) 66 | } 67 | }) 68 | } 69 | } 70 | 71 | func Test_signEthereumTransaction(t *testing.T) { 72 | r, _ := new(big.Int).SetString("52797251190525160680904325891953736147891461127620022141963968447794245047899", 10) 73 | s, _ := new(big.Int).SetString("39479225745237986495600255552539419729722647530016349795492341493095878075154", 10) 74 | type args struct { 75 | assembledTx *types2.Transaction 76 | ethKey string 77 | } 78 | tests := []struct { 79 | name string 80 | args args 81 | want []*big.Int 82 | wantErr bool 83 | }{ 84 | { 85 | name: "ok", 86 | args: args{ 87 | assembledTx: AssembleTransaction(types.TransactionPayload{ 88 | Value: 0.01, 89 | To: "0xa5D3241A1591061F2a4bB69CA0215F66520E67cf", 90 | Nonce: 0, 91 | Type: 2, 92 | ChainID: 5, 93 | Gas: 100000, 94 | MaxFeePerGas: 100000000000, 95 | MaxPriorityFeePerGas: 3000000000, 96 | Data: "", 97 | }), 98 | ethKey: "372369f374c68952bcb2ba2e3f3802d41d51cb255446c27def96cc848605d679", 99 | }, 100 | want: []*big.Int{ 101 | big.NewInt(1), 102 | r, 103 | s, 104 | }, 105 | wantErr: false, 106 | }, 107 | } 108 | for _, tt := range tests { 109 | t.Run(tt.name, func(t *testing.T) { 110 | got, err := SignEthereumTransaction(tt.args.assembledTx, tt.args.ethKey) 111 | if (err != nil) != tt.wantErr { 112 | t.Errorf("signEthereumTransaction() error = %v, wantErr %v", err, tt.wantErr) 113 | return 114 | } 115 | v, r, s := got.RawSignatureValues() 116 | gotSignature := []*big.Int{ 117 | v, 118 | r, 119 | s, 120 | } 121 | if !reflect.DeepEqual(gotSignature, tt.want) { 122 | t.Errorf("signEthereumTransaction() got = %v, want %v", gotSignature, tt.want) 123 | } 124 | }) 125 | } 126 | } 127 | 128 | func Test_signUserOps(t *testing.T) { 129 | bytes, _ := hexutil.Decode("0xf3df4bcb3b24437160ba86a88f41d522662ed994dddd11ac477cfc16e9a71869") 130 | 131 | type args struct { 132 | userOpsHash []byte 133 | ethKey string 134 | } 135 | tests := []struct { 136 | name string 137 | args args 138 | want string 139 | wantErr bool 140 | }{ 141 | { 142 | name: "ok", 143 | args: args{ 144 | userOpsHash: bytes, 145 | ethKey: "4ed3992ca0c7dda74dd9e77adec683afce86605866f1020c063b5ae1b67159c4", 146 | }, 147 | want: "0xd154b7efa02c822051d61899bbeb1ee9887b8bcc30f4f549a3d49950f13ee02d2140ced835ef716a4648082c762a161e1764950523f3716e0d0a51672b30205d1c", 148 | wantErr: false, 149 | }, 150 | } 151 | for _, tt := range tests { 152 | t.Run(tt.name, func(t *testing.T) { 153 | got, err := SignUserOps(tt.args.userOpsHash, tt.args.ethKey) 154 | if (err != nil) != tt.wantErr { 155 | t.Errorf("signUserOps() error = %v, wantErr %v", err, tt.wantErr) 156 | return 157 | } 158 | if got != tt.want { 159 | t.Errorf("signUserOps() got = %v, want %v", got, tt.want) 160 | } 161 | }) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/ethereum/transaction.go: -------------------------------------------------------------------------------- 1 | package ethereum 2 | 3 | import ( 4 | signerTypes "aws/ethereum-signer/internal/types" 5 | "crypto/ecdsa" 6 | "errors" 7 | "github.com/ethereum/go-ethereum/common" 8 | "github.com/ethereum/go-ethereum/crypto" 9 | "github.com/sirupsen/logrus" 10 | "math/big" 11 | 12 | ethTypes "github.com/ethereum/go-ethereum/core/types" 13 | ) 14 | 15 | func SignEthereumTransaction(assembledTx *ethTypes.Transaction, ethKey string) (*ethTypes.Transaction, error) { 16 | logrus.Debugf("assembledTxHash: %v / ethKey: %v", assembledTx.Hash(), ethKey) 17 | privateKey, err := crypto.HexToECDSA(ethKey) 18 | if err != nil { 19 | return ðTypes.Transaction{}, err 20 | } 21 | 22 | publicKey := privateKey.Public() 23 | publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) 24 | if !ok { 25 | return ðTypes.Transaction{}, errors.New("error happened casting public key to ECDSA") 26 | } 27 | if publicKeyECDSA == nil { 28 | return ðTypes.Transaction{}, errors.New("error happened casting public key to ECDSA") 29 | } 30 | fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) 31 | logrus.Debugf("assembling transaction for sender: %s", fromAddress.Hex()) 32 | 33 | londonSigner := ethTypes.NewLondonSigner(assembledTx.ChainId()) 34 | 35 | signedTx, err := ethTypes.SignTx(assembledTx, londonSigner, privateKey) 36 | if err != nil { 37 | return ðTypes.Transaction{}, err 38 | } 39 | 40 | return signedTx, nil 41 | } 42 | 43 | func AssembleTransaction(transactionPayload signerTypes.TransactionPayload) *ethTypes.Transaction { 44 | //log.Debugf("raw transaction payload: %v", transactionPayload) 45 | 46 | // todo edge cases here? error? 47 | chainID := big.NewInt(int64(transactionPayload.ChainID)) 48 | to := common.HexToAddress(transactionPayload.To) 49 | maxFeePerGas := big.NewInt(int64(transactionPayload.MaxFeePerGas)) 50 | maxPriorityFeePerGas := big.NewInt(int64(transactionPayload.MaxPriorityFeePerGas)) 51 | var data []byte 52 | var value *big.Int 53 | 54 | if transactionPayload.Value != 0 { 55 | value = etherToWei(big.NewFloat(float64(transactionPayload.Value))) 56 | } 57 | 58 | if transactionPayload.Data != "" { 59 | data = []byte(transactionPayload.Data) 60 | } 61 | 62 | tx := ethTypes.NewTx(ðTypes.DynamicFeeTx{ 63 | ChainID: chainID, 64 | Nonce: uint64(transactionPayload.Nonce), //#nosec G115 65 | GasTipCap: maxPriorityFeePerGas, 66 | GasFeeCap: maxFeePerGas, 67 | Gas: uint64(transactionPayload.Gas), //#nosec G115 68 | To: &to, 69 | Value: value, 70 | Data: data, 71 | }) 72 | 73 | return tx 74 | } 75 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/ethereum/units.go: -------------------------------------------------------------------------------- 1 | package ethereum 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ethereum/go-ethereum/params" 6 | "math/big" 7 | "strings" 8 | ) 9 | 10 | // https://github.com/ethereum/go-ethereum/issues/21221#issuecomment-802092592 11 | func etherToWei(eth *big.Float) *big.Int { 12 | truncInt, _ := eth.Int(nil) 13 | truncInt = new(big.Int).Mul(truncInt, big.NewInt(params.Ether)) 14 | fracStr := strings.Split(fmt.Sprintf("%.18f", eth), ".")[1] 15 | fracStr += strings.Repeat("0", 18-len(fracStr)) 16 | fracInt, _ := new(big.Int).SetString(fracStr, 10) 17 | wei := new(big.Int).Add(truncInt, fracInt) 18 | return wei 19 | } 20 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/ethereum/userop.go: -------------------------------------------------------------------------------- 1 | package ethereum 2 | 3 | import ( 4 | "github.com/ethereum/go-ethereum/common/hexutil" 5 | "github.com/ethereum/go-ethereum/crypto" 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func SignUserOps(userOpsHash []byte, ethKey string) (string, error) { 10 | //log.Debugf("userOpsHashRaw: %v / ethKey %v", userOpsHash, ethKey) 11 | privateKey, err := crypto.HexToECDSA(ethKey) 12 | if err != nil { 13 | return "", err 14 | } 15 | // (signature.recoveryParam ? "0x1c": "0x1b") 16 | //https://developer.cargox.digital/examples/signing_with_go.html 17 | signature, err := crypto.Sign(userOpsHash[:], privateKey) 18 | if err != nil { 19 | return "", err 20 | } 21 | // ethers implementation to determine v (legacy recovery parameter) for signed messages 22 | logrus.Debugf("signature (original) v: %v", signature[64]) 23 | signature[64] += byte(27) 24 | 25 | logrus.Debugf("signature (legacy) v: %v", signature[64]) 26 | signatureEncoded := hexutil.Encode(signature) 27 | 28 | return signatureEncoded, nil 29 | } 30 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/hmac/hmac.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | 7 | package hmac 8 | 9 | import ( 10 | "crypto/hmac" 11 | "crypto/sha256" 12 | "encoding/hex" 13 | log "github.com/sirupsen/logrus" 14 | "strconv" 15 | ) 16 | 17 | func CalculateHMAC(transactionPayloadSerialized []byte, secret string, timestamp int) string { 18 | log.Debugf("raw payload to sign: %s", transactionPayloadSerialized) 19 | 20 | transactionPayloadSerializedToSign := string(transactionPayloadSerialized) + strconv.Itoa(timestamp) 21 | log.Debugf("payload to sign: %s", transactionPayloadSerializedToSign) 22 | 23 | hmacFunc := hmac.New(sha256.New, []byte(secret)) 24 | hmacFunc.Write([]byte(transactionPayloadSerializedToSign)) 25 | hmacHex := hex.EncodeToString(hmacFunc.Sum(nil)) 26 | 27 | return hmacHex 28 | } 29 | 30 | func TimestampInRange(providedTimestamp, ownTimestamp, maxDelta int) bool { 31 | return ownTimestamp <= providedTimestamp+maxDelta 32 | } 33 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/hmac/hmac_test.go: -------------------------------------------------------------------------------- 1 | package hmac 2 | 3 | import "testing" 4 | 5 | func TestCalculateHMAC(t *testing.T) { 6 | type args struct { 7 | transactionPayloadSerialized []byte 8 | secret string 9 | timestamp int 10 | } 11 | tests := []struct { 12 | name string 13 | args args 14 | want string 15 | }{ 16 | {name: "userop_hash_ok", 17 | args: args{ 18 | transactionPayloadSerialized: []byte("{\"userOpHash\":\"0xf3df4bcb3b24437160ba86a88f41d522662ed994dddd11ac477cfc16e9a71869\"}"), 19 | secret: "9779d2b8f0bc495b1691ce1a2baf800453e18a58d4eea8bf1fe996a0ab291dba", 20 | timestamp: 1702632823, 21 | }, 22 | want: "b9ccfaf082645190ceb443de71b5b534ffc176e78471b0eaec4ed492610d3def", 23 | }, 24 | {name: "eth_tx_hash_ok", 25 | args: args{ 26 | transactionPayloadSerialized: []byte("{\"chainId\":5,\"gas\":100000,\"maxFeePerGas\":100000000000,\"maxPriorityFeePerGas\":3000000000,\"nonce\":0,\"to\":\"0xa5D3241A1591061F2a4bB69CA0215F66520E67cf\",\"type\":2,\"value\":0.01}"), 27 | secret: "9779d2b8f0bc495b1691ce1a2baf800453e18a58d4eea8bf1fe996a0ab291dba", 28 | timestamp: 1702632821, 29 | }, 30 | want: "ec6aabbc51be4c5a09938dc1fc6d3ccd677c9c804ee0813f127f810b44699641", 31 | }, 32 | } 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | got := CalculateHMAC(tt.args.transactionPayloadSerialized, tt.args.secret, tt.args.timestamp) 36 | if got != tt.want { 37 | t.Errorf("CalculateHMAC() got = %v, want %v", got, tt.want) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestTimestampInRange(t *testing.T) { 44 | // Test cases structure 45 | tests := []struct { 46 | name string 47 | providedTimestamp int 48 | ownTimestamp int 49 | maxDelta int 50 | expected bool 51 | }{ 52 | { 53 | name: "Equal timestamps", 54 | providedTimestamp: 1000, 55 | ownTimestamp: 1000, 56 | maxDelta: 5, 57 | expected: true, 58 | }, 59 | { 60 | name: "Timestamp within delta range", 61 | providedTimestamp: 1000, 62 | ownTimestamp: 1003, 63 | maxDelta: 5, 64 | expected: true, 65 | }, 66 | { 67 | name: "Timestamp at max delta", 68 | providedTimestamp: 1000, 69 | ownTimestamp: 1005, 70 | maxDelta: 5, 71 | expected: true, 72 | }, 73 | { 74 | name: "Timestamp outside delta range", 75 | providedTimestamp: 1000, 76 | ownTimestamp: 1006, 77 | maxDelta: 5, 78 | expected: false, 79 | }, 80 | { 81 | name: "Negative delta", 82 | providedTimestamp: 1000, 83 | ownTimestamp: 995, 84 | maxDelta: -5, 85 | expected: true, 86 | }, 87 | { 88 | name: "Zero delta", 89 | providedTimestamp: 1000, 90 | ownTimestamp: 1000, 91 | maxDelta: 0, 92 | expected: true, 93 | }, 94 | } 95 | 96 | // Run all test cases 97 | for _, tt := range tests { 98 | t.Run(tt.name, func(t *testing.T) { 99 | result := TimestampInRange(tt.providedTimestamp, tt.ownTimestamp, tt.maxDelta) 100 | if result != tt.expected { 101 | t.Errorf("TimestampInRange(%d, %d, %d) = %v; want %v", 102 | tt.providedTimestamp, tt.ownTimestamp, tt.maxDelta, result, tt.expected) 103 | } 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/keymanagement/kms.go: -------------------------------------------------------------------------------- 1 | package keymanagement 2 | 3 | import ( 4 | "aws/ethereum-signer/internal/attestation" 5 | aws2 "aws/ethereum-signer/internal/aws" 6 | "aws/ethereum-signer/internal/types" 7 | "bytes" 8 | "crypto" 9 | "crypto/aes" 10 | "crypto/cipher" 11 | "crypto/rand" 12 | "crypto/rsa" 13 | "crypto/x509" 14 | "encoding/asn1" 15 | "encoding/base64" 16 | "encoding/hex" 17 | "fmt" 18 | "github.com/aws/aws-sdk-go-v2/service/kms" 19 | kmstypes "github.com/aws/aws-sdk-go-v2/service/kms/types" 20 | log "github.com/sirupsen/logrus" 21 | "go.mozilla.org/pkcs7" 22 | "golang.org/x/net/context" 23 | "time" 24 | ) 25 | 26 | // AdvancedDecOpts config struct for advanced decryption options 27 | type AdvancedDecOpts struct { 28 | EncryptionContext map[string]string 29 | KeyId string 30 | EncryptionAlgorithm kmstypes.EncryptionAlgorithmSpec 31 | EphemeralRSAKey bool // set to FALSE to use env based key persistence 32 | } 33 | 34 | // Interfaces for dependencies 35 | 36 | type KMSProvider interface { 37 | Decrypt(ctx context.Context, params *kms.DecryptInput, optFns ...func(*kms.Options)) (*kms.DecryptOutput, error) 38 | Encrypt(ctx context.Context, params *kms.EncryptInput, optFns ...func(*kms.Options)) (*kms.EncryptOutput, error) 39 | } 40 | 41 | type AWSKMSProvider struct { 42 | client *kms.Client 43 | } 44 | 45 | func NewAWSKMSProvider(credentials types.AWSCredentials, region string, connectionType aws2.ConnectionType, contextId uint32, port uint32) (*AWSKMSProvider, error) { 46 | cfg, err := aws2.EnclaveSDKConfig(credentials, region, aws2.NewConnectionConfig(connectionType, contextId, port)) 47 | if err != nil { 48 | return nil, fmt.Errorf("unable to load SDK config: %v", err) 49 | } 50 | 51 | return &AWSKMSProvider{ 52 | client: kms.NewFromConfig(cfg), 53 | }, nil 54 | } 55 | 56 | func (p *AWSKMSProvider) Decrypt(ctx context.Context, params *kms.DecryptInput, optFns ...func(*kms.Options)) (*kms.DecryptOutput, error) { 57 | return p.client.Decrypt(ctx, params, optFns...) 58 | } 59 | 60 | func (p *AWSKMSProvider) Encrypt(ctx context.Context, params *kms.EncryptInput, optFns ...func(*kms.Options)) (*kms.EncryptOutput, error) { 61 | return p.client.Encrypt(ctx, params, optFns...) 62 | } 63 | 64 | func DecryptCiphertextWithAttestation(ciphertextB64 string, opts *AdvancedDecOpts, attestationProvider attestation.AttestationProvider, kmsProvider KMSProvider) (string, error) { 65 | 66 | if opts == nil { 67 | opts = &AdvancedDecOpts{} 68 | } 69 | 70 | // create ephemeral private/public key for communication with KMS 71 | keyGenerationStart := time.Now() 72 | ephemeralKey, err := ProvideRSAKey(opts.EphemeralRSAKey) 73 | if err != nil { 74 | return "", err 75 | } 76 | derPublicKey, err := DeriveDEREncodedPublicKey(ephemeralKey) 77 | if err != nil { 78 | return "", err 79 | } 80 | keyGenerationEnd := time.Since(keyGenerationStart).Milliseconds() 81 | log.Debugf("ephemeral key generation took %v ms", keyGenerationEnd) 82 | 83 | // include ephemeral public key in attestation doc and thus provide key to KMS for CMS encryption (RFC5652 section 6) 84 | // nonce and userData is not required/processed by KMS 85 | attestationStart := time.Now() 86 | 87 | attestationDocument, err := attestationProvider.GetAttestationDoc(nil, nil, derPublicKey) 88 | if err != nil { 89 | log.Errorf("failed to get attestation document: %v", err) 90 | return "", err 91 | } 92 | attestationEnd := time.Since(attestationStart).Milliseconds() 93 | log.Debugf("attestation document generation took %v ms", attestationEnd) 94 | 95 | ciphertext, err := base64.StdEncoding.DecodeString(ciphertextB64) 96 | if err != nil { 97 | log.Errorf("failed to decode ciphertext: %v", err) 98 | return "", err 99 | } 100 | 101 | // send decrypt request to KMS including the attestation doc 102 | kmsRequestStart := time.Now() 103 | ciphertextForRecipient, err := decryptCiphertextWithAttestationViaKMS(ciphertext, attestationDocument, opts, kmsProvider) 104 | if err != nil { 105 | return "", err 106 | } 107 | kmsRequestEnd := time.Since(kmsRequestStart).Milliseconds() 108 | log.Debugf("kms request took %v ms", kmsRequestEnd) 109 | 110 | // any value in providing openssl based solution here? 111 | decryptRecipientCiphertext := time.Now() 112 | log.Debugf("ciphertext for recipient: %v", base64.StdEncoding.EncodeToString(ciphertextForRecipient)) 113 | log.Debugf("ephemeral private key: %v", base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PrivateKey(ephemeralKey))) 114 | resultPlaintext, err := decryptCiphertextForRecipient(ciphertextForRecipient, ephemeralKey) 115 | if err != nil { 116 | return "", err 117 | } 118 | resultPlaintextB64 := base64.StdEncoding.EncodeToString(resultPlaintext) 119 | decryptRecipientCiphertextEnd := time.Since(decryptRecipientCiphertext).Milliseconds() 120 | log.Debugf("decrypt recipient ciphertext took %v ms", decryptRecipientCiphertextEnd) 121 | log.Debugf("plaintext result b64: %v", resultPlaintextB64) 122 | 123 | return resultPlaintextB64, nil 124 | } 125 | 126 | func decryptCiphertextWithAttestationViaKMS(ciphertext []byte, attestation []byte, opts *AdvancedDecOpts, kmsProvider KMSProvider) ([]byte, error) { 127 | 128 | // attestation doc including the public key 129 | recipientInfo := &kmstypes.RecipientInfo{ 130 | AttestationDocument: attestation, 131 | KeyEncryptionAlgorithm: "RSAES_OAEP_SHA_256", 132 | } 133 | 134 | decryptInput := &kms.DecryptInput{ 135 | CiphertextBlob: ciphertext, 136 | Recipient: recipientInfo} 137 | 138 | // process optional decrypt options and include in request 139 | if opts.EncryptionContext != nil { 140 | decryptInput.EncryptionContext = opts.EncryptionContext 141 | } 142 | if opts.KeyId != "" && opts.EncryptionAlgorithm != "" { 143 | decryptInput.KeyId = &opts.KeyId 144 | decryptInput.EncryptionAlgorithm = opts.EncryptionAlgorithm 145 | } 146 | 147 | kmsResponse, err := kmsProvider.Decrypt(context.TODO(), decryptInput) 148 | if err != nil { 149 | return nil, fmt.Errorf("exception happened decrypting payload via KMS: %s", err) 150 | } 151 | 152 | return kmsResponse.CiphertextForRecipient, nil 153 | } 154 | 155 | func decryptRecipientInfo(encryptedKey []byte, privateKey *rsa.PrivateKey) ([]byte, error) { 156 | 157 | hash := crypto.SHA256 158 | 159 | // decrypt the encrypted key using RSAES-OAEP 160 | decryptedKey, err := rsa.DecryptOAEP( 161 | hash.New(), 162 | rand.Reader, 163 | privateKey, 164 | encryptedKey, 165 | []byte{}, 166 | ) 167 | if err != nil { 168 | return nil, fmt.Errorf("failed to decrypt key: %v", err) 169 | } 170 | 171 | return decryptedKey, nil 172 | } 173 | 174 | func decryptCiphertextForRecipient(envelopedData []byte, privateKey *rsa.PrivateKey) ([]byte, error) { 175 | // parse the CMS structure 176 | der, err := pkcs7.Ber2der(envelopedData) 177 | if err != nil { 178 | return nil, err 179 | } 180 | var info types.ContentInfo 181 | if _, err := asn1.Unmarshal(der, &info); err != nil { 182 | return nil, fmt.Errorf("failed to parse CMS: %v", err) 183 | } 184 | 185 | var ed types.EnvelopedDataStruct 186 | if _, err := asn1.Unmarshal(info.Content.Bytes, &ed); err != nil { 187 | return nil, fmt.Errorf("failed to parse EnvelopedData: %v", err) 188 | } 189 | log.Debugf("encrypted key: %v", hex.EncodeToString(ed.RecipientInfos[0].EncryptedKey)) 190 | 191 | // decrypt the content encryption key 192 | contentEncryptionKey, err := decryptRecipientInfo( 193 | ed.RecipientInfos[0].EncryptedKey, 194 | privateKey, 195 | ) 196 | if err != nil { 197 | return nil, fmt.Errorf("failed to decrypt content encryption key: %v", err) 198 | } 199 | log.Debugf("content encryption key: %v", hex.EncodeToString(contentEncryptionKey)) 200 | 201 | block, err := aes.NewCipher(contentEncryptionKey) 202 | if err != nil { 203 | return nil, fmt.Errorf("failed to create new cipher block: %v", err) 204 | } 205 | mode := cipher.NewCBCDecrypter(block, ed.EncryptedContentInfo.ContentEncryptionAlgorithm.Parameters.Bytes) //#nosec G407 passing fixed IV token from envelope 206 | 207 | plaintext := make([]byte, len(ed.EncryptedContentInfo.EncryptedContent)) 208 | mode.CryptBlocks(plaintext, ed.EncryptedContentInfo.EncryptedContent) 209 | 210 | length := len(plaintext) 211 | 212 | if length%block.BlockSize() != 0 { 213 | log.Printf("pkcs7: Data is not block-aligned") 214 | } 215 | padLen := int(plaintext[length-1]) 216 | ref := bytes.Repeat([]byte{byte(padLen)}, padLen) 217 | if padLen > block.BlockSize() || padLen == 0 || !bytes.HasSuffix(plaintext, ref) { 218 | log.Printf("pkcs7: Invalid padding") 219 | } 220 | 221 | return plaintext[:length-padLen], nil 222 | } 223 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/keymanagement/kms_test.go: -------------------------------------------------------------------------------- 1 | package keymanagement 2 | 3 | import ( 4 | "aws/ethereum-signer/internal/types" 5 | "encoding/base64" 6 | "errors" 7 | "github.com/aws/aws-sdk-go-v2/service/kms" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | "golang.org/x/net/context" 11 | "os" 12 | "testing" 13 | ) 14 | 15 | const CiphertextForRecipientB64 = "MIAGCSqGSIb3DQEHA6CAMIACAQIxggFrMIIBZwIBAoAg898CKVBqrNfD8U7ZohZms68btEvMiu/aHunx0qvk2oEwPAYJKoZIhvcNAQEHMC+gDzANBglghkgBZQMEAgEFAKEcMBoGCSqGSIb3DQEBCDANBglghkgBZQMEAgEFAASCAQAvKYW8jMsvWf3A7zHMmRa9Ko7/lWnOa9lBcamoabqSPAjUe2Hp1bR/iIFrc/1fmdBGpixO3YeUlKVagzO8sksDFTPMYfLJAFWXosxrKjicWT79zEAxWAzZtj6OTr5MBK+kzlEaiR25Lihts4QGVi3l94e7w4LcPLHrXJp+xSEIqv5ttAN65uCTCPiKV9KOJqQJnIv6AM0n4CADmkK9OH2rGfwTErpGAfLVmQzU4jNzYYlucnPYGeGB5FnyGVCybU6F8dhce7pzvg4fTW4VMjSxMSCEzZUYkWINF3GP7hQW2OJCtOdOT/4LypL4G+ts9v6pLDqsN2aWu6Guxkhx4TJTMIAGCSqGSIb3DQEHATAdBglghkgBZQMEASoEEO+jHoApImXndcYDXJCwxbWggASBoBb7hNObeGBPstJaCKBerxcJX+qsGyiajD4927XBu+zmD7DloS10QznG/w71FgxW7N1donMmmWi33KzqHZ1rOJawPm7jYDoanX3+Kp9kYiemO2iYp+Eujr4yjEsLlcZyWmLMP0EOjd7phL16duEnYG9SYk6KmZlwnm4DO95q/bvFp1tKYaANT3uTaUPP7uYc55rEqp8bCd0QX3ZHSD5A0n0AAAAAAAAAAAAA" 16 | const PrivateKeyB64 = "MIIEpAIBAAKCAQEAxGk1BnKDygnEWPxS2HONDCsdSUPtLwZwJ78tX1BkNOyEXt5FuBJpGtpUBAEunJmybgBfieT3o/j3VXWM88r1o9huVTcXIlPQUEymPMZ/uCR4CYy4WpFT3L9R7iO922zscc/EtdP87HaFTWELETZmlHDirNAcX+k5gpb3FA9BnHDYSCTjoIM9VEcetU/mC8bbU08J5eQMiNR5j+tBVaTtYD4sFF/Yv+QnlO3HRr040etCIzOD+2qkwuKDmBldu8HkatMidzghsidpyIpeSM7c2bJb+gmqqI262eIPgtKvrYbJa2SfJ5QPXuOQ8rkk1lm+POHyNLiTQD1EwlLf7SMoSwIDAQABAoIBAHT4iwG59U6/nlW4f8Y0ms2iZ6CYeYrF9MlXC7h18hequ/KbwT2siTfayqpP4eiViDQGuN8wo2LeBL66cSVHvB7F6H+LfZWOAMOxwlbziGCsJ2jYi3o0jpMqxapjUtB5AB+PswDurPROaXj50FOB6HmC+RweHKfqB7wEGEW0CEkXxzKQxliXjyH56s2O+YV7CEGOaPm2FDh4hDvo5/tdvBIyntFOgAHZ0snPcbaDJ6t/mzwfyb2U8haNQt1HIlhJ0RYrKg8Ma2sXOMiJzwkk4USE3czmkOc08CGGSROzxJwFAheECQx6az/Xgl4iopdtpNLeN/mRBayjL3VyoJkMt0ECgYEAzStKBGsKWJwxpWGoa3lXCMkpSsgvBIRSYhbBvowb3VXpjEIrmoCnojQKcD8GS3Rtfd9qAw8tc6RWdWHNOAFyM29yH/yHF7usAHZr9cXq9rtko4pQYLOH3nhFW8K+bbhf/1mroz/C/AqFObSDU4h4mFZql3FsUDxkb4FF6RP4rDsCgYEA9RJwel/EAnKhM/YtDbBNmVc+qt++Tmfi3AGOjBzI6Yao1f/YskLlwOyKvkvUy8Py0JWf1Ae656lDJhwZwAOCYkKfLMimPVmFBx7B6kuwlAC/Ywah1cIjhne9WWSPxpilJGxhjE5uGeMMnRK0OA7YbD/VRlOGZ/9KFME2zDC4gzECgYEAlqGVsjC0Y+IpQPa2JFHt6HFoc5MNkg9kPMfgbvmG67XLxkI+qSyT5q62izp6cKOGT8fbmWtnP2QEZiHr/ZZyNfk4nOtWc8JBwgUvtj4dCBEFDlzaLmUg9+DtazVLglq/gEZhkXWavlkq/vbdBFNJ1u57S7zmfPIZ+xO6NCmJhUkCgYAM9vPCVYyeAIhsokpR3hDM2uOy0HFV3oMO1no/CUrLp9cIsyc4jvdulFTmqkZQnUYcKL4yzlHh7X9i5buq/8SHBDU9fkPlHPY/oS3rAiQOQFffmjs3frS4aV83+mzsuaiK27zxWjjS38MMEDA+gvKKD3pt5P9IQyYdIPeQJ8erEQKBgQDLxwCIMA/upR8cJ2vKCCPZiyG18MHVqClHJ3GOKeAlALUD7qZcPrIy1XPEmSy+BR2ZWqN9C8sg02S47I0CkuRuoas+VzmS9/zbnmn6SPjc1OS9IArPkHXbGY0b6HbYqC6WkzsZnxoVQyTzgzb+XOkFjToNn+q++7NSWKBboZAexQ==" 17 | 18 | // Mock implementations for testing 19 | type MockAttestationProvider struct { 20 | mock.Mock 21 | } 22 | 23 | func (m *MockAttestationProvider) GetAttestationDoc(nonce []byte, userData []byte, publicKey []byte) ([]byte, error) { 24 | args := m.Called(nonce, userData, publicKey) 25 | return args.Get(0).([]byte), args.Error(1) 26 | } 27 | 28 | type MockKMSProvider struct { 29 | mock.Mock 30 | } 31 | 32 | func (m *MockKMSProvider) Encrypt(ctx context.Context, params *kms.EncryptInput, optFns ...func(*kms.Options)) (*kms.EncryptOutput, error) { 33 | args := m.Called(ctx, params, optFns) 34 | return args.Get(0).(*kms.EncryptOutput), args.Error(1) 35 | } 36 | 37 | func (m *MockKMSProvider) Decrypt(ctx context.Context, params *kms.DecryptInput, optFns ...func(*kms.Options)) (*kms.DecryptOutput, error) { 38 | args := m.Called(ctx, params, optFns) 39 | return args.Get(0).(*kms.DecryptOutput), args.Error(1) 40 | } 41 | 42 | // Test implementation 43 | func TestDecryptCiphertextWithAttestation(t *testing.T) { 44 | type testCase struct { 45 | name string 46 | ciphertext string 47 | setupMocks func(*MockAttestationProvider, *MockKMSProvider) 48 | env map[string]string 49 | expectedError string 50 | expectedResult types.PlainKey 51 | } 52 | 53 | ciphertextB64 := base64.StdEncoding.EncodeToString([]byte("base64EncodedTestData")) 54 | ciphertextForRecipient, _ := base64.StdEncoding.DecodeString(CiphertextForRecipientB64) 55 | 56 | testCases := []testCase{ 57 | { 58 | name: "successful decryption", 59 | ciphertext: ciphertextB64, 60 | 61 | setupMocks: func(ma *MockAttestationProvider, mk *MockKMSProvider) { 62 | // Setup attestation mock 63 | ma.On("GetAttestationDoc", 64 | mock.Anything, 65 | mock.Anything, 66 | mock.Anything, 67 | // todo attestation doc should be enclosed in kms request -> intercept?? 68 | ).Return([]byte("mock-attestation-doc"), nil) 69 | 70 | // Setup KMS mock 71 | mk.On("Decrypt", 72 | mock.Anything, // context 73 | mock.MatchedBy(func(input *kms.DecryptInput) bool { 74 | // Add validation logic if needed 75 | return true 76 | }), 77 | mock.Anything, 78 | ).Return(&kms.DecryptOutput{ 79 | // todo return CFR with matching priv/pub key 80 | CiphertextForRecipient: ciphertextForRecipient, 81 | }, nil) 82 | }, 83 | env: map[string]string{ 84 | "RSA_PRIVATE_KEY": PrivateKeyB64, 85 | }, 86 | expectedResult: types.PlainKey{ 87 | Secret: "9779d2b8f0bc495b1691ce1a2baf800453e18a58d4eea8bf1fe996a0ab291dba", 88 | EthKey: "25e82557e8d2d154503f8e371a36b27b067c5c60793737e76313de1a431e8099"}, 89 | }, 90 | { 91 | name: "attestation error", 92 | ciphertext: "base64EncodedTestData", 93 | setupMocks: func(ma *MockAttestationProvider, mk *MockKMSProvider) { 94 | ma.On("GetAttestationDoc", 95 | mock.Anything, 96 | mock.Anything, 97 | mock.Anything, 98 | ).Return([]byte{}, errors.New("attestation failed")) 99 | }, 100 | expectedError: "attestation failed", 101 | }, 102 | } 103 | 104 | for _, tc := range testCases { 105 | t.Run(tc.name, func(t *testing.T) { 106 | // Create mocks 107 | mockAttestation := &MockAttestationProvider{} 108 | mockKMS := &MockKMSProvider{} 109 | 110 | // Setup mocks 111 | tc.setupMocks(mockAttestation, mockKMS) 112 | 113 | // Setup environment 114 | for k, v := range tc.env { 115 | os.Setenv(k, v) 116 | defer os.Unsetenv(k) 117 | } 118 | 119 | // Test options 120 | opts := &AdvancedDecOpts{ 121 | EphemeralRSAKey: false, 122 | } 123 | 124 | // Call function 125 | result, err := DecryptCiphertextWithAttestation( 126 | tc.ciphertext, 127 | opts, 128 | mockAttestation, 129 | mockKMS, 130 | ) 131 | 132 | resultParsed, _ := ParsePlaintext(result) 133 | // Verify results 134 | if tc.expectedError != "" { 135 | assert.Error(t, err) 136 | assert.Contains(t, err.Error(), tc.expectedError) 137 | } else { 138 | assert.NoError(t, err) 139 | assert.Equal(t, tc.expectedResult, resultParsed) 140 | } 141 | 142 | // Verify all mock expectations were met 143 | mockAttestation.AssertExpectations(t) 144 | mockKMS.AssertExpectations(t) 145 | }) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/keymanagement/storage.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | 7 | package keymanagement 8 | 9 | import ( 10 | aws2 "aws/ethereum-signer/internal/aws" 11 | signerTypes "aws/ethereum-signer/internal/types" 12 | "context" 13 | b64 "encoding/base64" 14 | "encoding/json" 15 | "fmt" 16 | "github.com/aws/aws-sdk-go-v2/aws/retry" 17 | "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" 18 | ddb "github.com/aws/aws-sdk-go-v2/service/dynamodb" 19 | "github.com/aws/aws-sdk-go-v2/service/kms" 20 | "github.com/google/uuid" 21 | "time" 22 | ) 23 | 24 | // dynamodb interface 25 | type DDBProvider interface { 26 | PutItem(ctx context.Context, params *ddb.PutItemInput, optFns ...func(*ddb.Options)) (*ddb.PutItemOutput, error) 27 | } 28 | 29 | type AWSDDBProvider struct { 30 | client *ddb.Client 31 | } 32 | 33 | func NewAWSDDBProvider(credentials signerTypes.AWSCredentials, region string, connectionType aws2.ConnectionType, contextId uint32, port uint32) (*AWSDDBProvider, error) { 34 | cfg, err := aws2.EnclaveSDKConfig(credentials, region, aws2.NewConnectionConfig(connectionType, contextId, port)) 35 | if err != nil { 36 | return nil, fmt.Errorf("unable to load SDK config: %v", err) 37 | } 38 | 39 | return &AWSDDBProvider{ 40 | client: ddb.NewFromConfig(cfg), 41 | }, nil 42 | } 43 | 44 | func (p *AWSDDBProvider) PutItem(ctx context.Context, params *ddb.PutItemInput, optFns ...func(options *ddb.Options)) (*ddb.PutItemOutput, error) { 45 | return p.client.PutItem(ctx, params, optFns...) 46 | } 47 | 48 | func EncryptAndSaveKey(kmsProvider KMSProvider, ddbProvider DDBProvider, keyARN, secretTable string, plainKeyPayload signerTypes.PlainKey, address string) (string, error) { 49 | 50 | // Generate UUID early to fail fast if there's an issue 51 | secretID := uuid.New().String() 52 | 53 | // Use a timeout context instead of context.TODO() 54 | ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) 55 | defer cancel() 56 | 57 | // Marshal the payload 58 | plainKeyPayloadBytes, err := json.Marshal(plainKeyPayload) 59 | if err != nil { 60 | return "", fmt.Errorf("failed to marshal key payload: %w", err) 61 | } 62 | 63 | // Encrypt the payload 64 | result, err := kmsProvider.Encrypt(ctx, &kms.EncryptInput{ 65 | KeyId: &keyARN, 66 | Plaintext: plainKeyPayloadBytes, 67 | }) 68 | if err != nil { 69 | return "", fmt.Errorf("failed to encrypt payload via KMS: %w", err) 70 | } 71 | 72 | // Prepare DynamoDB item 73 | itemRaw := signerTypes.Ciphertext{ 74 | KeyID: secretID, 75 | Ciphertext: b64.StdEncoding.EncodeToString(result.CiphertextBlob), 76 | Address: address, 77 | } 78 | 79 | itemDD, err := attributevalue.MarshalMap(itemRaw) 80 | if err != nil { 81 | return "", fmt.Errorf("failed to marshal DynamoDB item: %w", err) 82 | } 83 | 84 | // Configure retry options for DynamoDB 85 | retry := retry.AddWithMaxAttempts(retry.NewStandard(), 3) 86 | putItemInput := &ddb.PutItemInput{ 87 | Item: itemDD, 88 | TableName: &secretTable, 89 | } 90 | 91 | // Store with retry logic 92 | _, err = ddbProvider.PutItem(ctx, putItemInput, func(o *ddb.Options) { 93 | o.Retryer = retry 94 | }) 95 | if err != nil { 96 | return "", fmt.Errorf("failed to store item in DynamoDB: %w", err) 97 | } 98 | 99 | return secretID, nil 100 | } 101 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/keymanagement/storage_test.go: -------------------------------------------------------------------------------- 1 | package keymanagement 2 | 3 | import ( 4 | signerTypes "aws/ethereum-signer/internal/types" 5 | "context" 6 | "fmt" 7 | ddb "github.com/aws/aws-sdk-go-v2/service/dynamodb" 8 | "github.com/aws/aws-sdk-go-v2/service/kms" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/mock" 11 | "testing" 12 | ) 13 | 14 | // Mock DDB Provider 15 | type MockDDBProvider struct { 16 | mock.Mock 17 | } 18 | 19 | func (m *MockDDBProvider) PutItem(ctx context.Context, params *ddb.PutItemInput, optFns ...func(*ddb.Options)) (*ddb.PutItemOutput, error) { 20 | args := m.Called(ctx, params) 21 | if args.Get(0) == nil { 22 | return nil, args.Error(1) 23 | } 24 | return args.Get(0).(*ddb.PutItemOutput), args.Error(1) 25 | } 26 | 27 | func TestEncryptAndSaveKey(t *testing.T) { 28 | tests := []struct { 29 | name string 30 | keyARN string 31 | secretTable string 32 | plainKey signerTypes.PlainKey 33 | address string 34 | kmsError error 35 | ddbError error 36 | expectedError bool 37 | }{ 38 | { 39 | name: "successful encryption and storage", 40 | keyARN: "arn:aws:kms:region:account:key/test", 41 | secretTable: "test-table", 42 | plainKey: signerTypes.PlainKey{ /* fill with test data */ }, 43 | address: "0x123", 44 | kmsError: nil, 45 | ddbError: nil, 46 | expectedError: false, 47 | }, 48 | { 49 | name: "dynamodb storage fails", 50 | keyARN: "arn:aws:kms:region:account:key/test", 51 | secretTable: "test-table", 52 | plainKey: signerTypes.PlainKey{ /* fill with test data */ }, 53 | address: "0x123", 54 | kmsError: nil, 55 | ddbError: fmt.Errorf("DynamoDB put failed"), 56 | expectedError: true, 57 | }, 58 | } 59 | 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | // Setup mocks 63 | mockKMS := new(MockKMSProvider) 64 | mockDDB := new(MockDDBProvider) 65 | 66 | // Setup KMS mock expectations 67 | if tt.kmsError == nil { 68 | mockKMS.On("Encrypt", mock.Anything, mock.Anything, mock.Anything).Return( 69 | &kms.EncryptOutput{ 70 | CiphertextBlob: []byte("encrypted-data"), 71 | }, 72 | nil, 73 | ) 74 | } else { 75 | mockKMS.On("Encrypt", mock.Anything, mock.Anything, mock.Anything).Return( 76 | nil, 77 | tt.kmsError, 78 | ) 79 | } 80 | 81 | // Setup DDB mock expectations 82 | if tt.ddbError == nil { 83 | mockDDB.On("PutItem", mock.Anything, mock.Anything).Return( 84 | &ddb.PutItemOutput{}, 85 | nil, 86 | ) 87 | } else { 88 | mockDDB.On("PutItem", mock.Anything, mock.Anything).Return( 89 | nil, 90 | tt.ddbError, 91 | ) 92 | } 93 | 94 | // Execute test 95 | secretID, err := EncryptAndSaveKey( 96 | mockKMS, 97 | mockDDB, 98 | tt.keyARN, 99 | tt.secretTable, 100 | tt.plainKey, 101 | tt.address, 102 | ) 103 | 104 | // Verify results 105 | if tt.expectedError { 106 | assert.Error(t, err) 107 | assert.Empty(t, secretID) 108 | } else { 109 | assert.NoError(t, err) 110 | assert.NotEmpty(t, secretID) 111 | } 112 | 113 | // Verify mock expectations 114 | mockKMS.AssertExpectations(t) 115 | mockDDB.AssertExpectations(t) 116 | }) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/keymanagement/util.go: -------------------------------------------------------------------------------- 1 | package keymanagement 2 | 3 | import ( 4 | "aws/ethereum-signer/internal/types" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | "encoding/base64" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | log "github.com/sirupsen/logrus" 13 | "os" 14 | ) 15 | 16 | func ProvideRSAKey(ephemeral bool) (*rsa.PrivateKey, error) { 17 | var privateKey *rsa.PrivateKey 18 | var err error 19 | if !ephemeral { 20 | // check if key stored in env, if yes - load, unmarshal 21 | // if not set, run key creation script write to env and return key 22 | privateKey, err = loadPrivateKeyFromEnv() 23 | if err != nil { 24 | return nil, fmt.Errorf("failed to load private key from environment: %w", err) 25 | } 26 | 27 | } else { 28 | privateKey, err = generateEphemeralRSAKey() 29 | if err != nil { 30 | return nil, fmt.Errorf("failed to generate ephemeral RSA key: %w", err) 31 | } 32 | } 33 | 34 | return privateKey, nil 35 | } 36 | 37 | func DeriveDEREncodedPublicKey(privateKey *rsa.PrivateKey) ([]byte, error) { 38 | publicKey := privateKey.PublicKey 39 | 40 | publicKeyDER, err := x509.MarshalPKIXPublicKey(&publicKey) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | return publicKeyDER, nil 46 | } 47 | 48 | func loadPrivateKeyFromEnv() (*rsa.PrivateKey, error) { 49 | // get the base64-encoded DER private key from environment variable 50 | privateKeyBase64 := os.Getenv("RSA_PRIVATE_KEY") 51 | 52 | // if privateKey is empty, run RSA key generation - serialize the key and store in env, return private key 53 | if privateKeyBase64 == "" { 54 | log.Infof("RSA_PRIVATE_KEY environment variable is not set. Generating ephemeral RSA key...") 55 | privateKey, err := generateEphemeralRSAKey() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) 61 | privateKeyBase64 = base64.StdEncoding.EncodeToString(privateKeyBytes) 62 | err = os.Setenv("RSA_PRIVATE_KEY", privateKeyBase64) 63 | if err != nil { 64 | return nil, err 65 | } 66 | } 67 | 68 | // decode the base64 string to get DER bytes 69 | derBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64) 70 | if err != nil { 71 | return nil, errors.New("failed to decode base64 private key") 72 | } 73 | 74 | // parse the private key from DER format 75 | privateKey, err := x509.ParsePKCS1PrivateKey(derBytes) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | return privateKey, nil 81 | } 82 | 83 | func generateEphemeralRSAKey() (*rsa.PrivateKey, error) { 84 | privateKey, err := rsa.GenerateKey(rand.Reader, 2048) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | return privateKey, nil 90 | } 91 | 92 | func ParsePlaintext(kmsResultB64 string) (types.PlainKey, error) { 93 | log.Debugf("raw kmsResultB64: %v", kmsResultB64) 94 | 95 | kmsResult, err := base64.StdEncoding.DecodeString(kmsResultB64) 96 | if err != nil { 97 | return types.PlainKey{}, fmt.Errorf("failed to decode kmsResultB64: %v", err) 98 | } 99 | 100 | var userKey types.PlainKey 101 | 102 | err = json.Unmarshal(kmsResult, &userKey) 103 | if err != nil { 104 | return types.PlainKey{}, fmt.Errorf("failed to unmarshal kmsResult: %v", err) 105 | } 106 | 107 | return userKey, nil 108 | } 109 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/keymanagement/util_test.go: -------------------------------------------------------------------------------- 1 | package keymanagement 2 | 3 | import ( 4 | "aws/ethereum-signer/internal/types" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/base64" 8 | "os" 9 | "reflect" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | func TestProvideRSAKey(t *testing.T) { 15 | // Clean up environment variable after tests 16 | defer os.Unsetenv("RSA_PRIVATE_KEY") 17 | 18 | t.Run("ephemeral key generation", func(t *testing.T) { 19 | key, err := ProvideRSAKey(true) 20 | if err != nil { 21 | t.Errorf("Failed to generate ephemeral key: %v", err) 22 | } 23 | 24 | // Verify it's a valid RSA key by checking basic properties 25 | if key.Size() < 256 { 26 | t.Error("Generated key size is too small") 27 | } 28 | }) 29 | 30 | t.Run("ephemeral key generation", func(t *testing.T) { 31 | key, err := ProvideRSAKey(true) 32 | if err != nil { 33 | t.Errorf("Failed to generate ephemeral key: %v", err) 34 | } 35 | 36 | // Verify it's a different RSA key calling it the second time 37 | key2, err := ProvideRSAKey(true) 38 | if err != nil { 39 | t.Errorf("Failed to generate ephemeral key: %v", err) 40 | } 41 | if compareRSAPrivateKeys(key, key2) { 42 | t.Error("Generated keys are the same, expected different") 43 | } 44 | }) 45 | 46 | t.Run("non-ephemeral key with empty env", func(t *testing.T) { 47 | os.Unsetenv("RSA_PRIVATE_KEY") 48 | 49 | key, err := ProvideRSAKey(false) 50 | if err != nil { 51 | t.Errorf("Failed to handle empty environment: %v", err) 52 | } 53 | if key == nil { 54 | t.Error("Expected non-nil key when environment variable is not set") 55 | } 56 | 57 | retrievedKey, err := ProvideRSAKey(false) 58 | if err != nil { 59 | t.Errorf("Failed to retrieve key from environment: %v", err) 60 | } 61 | if retrievedKey == nil { 62 | t.Error("Expected non-nil key from environment") 63 | } 64 | 65 | // Verify the retrieved key matches the original 66 | if !compareRSAPrivateKeys(key, retrievedKey) { 67 | t.Errorf("Retrieved key does not match original key: key1/%v key2/%v", key, retrievedKey) 68 | } 69 | 70 | }) 71 | 72 | t.Run("non-ephemeral key with existing env", func(t *testing.T) { 73 | 74 | // Generate and store a test key in environment 75 | testKey, err := generateEphemeralRSAKey() 76 | if err != nil { 77 | t.Fatalf("Failed to generate test key: %v", err) 78 | } 79 | 80 | // Convert to DER and base64 encode 81 | derBytes := x509.MarshalPKCS1PrivateKey(testKey) 82 | encodedKey := base64.StdEncoding.EncodeToString(derBytes) 83 | 84 | // Set environment variable 85 | os.Setenv("RSA_PRIVATE_KEY", encodedKey) 86 | 87 | // Test key retrieval 88 | retrievedKey, err := ProvideRSAKey(false) 89 | if err != nil { 90 | t.Errorf("Failed to retrieve key from environment: %v", err) 91 | } 92 | if retrievedKey == nil { 93 | t.Error("Expected non-nil key from environment") 94 | } 95 | 96 | // Verify the retrieved key matches the original 97 | if !compareRSAPrivateKeys(testKey, retrievedKey) { 98 | t.Error("Retrieved key does not match original key") 99 | } 100 | }) 101 | 102 | t.Run("non-ephemeral key with invalid env value", func(t *testing.T) { 103 | os.Setenv("RSA_PRIVATE_KEY", "invalid-key-data") 104 | 105 | _, err := ProvideRSAKey(false) 106 | if err == nil { 107 | t.Error("Expected error with invalid environment variable data") 108 | } 109 | }) 110 | } 111 | 112 | func TestParsePlaintext(t *testing.T) { 113 | tests := []struct { 114 | name string 115 | input string 116 | want types.PlainKey 117 | wantErr bool 118 | errContains string 119 | }{ 120 | { 121 | name: "valid plaintext", 122 | input: base64.StdEncoding.EncodeToString([]byte(`{"secret": "secret", "eth_key": "eth_key"}`)), 123 | want: types.PlainKey{Secret: "secret", 124 | EthKey: "eth_key"}, 125 | wantErr: false, 126 | }, 127 | { 128 | name: "invalid base64", 129 | input: "invalid-base64!@#$", 130 | want: types.PlainKey{}, 131 | wantErr: true, 132 | errContains: "failed to decode kmsResultB64", 133 | }, 134 | { 135 | name: "invalid json", 136 | input: base64.StdEncoding.EncodeToString([]byte(`{invalid-json}`)), 137 | want: types.PlainKey{}, 138 | wantErr: true, 139 | errContains: "failed to unmarshal kmsResult", 140 | }, 141 | } 142 | 143 | for _, tt := range tests { 144 | t.Run(tt.name, func(t *testing.T) { 145 | got, err := ParsePlaintext(tt.input) 146 | 147 | if tt.wantErr { 148 | if err == nil { 149 | t.Errorf("ParsePlaintext() error = nil, wantErr %v", tt.wantErr) 150 | return 151 | } 152 | if !strings.Contains(err.Error(), tt.errContains) { 153 | t.Errorf("ParsePlaintext() error = %v, want error containing %v", err, tt.errContains) 154 | } 155 | return 156 | } 157 | 158 | if err != nil { 159 | t.Errorf("ParsePlaintext() unexpected error = %v", err) 160 | return 161 | } 162 | 163 | if !reflect.DeepEqual(got, tt.want) { 164 | t.Errorf("ParsePlaintext() = %v, want %v", got, tt.want) 165 | } 166 | }) 167 | } 168 | } 169 | 170 | // Helper function to compare two RSA private keys 171 | func compareRSAPrivateKeys(key1, key2 *rsa.PrivateKey) bool { 172 | // Compare modulus and private exponent 173 | return key1.N.Cmp(key2.N) == 0 && key1.D.Cmp(key2.D) == 0 174 | } 175 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/metrics/client.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "aws/ethereum-signer/internal/types" 5 | "encoding/json" 6 | "github.com/mackerelio/go-osstat/cpu" 7 | "github.com/mackerelio/go-osstat/memory" 8 | "github.com/mdlayher/vsock" 9 | log "github.com/sirupsen/logrus" 10 | "math" 11 | "time" 12 | ) 13 | 14 | type Client struct { 15 | cid uint32 16 | port uint32 17 | frequency time.Duration 18 | } 19 | 20 | func NewMetricsClient(cid uint32, port uint32, frequency time.Duration) *Client { 21 | return &Client{ 22 | cid: cid, 23 | port: port, 24 | frequency: frequency, 25 | } 26 | } 27 | 28 | func (mc *Client) Start() { 29 | go mc.logSystemStats() 30 | } 31 | 32 | func (mc *Client) monitorSystemCPU() (int, int, error) { 33 | before, err := cpu.Get() 34 | if err != nil { 35 | return 0.0, 0.0, err 36 | } 37 | time.Sleep(time.Duration(1) * time.Second) 38 | after, err := cpu.Get() 39 | if err != nil { 40 | return 0.0, 0.0, err 41 | } 42 | total := float64(after.Total - before.Total) 43 | cpuUser := int(math.Round(float64(after.User-before.User) / total * 100)) 44 | cpuSystem := int(math.Round(float64(after.System-before.System) / total * 100)) 45 | //log.Infof("cpuUser: %v, cpuSystem: %v", cpuUser, cpuSystem) 46 | 47 | return cpuUser, cpuSystem, nil 48 | } 49 | 50 | func (mc *Client) monitorSystemMemory() (int, int, error) { 51 | memory, err := memory.Get() 52 | if err != nil { 53 | return 0.0, 0.0, err 54 | } 55 | memoryUsed := int(math.Round(float64(memory.Used) / float64(memory.Total) * 100)) 56 | memoryCached := int(math.Round(float64(memory.Cached) / float64(memory.Total) * 100)) 57 | //log.Infof("memoryUsed: %v, memoryCached: %v", memoryUsed, memoryCached) 58 | 59 | return memoryUsed, memoryCached, nil 60 | } 61 | 62 | func (mc *Client) pushToMetricsServer(enclaveMetrics types.EnclaveSystemMetrics) error { 63 | metricsSerialized, err := json.Marshal(enclaveMetrics) 64 | if err != nil { 65 | return err 66 | } 67 | //log.Debugf("serialized metrics payload: %q", metricsSerialized) 68 | 69 | conn, err := vsock.Dial(mc.cid, mc.port, nil) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | _, err = conn.Write(metricsSerialized) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | err = conn.Close() 80 | if err != nil { 81 | return err 82 | } 83 | return nil 84 | } 85 | 86 | func (mc *Client) logSystemStats() { 87 | for range time.Tick(mc.frequency) { 88 | cpuUser, cpuSystem, err := mc.monitorSystemCPU() 89 | if err != nil { 90 | log.Errorf("error happened gathering cpu metrics: %s", err) 91 | } 92 | 93 | memoryUsed, memoryCached, err := mc.monitorSystemMemory() 94 | if err != nil { 95 | log.Errorf("error happened gethering memory metrics: %s", err) 96 | } 97 | 98 | // convert to int, no float precession required for CW metrics/alarms 99 | metrics := types.EnclaveSystemMetrics{ 100 | Timestamp: 0, 101 | CPUConsumptionUser: cpuUser, 102 | CPUConsumptionSystem: cpuSystem, 103 | MemoryUsed: memoryUsed, 104 | MemoryCached: memoryCached, 105 | } 106 | 107 | err = mc.pushToMetricsServer(metrics) 108 | if err != nil { 109 | log.Errorf("error happened pushing out metrics: %s\npayload: %v", err, metrics) 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/metrics/server.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | metricTypes "aws/ethereum-signer/internal/types" 5 | "encoding/json" 6 | "github.com/mdlayher/vsock" 7 | "github.com/prozz/aws-embedded-metrics-golang/emf" 8 | log "github.com/sirupsen/logrus" 9 | "net" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | PortOffset = 10 16 | ) 17 | 18 | type Server struct { 19 | cid uint32 20 | port uint32 21 | podName string 22 | nodeName string 23 | deploymentName string 24 | } 25 | 26 | func NewMetricsServer(cid uint32, port uint32) *Server { 27 | 28 | return &Server{ 29 | cid: cid, 30 | port: port, 31 | podName: os.Getenv("POD_NAME"), 32 | nodeName: os.Getenv("NODE_NAME"), 33 | deploymentName: strings.Join(strings.Split(os.Getenv("POD_NAME"), "-")[0:3], "-"), 34 | } 35 | } 36 | 37 | func (ms *Server) listen() (net.Listener, error) { 38 | var ln net.Listener 39 | var err error 40 | 41 | ln, err = vsock.ListenContextID(ms.cid, ms.port, nil) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return ln, nil 47 | } 48 | 49 | func (ms *Server) handleIncomingMetrics() error { 50 | ln, err := ms.listen() 51 | if err != nil { 52 | return err 53 | } 54 | 55 | log.Infof("Listening for incoming metrics on %v:%v", ms.cid, ms.port) 56 | 57 | go func() { 58 | for { 59 | inMetrics, err := ln.Accept() 60 | if err != nil { 61 | log.Errorf("error happened accepted incoming metrics push request: %s", err) 62 | continue 63 | } 64 | 65 | buf := make([]byte, 512) 66 | 67 | n, err := inMetrics.Read(buf) 68 | if err != nil { 69 | log.Errorf("exception happened reading from incoming connection: %s", err) 70 | } 71 | log.Infof("read buffer length: %v", n) 72 | log.Debugf("raw enclave metrics: %s", buf) 73 | 74 | enclaveSystemMetrics := metricTypes.EnclaveSystemMetrics{} 75 | 76 | err = json.Unmarshal(buf[:n], &enclaveSystemMetrics) 77 | if err != nil { 78 | log.Errorf("exception happened unmarshalling metrics payload: %s", err) 79 | continue 80 | } 81 | log.Debugf("unmarshaled enclave metrics payload: %v", enclaveSystemMetrics) 82 | 83 | ms.createEMFLogs( 84 | enclaveSystemMetrics.CPUConsumptionUser, 85 | enclaveSystemMetrics.CPUConsumptionSystem, 86 | enclaveSystemMetrics.MemoryUsed, 87 | enclaveSystemMetrics.MemoryCached, 88 | ) 89 | 90 | err = inMetrics.Close() 91 | if err != nil { 92 | log.Errorf("error happened closing incoming metrics push request: %s", err) 93 | } 94 | } 95 | }() 96 | return nil 97 | } 98 | 99 | func (ms *Server) Start() error { 100 | err := ms.handleIncomingMetrics() 101 | 102 | return err 103 | } 104 | 105 | func (ms *Server) createEMFLogs(enclaveCPUUser int, enclaveCPUSystem int, enclaveMemoryUsed int, enclaveMemoryCached int) { 106 | emf.New(emf.WithoutDimensions()).Namespace("NitroEnclave").DimensionSet( 107 | emf.NewDimension("NodeName", ms.nodeName), 108 | emf.NewDimension("Deployment", ms.deploymentName), 109 | emf.NewDimension("Pod", ms.podName)). 110 | MetricsAs(map[string]int{ 111 | "enclave_memory_utilization_used": enclaveMemoryUsed, 112 | "enclave_memory_utilization_cached": enclaveMemoryCached, 113 | "enclave_cpu_utilization_user": enclaveCPUUser, 114 | "enclave_cpu_utilization_system": enclaveCPUSystem}, emf.Percent).Log() 115 | } 116 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/pod/config.go: -------------------------------------------------------------------------------- 1 | package pod 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | type Config struct { 10 | LogLevel string 11 | CertFile string 12 | KeyFile string 13 | ListenAddress string 14 | EnclaveCID uint64 15 | EnclavePort uint64 16 | } 17 | 18 | func LoadConfig() (*Config, error) { 19 | config := &Config{ 20 | LogLevel: os.Getenv("LOG_LEVEL"), 21 | CertFile: os.Getenv("CERT_FILE"), 22 | KeyFile: os.Getenv("KEY_FILE"), 23 | ListenAddress: os.Getenv("LISTEN_ADDRESS"), 24 | } 25 | 26 | // Validate required fields 27 | requiredEnvs := map[string]string{ 28 | "CERT_FILE": config.CertFile, 29 | "KEY_FILE": config.KeyFile, 30 | "LISTEN_ADDRESS": config.ListenAddress, 31 | } 32 | 33 | for env, value := range requiredEnvs { 34 | if value == "" { 35 | return nil, fmt.Errorf("%s cannot be empty", env) 36 | } 37 | } 38 | 39 | // Parse Enclave CID 40 | enclaveCID := os.Getenv("ENCLAVE_CID") 41 | if enclaveCID == "" { 42 | return nil, fmt.Errorf("ENCLAVE_CID cannot be empty") 43 | } 44 | cid, err := strconv.ParseUint(enclaveCID, 10, 32) 45 | if err != nil { 46 | return nil, fmt.Errorf("failed to parse ENCLAVE_CID: %w", err) 47 | } 48 | config.EnclaveCID = cid 49 | 50 | // Parse Enclave Port 51 | enclavePort := os.Getenv("VSOCK_BASE_PORT") 52 | if enclavePort == "" { 53 | return nil, fmt.Errorf("VSOCK_BASE_PORT cannot be empty") 54 | } 55 | port, err := strconv.ParseUint(enclavePort, 10, 32) 56 | if err != nil { 57 | return nil, fmt.Errorf("failed to parse VSOCK_BASE_PORT: %w", err) 58 | } 59 | config.EnclavePort = port 60 | 61 | return config, nil 62 | } 63 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/pod/credentials.go: -------------------------------------------------------------------------------- 1 | package pod 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/service/sts" 9 | "github.com/aws/aws-sdk-go-v2/service/sts/types" 10 | log "github.com/sirupsen/logrus" 11 | "os" 12 | ) 13 | 14 | func GetAWSWebIdentityCredentials(config *aws.Config, sessionName string) (*types.Credentials, error) { 15 | if config == nil { 16 | return &types.Credentials{}, errors.New("error happened validating config - value cannot be nil") 17 | } 18 | stsClient := sts.NewFromConfig(*config) 19 | 20 | webIdentityTokenFileLocation := os.Getenv("AWS_WEB_IDENTITY_TOKEN_FILE") 21 | if len(webIdentityTokenFileLocation) == 0 { 22 | return &types.Credentials{}, errors.New("error happened looking up AWS_WEB_IDENTITY_TOKEN_FILE environment variable - value cannot be nil") 23 | } 24 | 25 | awsRoleArn := os.Getenv("AWS_ROLE_ARN") 26 | if len(awsRoleArn) == 0 { 27 | return &types.Credentials{}, errors.New("error happened looking up AWS_ROLE_ARN environment variable - value cannot be empty") 28 | } 29 | 30 | log.Debugf("loading webIdentityTokenFile from %s", webIdentityTokenFileLocation) 31 | webIdentityTokenFile, err := os.ReadFile(webIdentityTokenFileLocation) // #nosec G304 32 | if err != nil { 33 | return &types.Credentials{}, err 34 | } 35 | 36 | roleSessionName := sessionName 37 | webIdentityTokenStr := bytes.NewBuffer(webIdentityTokenFile).String() 38 | durationSeconds := int32(900) 39 | 40 | webIdentityToken, err := stsClient.AssumeRoleWithWebIdentity(context.TODO(), &sts.AssumeRoleWithWebIdentityInput{ 41 | RoleArn: &awsRoleArn, 42 | RoleSessionName: &roleSessionName, 43 | WebIdentityToken: &webIdentityTokenStr, 44 | DurationSeconds: &durationSeconds, 45 | }) 46 | if err != nil { 47 | return &types.Credentials{}, err 48 | } 49 | 50 | return webIdentityToken.Credentials, nil 51 | } 52 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/types/kms.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "crypto/x509/pkix" 5 | "encoding/asn1" 6 | ) 7 | 8 | type ContentInfo struct { 9 | ContentType asn1.ObjectIdentifier 10 | Content asn1.RawValue `asn1:"explicit,optional,tag:0"` 11 | } 12 | 13 | type KeyTransRecipientInfo struct { 14 | Version int 15 | Rid asn1.RawValue `asn1:"choice"` 16 | KeyEncryptionAlgorithm pkix.AlgorithmIdentifier 17 | EncryptedKey []byte 18 | } 19 | 20 | type EnvelopedDataStruct struct { 21 | Version int 22 | RecipientInfos []KeyTransRecipientInfo `asn1:"set"` 23 | EncryptedContentInfo struct { 24 | ContentType asn1.ObjectIdentifier 25 | ContentEncryptionAlgorithm pkix.AlgorithmIdentifier 26 | EncryptedContent []byte `asn1:"explicit,tag:0"` 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/types/metrics.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type EnclaveSystemMetrics struct { 4 | Timestamp int `json:"Timestamp"` 5 | CPUConsumptionUser int `json:"CPUConsumptionUser"` 6 | CPUConsumptionSystem int `json:"CPUConsumptionSystem"` 7 | MemoryUsed int `json:"MemoryUsed"` 8 | MemoryCached int `json:"MemoryCached"` 9 | } 10 | -------------------------------------------------------------------------------- /applications/ethereum-signer/internal/types/signing.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | SPDX-License-Identifier: MIT-0 5 | */ 6 | 7 | package types 8 | 9 | type SecretNotFoundError struct { 10 | Err error 11 | } 12 | 13 | func (e *SecretNotFoundError) Error() string { 14 | return e.Err.Error() 15 | } 16 | 17 | type TransactionPayload struct { 18 | Value float32 `json:"value,omitempty"` 19 | To string `json:"to" validate:"required"` 20 | Nonce int `json:"nonce" validate:"gte=0"` 21 | Type int `json:"type" validate:"required"` 22 | ChainID int `json:"chainId" validate:"required"` 23 | Gas int `json:"gas" validate:"required"` 24 | MaxFeePerGas int `json:"maxFeePerGas" validate:"required"` 25 | MaxPriorityFeePerGas int `json:"maxPriorityFeePerGas" validate:"required"` 26 | Data string `json:"data,omitempty"` 27 | } 28 | 29 | type UserOpPayload struct { 30 | UserOpHash string `json:"userOpHash" validate:"len=66"` 31 | } 32 | 33 | type SigningRequest struct { 34 | TransactionPayload map[string]interface{} `json:"transaction_payload" validate:"required"` 35 | KeyID string `json:"key_id" validate:"len=36"` 36 | Timestamp int `json:"timestamp" validate:"required"` 37 | HMAC string `json:"hmac" validate:"required"` 38 | } 39 | 40 | // https://stackoverflow.com/a/68806602 41 | type PlainKey struct { 42 | Secret string `json:"secret" validate:"min=36"` 43 | EthKey string `json:"eth_key" validate:"omitempty,len=64"` 44 | } 45 | 46 | type AWSCredentials struct { 47 | AccessKeyID string `json:"access_key_id"` 48 | SecretAccessKey string `json:"secret_access_key"` 49 | Token string `json:"token"` 50 | } 51 | 52 | type EnclaveSigningPayload struct { 53 | Credential AWSCredentials `json:"credential" validate:"required"` 54 | TransactionPayload map[string]interface{} `json:"transaction_payload" validate:"required"` 55 | EncryptedKey string `json:"encrypted_key" validate:"required"` 56 | Timestamp int `json:"timestamp" validate:"required"` 57 | HMAC string `json:"hmac" validate:"required"` 58 | } 59 | 60 | type EnclaveKeyGenerationPayload struct { 61 | Credential AWSCredentials `json:"credential" validate:"required"` 62 | Secret string `json:"secret" validate:"min=36"` 63 | SecretsTable string `json:"secrets_table" validate:"required"` 64 | KeyARN string `json:"key_arn" validate:"required"` 65 | } 66 | 67 | type SignedTransaction struct { 68 | TxHash string `json:"tx_hash,omitempty"` 69 | SignedTX string `json:"tx_signed,omitempty"` 70 | Signature string `json:"signature,omitempty"` 71 | Error string `json:"error,omitempty"` 72 | } 73 | 74 | type EnclaveResult struct { 75 | Status int `json:"status"` 76 | Body interface{} `json:"body"` 77 | } 78 | 79 | type Ciphertext struct { 80 | KeyID string `dynamodbav:"key_id" json:"key_id"` 81 | Ciphertext string `dynamodbav:"ciphertext" json:"ciphertext,omitempty"` 82 | Address string `dynamodbav:"address" json:"address,omitempty"` 83 | } 84 | 85 | type UserRequest struct { 86 | Operation string `json:"operation" validate:"required"` 87 | Payload interface{} `json:"payload" validate:"required"` 88 | } 89 | 90 | type UserResponse struct { 91 | EnclaveStatus int `json:"enclave_status"` 92 | EnclaveResult interface{} `json:"enclave_result"` 93 | } 94 | 95 | type UserSigningRequest struct { 96 | TransactionPayload map[string]interface{} `json:"transaction_payload" validate:"required"` 97 | KeyID string `json:"key_id" validate:"len=36"` 98 | Secret string `json:"secret" validate:"min=36"` 99 | } 100 | -------------------------------------------------------------------------------- /assets/deployment_confirmation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nitro-enclave-blockchain-wallet-on-eks/aea0153b104075fe90531e92140f166ad44c562a/assets/deployment_confirmation.png -------------------------------------------------------------------------------- /assets/deployment_output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nitro-enclave-blockchain-wallet-on-eks/aea0153b104075fe90531e92140f166ad44c562a/assets/deployment_output.png -------------------------------------------------------------------------------- /assets/eks_enclaves.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nitro-enclave-blockchain-wallet-on-eks/aea0153b104075fe90531e92140f166ad44c562a/assets/eks_enclaves.png -------------------------------------------------------------------------------- /assets/eks_enclaves_key_generation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nitro-enclave-blockchain-wallet-on-eks/aea0153b104075fe90531e92140f166ad44c562a/assets/eks_enclaves_key_generation.png -------------------------------------------------------------------------------- /assets/eks_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nitro-enclave-blockchain-wallet-on-eks/aea0153b104075fe90531e92140f166ad44c562a/assets/eks_overview.png -------------------------------------------------------------------------------- /assets/eks_overview_v2-EKS_Cluster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nitro-enclave-blockchain-wallet-on-eks/aea0153b104075fe90531e92140f166ad44c562a/assets/eks_overview_v2-EKS_Cluster.png -------------------------------------------------------------------------------- /assets/eks_overview_v2-Web3_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nitro-enclave-blockchain-wallet-on-eks/aea0153b104075fe90531e92140f166ad44c562a/assets/eks_overview_v2-Web3_app.png -------------------------------------------------------------------------------- /assets/eks_overview_v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nitro-enclave-blockchain-wallet-on-eks/aea0153b104075fe90531e92140f166ad44c562a/assets/eks_overview_v2.png -------------------------------------------------------------------------------- /assets/key_generation_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nitro-enclave-blockchain-wallet-on-eks/aea0153b104075fe90531e92140f166ad44c562a/assets/key_generation_flow.png -------------------------------------------------------------------------------- /assets/metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nitro-enclave-blockchain-wallet-on-eks/aea0153b104075fe90531e92140f166ad44c562a/assets/metrics.png -------------------------------------------------------------------------------- /assets/signing_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nitro-enclave-blockchain-wallet-on-eks/aea0153b104075fe90531e92140f166ad44c562a/assets/signing_flow.png -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "python3 app.py", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "requirements*.txt", 11 | "source.bat", 12 | "**/__init__.py", 13 | "python/__pycache__", 14 | "tests" 15 | ] 16 | }, 17 | "context": { 18 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 19 | "@aws-cdk/core:checkSecretUsage": true, 20 | "@aws-cdk/core:target-partitions": [ 21 | "aws", 22 | "aws-cn" 23 | ], 24 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 25 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 26 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 29 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 30 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 31 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 32 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 33 | "@aws-cdk/core:enablePartitionLiterals": true, 34 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 35 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 36 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /eks_nitro_wallet/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nitro-enclave-blockchain-wallet-on-eks/aea0153b104075fe90531e92140f166ad44c562a/eks_nitro_wallet/__init__.py -------------------------------------------------------------------------------- /eks_nitro_wallet/eks_utils.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import yaml 3 | from aws_cdk import aws_ec2 as ec2, aws_eks as eks 4 | 5 | 6 | def create_private_link(self, vpc, services): 7 | for service in services: 8 | if service in ["DYNAMODB", "S3"]: 9 | service_gateway = getattr(ec2.GatewayVpcEndpointAwsService, service) 10 | vpc.add_gateway_endpoint( 11 | "{}GatewayEndpoint".format(service), service=service_gateway 12 | ) 13 | else: 14 | service_endpoint = getattr(ec2.InterfaceVpcEndpointAwsService, service) 15 | ec2.InterfaceVpcEndpoint( 16 | self, 17 | "{}InterfaceEndpoint".format(service), 18 | vpc=vpc, 19 | subnets=ec2.SubnetSelection( 20 | subnet_type=ec2.SubnetType.PRIVATE_WITH_EGRESS 21 | ), 22 | service=service_endpoint, 23 | private_dns_enabled=True, 24 | ) 25 | 26 | 27 | def apply_k8s_remote_manifest( 28 | self, cluster: eks.ICluster, manifest_url: str, manifest_name: str 29 | ) -> None: 30 | # Fetch the manifest from remote URL 31 | try: 32 | response = requests.get(manifest_url) 33 | response.raise_for_status() 34 | manifest_content = response.text 35 | 36 | # Parse all YAML documents in the manifest 37 | manifests = list(yaml.safe_load_all(manifest_content)) 38 | 39 | # Apply the manifest to the cluster 40 | eks.KubernetesManifest( 41 | self, f"{manifest_name}Manifest", cluster=cluster, manifest=manifests 42 | ) 43 | except requests.exceptions.RequestException as e: 44 | print(f"Failed to fetch manifest from {manifest_url}: {e}") 45 | raise 46 | except yaml.YAMLError as e: 47 | print(f"Failed to parse manifest YAML: {e}") 48 | raise 49 | 50 | 51 | def create_k8s_namespace( 52 | self, name: str, cluster: eks.ICluster, override: bool = True 53 | ) -> eks.KubernetesManifest: 54 | return eks.KubernetesManifest( 55 | self, 56 | f"{name}NamespaceManifest", 57 | cluster=cluster, 58 | overwrite=override, 59 | manifest=[ 60 | { 61 | "apiVersion": "v1", 62 | "kind": "Namespace", 63 | "metadata": { 64 | "name": name, 65 | }, 66 | } 67 | ], 68 | ) 69 | -------------------------------------------------------------------------------- /lib/docker/Dockerfile_device_plugin: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Build the device plugin from sources 5 | 6 | FROM golang:1.22 as builder 7 | 8 | WORKDIR /build_dir/ 9 | COPY *.go ./ 10 | COPY go.sum ./ 11 | ENV GOPROXY=direct 12 | RUN go mod init k8s-ne-device-plugin && \ 13 | go mod tidy && \ 14 | go mod vendor 15 | 16 | RUN CGO_ENABLED=0 go build -a -ldflags='-s -w -extldflags="-static"' . 17 | 18 | # Create a bare minumum image that only contains the device plugin binary. 19 | 20 | FROM scratch as device_plugin 21 | 22 | COPY --from=builder /build_dir/k8s-ne-device-plugin /usr/bin/k8s-ne-device-plugin 23 | 24 | CMD ["/usr/bin/k8s-ne-device-plugin","-logtostderr=true","-v=0"] 25 | -------------------------------------------------------------------------------- /lib/docker/Dockerfile_device_plugin_amazon_linux_2023: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | # Build the device plugin from sources 5 | 6 | FROM amazonlinux:2023 as builder 7 | 8 | RUN dnf install tar gzip git -y 9 | RUN curl -LO https://go.dev/dl/go1.22.3.linux-amd64.tar.gz 10 | RUN tar -C /usr/local -xzf go1.22.3.linux-amd64.tar.gz 11 | ENV PATH=$PATH:/usr/local/go/bin 12 | 13 | WORKDIR /build_dir/ 14 | COPY *.go ./ 15 | COPY go.sum ./ 16 | ENV GOPROXY=direct 17 | RUN go mod init k8s-ne-device-plugin && \ 18 | go mod tidy && \ 19 | go mod vendor 20 | 21 | RUN CGO_ENABLED=0 go build -a -ldflags='-s -w -extldflags="-static"' . 22 | 23 | # Create a bare minumum image that only contains the device plugin binary. 24 | 25 | FROM scratch as device_plugin 26 | 27 | COPY --from=builder /build_dir/k8s-ne-device-plugin /usr/bin/k8s-ne-device-plugin 28 | 29 | CMD ["/usr/bin/k8s-ne-device-plugin","-logtostderr=true","-v=0"] 30 | -------------------------------------------------------------------------------- /lib/docker/Dockerfile_go_base: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | ## build 5 | FROM golang:1.22.1-bullseye AS go_base 6 | ARG TARGETARCH 7 | WORKDIR /app 8 | 9 | ENV GOPROXY=direct 10 | COPY go.mod go.sum ./ 11 | RUN --mount=type=cache,target=/go/pkg/mod,id=${TARGETARCH} \ 12 | --mount=type=cache,target=/root/.cache/go-build,id=${TARGETARCH} \ 13 | go mod download 14 | 15 | ARG SKIP_TEST_ARG 16 | 17 | # can be made optional if required 18 | #RUN if [ "$SKIP_TEST_ARG" != "true" ]; then go install honnef.co/go/tools/cmd/staticcheck@latest && \ 19 | # go install github.com/securego/gosec/v2/cmd/gosec@latest; fi 20 | RUN --mount=type=cache,target=/go/pkg/mod,id=${TARGETARCH} \ 21 | --mount=type=cache,target=/root/.cache/go-build,id=${TARGETARCH} \ 22 | go install honnef.co/go/tools/cmd/staticcheck@latest 23 | RUN --mount=type=cache,target=/go/pkg/mod,id=${TARGETARCH} \ 24 | --mount=type=cache,target=/root/.cache/go-build,id=${TARGETARCH} \ 25 | go install github.com/securego/gosec/v2/cmd/gosec@latest 26 | 27 | FROM go_base AS go_eks_base 28 | FROM go_base AS go_lambda_base 29 | -------------------------------------------------------------------------------- /lib/docker/Dockerfile_nitro-cli_build: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | FROM amazonlinux:2023 as nitro-cli_build_image 5 | RUN dnf install aws-nitro-enclaves-cli aws-nitro-enclaves-cli-devel -y 6 | WORKDIR /app 7 | COPY /build/nitro_cli/release/nitro-cli /usr/bin 8 | -------------------------------------------------------------------------------- /lib/docker/Dockerfile_pod: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | # todo upgrade to al 2023 5 | # yum to be replace by dnf 6 | FROM amazonlinux:2 as build_image 7 | 8 | RUN amazon-linux-extras install aws-nitro-enclaves-cli && \ 9 | yum install aws-nitro-enclaves-cli-devel jq -y 10 | 11 | ######## app image ######## 12 | FROM build_image as pod_image 13 | 14 | RUN yum install python3 awscli jq -y 15 | -------------------------------------------------------------------------------- /lib/k8s_templates/deployment_spec_template.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: MIT-0 3 | 4 | apiVersion: apps/v1 5 | kind: Deployment 6 | metadata: 7 | name: