diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index fc53f36ce..000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,227 +0,0 @@ ---- -version: 2.1 - -orbs: - slack: circleci/slack@3.4.2 - -executors: - executor_med: # 2cpu, 4G ram - docker: - - image: circleci/openjdk:11.0.4-jdk-stretch - resource_class: medium - working_directory: ~/project - environment: - JAVA_TOOL_OPTIONS: -Xmx2048m - GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.workers.max=2 -Xmx2048m - - executor_large: # 4cpu, 8G ram - docker: - - image: circleci/openjdk:11.0.4-jdk-stretch - resource_class: large - working_directory: ~/project - environment: - JAVA_TOOL_OPTIONS: -Xmx4096m - GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.workers.max=4 -Xmx4096m - - executor_machine: # 2cpu , 8G ram - machine: - image: ubuntu-1604:201903-01 #Ubuntu 16.04, docker 18.09.3, docker-compose 1.23.1 - docker_layer_caching: true - working_directory: ~/project - environment: - JAVA_TOOL_OPTIONS: -Xmx4096m - GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.workers.max=2 -Xmx4096m - -commands: - prepare: - description: "Prepare" - steps: - - checkout - - restore_cache: - name: Restore cached gradle dependencies - keys: - - deps-{{ checksum "build.gradle" }}-{{ .Branch }}-{{ .Revision }} - - deps-{{ checksum "build.gradle" }} - - deps- - - capture_test_results: - description: "Capture test results" - steps: - - run: - name: Gather test results - when: always - command: | - FILES=`find . -name test-results` - for FILE in $FILES - do - MODULE=`echo "$FILE" | sed -e 's@./\(.*\)/build/test-results@\1@'` - TARGET="build/test-results/$MODULE" - mkdir -p "$TARGET" - cp -rf ${FILE}/*/* "$TARGET" - done - - store_test_results: - path: build/test-results - - capture_test_reports: - description: "Capture test reports" - steps: - - run: - name: Gather test results - when: always - command: | - FILES=`find . -name reports` - for FILE in $FILES - do - MODULE=`echo "$FILE" | sed -e 's@./\(.*\)/build/reports@\1@'` - TARGET="build/test-reports/$MODULE" - mkdir -p "$TARGET" - cp -rf ${FILE}/*/* "$TARGET" - done - - store_artifacts: - path: build/test-reports - destination: test-reports - - notify: - description: "Notify Slack" - steps: - - slack/status: - fail_only: true - only_for_branches: 'master' - -jobs: - build: - executor: executor_large - steps: - - prepare - - run: - name: Build - command: | - ./gradlew --no-daemon --parallel build - - run: - name: Test - no_output_timeout: 20m - command: | - ./gradlew --no-daemon --parallel test - - run: - name: Integration Test - no_output_timeout: 20m - command: | - ./gradlew --no-daemon --parallel integrationTest --info - - notify - - capture_test_results - - capture_test_reports - - save_cache: - name: Caching gradle dependencies - key: deps-{{ checksum "build.gradle" }}-{{ .Branch }}-{{ .Revision }} - paths: - - .gradle - - ~/.gradle - - persist_to_workspace: - root: ~/project - paths: - - ./ - - acceptanceTests: - executor: executor_large - steps: - - prepare - - run: - name: Acceptance Test - no_output_timeout: 20m - command: | - ./gradlew --no-daemon --parallel acceptanceTest - - notify - - capture_test_results - - capture_test_reports - - buildDocker: - executor: executor_med - steps: - - prepare - - setup_remote_docker - - attach_workspace: - at: ~/project - - run: - name: hadoLint - command: | - docker run --rm -i hadolint/hadolint < docker/Dockerfile - - run: - name: build image - command: | - ./gradlew --no-daemon distDocker - - run: - name: test image - command: | - mkdir -p docker/reports - ./gradlew --no-daemon testDocker - - notify - - publish: - executor: executor_med - steps: - - prepare - - attach_workspace: - at: ~/project - - run: - name: Publish - command: | - ./gradlew --no-daemon --parallel bintrayUpload - - notify - - publishDocker: - executor: executor_med - steps: - - prepare - - setup_remote_docker - - attach_workspace: - at: ~/project - - run: - name: Publish Docker - command: | - docker login --username "${DOCKER_USER}" --password "${DOCKER_PASSWORD}" - ./gradlew --no-daemon --parallel "-Pbranch=${CIRCLE_BRANCH}" dockerUpload - - notify - -workflows: - version: 2 - nightly: - triggers: - - schedule: - cron: "0 11 * * *" - filters: - branches: - only: - - master - jobs: - - build - - acceptanceTests: - requires: - - build - default: - jobs: - - build - - acceptanceTests: - requires: - - build - - buildDocker: - requires: - - build - - publish: - filters: - branches: - only: - - master - - /^release-.*/ - requires: - - build - - acceptanceTests - - publishDocker: - filters: - branches: - only: - - master - - /^release-.*/ - requires: - - build - - acceptanceTests - - buildDocker diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 1fc2ea4b8..000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,90 +0,0 @@ -# Changelog - -## 0.7.1 - -### Features Added -- Support for using config file and environment variables as default values for cli options -- Updated signers library to the latest version -- Accessing Azure signing service requires tenant id as part of Azure configuration -- Communication details moved to Discord - -### Bugs Fixed -- Prevent multiple transmission exceptions propagation upwards [#312](https://github.com/PegaSysEng/ethsigner/pull/312) -- Resolve failures in the application of CORS headers [#286](https://github.com/PegaSysEng/ethsigner/pull/286) - -## 0.7.0 - -### Features Added -- Added "eth_sign" JSON RPC -- Added "--http-cors-origins" commandline option to allow browser based apps (remix/metamask) to connect to EthSigner -- Added "--downstream-http-path" commandline option to allow Ethsigner to connect to a downstream web3 provider not on root path (eg web3 provider running in infura) -- If inbound request contains the "Host" header, it is renamed to "X-Forwarded-Host" and added to downstream request -- Code base split, crypto operations moved to "Signers" [repository](https://github.com/PegaSysEng/signers) -- First line of Password file (stripping EOL) is treated as the password (rather than whole file content) - -### Bugs Fixed -- Create invalid signature when Signature field was treated as negative BigInteger [#247](https://github.com/PegaSysEng/ethsigner/issues/247) - -## 0.6.0 - -Changed CLI option name from `--downstream-http-tls-ca-auth-disabled` to `--downstream-http-tls-ca-auth-enabled` https://github.com/PegaSysEng/ethsigner/pull/230 - -## 0.5.0 - -### Features Added -- [Added TLS support for incoming and outgoing RPC endpoints](https://docs.ethsigner.pegasys.tech/en/latest/Concepts/TLS/) -- [Added TLS support for connecting to Hashicorp vault](https://docs.ethsigner.pegasys.tech/en/latest/Concepts/TLS/) -- Upgraded PicoCLI to 4.1.4 - -### Bugs Fixed -- Received headers are now forwarded to the web3 provider, resolving an issue where JWT token was not being passed in header https://github.com/PegaSysEng/ethsigner/pull/208 -- Resolved an issue where private transactions using privacyGroupId without a nonce failed https://github.com/PegaSysEng/ethsigner/pull/215 - -## 0.4.0 - -### Features Added -- Multi-key signing: Ethsigner is initialised with a directory containing a number of TOML metadata files, each of which describe a key which may be used for signing. Upon reception of a Transaction, Ethsigner loads the corresponding metadata file, and signs the Transaction with the key defined therein. -- Relaxed definition of 'optional' when parsing eth_SendTransaction (empty string, null an "0x" are deemed a missing optional parameter). -- All endpoints (not just "/") are proxied to the downstream web3j provider (eg. "/login") -- CI moved from Jenkins to CircleCI -- Updated to Web3j 4.5.5 -- Updated to JUnit 5 - -### Bugs Fixed -- When a private transaction is submitted without a nonce, a nonce is generated and inserted. However, if the supplied nonce is too low, the transaction is not resubmitted with a new nonce. Rather an error is returned to the caller (resolved in Besu 1.2.5). -- Removed intermittent "out of memory" failure during integration testing. -- Resolved an issue whereby a missing optional field in eth_SendTransaction would fail - -## 0.3.0 - -### Known Issues -- When a private transaction is submitted without a nonce, a nonce is generated and inserted. However, if the supplied nonce is too low, the transaction is not resubmitted with a new nonce. Rather an error is returned to the caller. - -### Features Added -- Updated to use Web3j 4.5.0 -- Accepts Private Transactions addressed with "PrivacyGroupId", not just "PrivateFor" - -### Bugs Fixed -- Private Transactions without nonces are now accepted and the nonce populated (see "Known Issues") - -## 0.2.0 - -### Known Issues -- When a private transaction is submitted without a nonce, then transaction will be rejected. Ethsigner is unable to derive an appropriate nonce for a private transaction, as such the `nonce` field of `eea_SendTransaction` is mandatory - if a private transaction is submitted without a nonce an error will be returned. DApps can use the [`priv_getTransactionCount`]( (https://docs.pantheon.pegasys.tech/en/latest/Reference/Pantheon-API-Methods/#priv_gettransactioncount)) JSON RPC to determine the correct nonce prior to transaction transmission. - -### Breaking Changes -- Command line reworked to specify the source of the key used for transaction signing. -- EthSigner is supported on Java 11+ only; Java 8 is no longer supported. - -### Features Added -- Created [EthSigner documentation](https://docs.ethsigner.pegasys.tech/en/latest/) -- Allow EthSigner to be deployed as a Docker image -- Support signing transaction with a key stored in an Azure KeyVault \(cloud based software/HSM signing service\) (thanks to [jimthematrix](https://github.com/jimthematrix)) -- Added an Upcheck endpoint -- Support signing transactions with a key stored in a Hashicorp vault -- Sign private transaction submitted via eea_SendTransaction -- Jar files are available from the EthSigner bintray repository. - -### Bugs Fixed -- N/A - diff --git a/CLA.md b/CLA.md deleted file mode 100644 index 806961db7..000000000 --- a/CLA.md +++ /dev/null @@ -1,19 +0,0 @@ -Sign the CLA -============= - -This page is the step-by-step guide to signing the Consensys AG -Individual Contributor License Agreement. - -1. First and foremost, read [the current version of the CLA]. - It is written to be as close to plain English as possible. - -2. Make an account on [GitHub] if you don't already have one. - -3. After creating your first pull request, you will see a merge - pre-requisite requiring to you read and sign the CLA. - -If you have any questions, you can reach us on [Discord]. - -[GitHub]: https://github.com/ -[the current version of the CLA]: https://gist.github.com/rojotek/978b48a5e8b68836856a8961d6887992 -[Discord]: https://discord.gg/5U9Jwp7 diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md deleted file mode 100644 index dfd1c60cc..000000000 --- a/CODE-OF-CONDUCT.md +++ /dev/null @@ -1,74 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at [private@pegasys.tech]. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[Contributor Covenant]: https://www.contributor-covenant.org -[private@pegasys.tech]: mailto:private@pegasys.tech \ No newline at end of file diff --git a/CODING-CONVENTIONS.md b/CODING-CONVENTIONS.md index 2a58509d2..dfb2e4a57 100644 --- a/CODING-CONVENTIONS.md +++ b/CODING-CONVENTIONS.md @@ -45,7 +45,7 @@ EthSigner embraces typical Java idioms including using an Object Oriented approa - Don't pass lambdas into executors because it makes it harder to identify the threading interactions. The lambda makes the code shorter but not clearer. Instead use a separate class or extract a method. * For good examples, refer to the APIs the JDK itself exposes. ->**Note** If you're not sure what idiomatic Java looks like, start by following the typical patterns and naming used in this or other PegaSys codebases. +>**Note** If you're not sure what idiomatic Java looks like, start by following the typical patterns and naming used in this or other ConsenSys codebases. ## 2.3 You Ain't Gonna Need It (YAGNI) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d10bf36cf..be3b71524 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,7 +27,7 @@ and feel free to propose changes to this document in a pull request. ## Code of Conduct This project and everyone participating in it is governed by the [EthSigner Code of Conduct](CODE-OF-CONDUCT.md). -By participating, you are expected to uphold this code. Please report unacceptable behavior to [private@pegasys.tech]. +By participating, you are expected to uphold this code. Please report unacceptable behavior to [private-quorum@consensys.net]. ## I just have a quick question @@ -242,7 +242,7 @@ These are not strictly enforced during the build, but should be adhered to and c | [`requires-changes`][search-label-requires-changes] | Pull requests which need to be updated based on review comments and then reviewed again. | | [`needs engineering approval`][search-label-needs-engineering-approval] | Pull requests which need to be approved from a technical person, mainly documentation PRs. | -[private@pegasys.tech]: mailto:private@pegasys.tech +[private-quorum@consensys.net]: mailto:private-quorum@consensys.net [Discord]: https://discord.gg/5U9Jwp7 [Github issues]: https://github.com/PegaSysEng/ethsigner/issues [EthSigner documentation]: https://docs.ethsigner.pegasys.tech/ diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 2206efdee..d0693e382 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1,5 +1,5 @@ # Overview -This project is led by a benevolent dictator (PegaSys) and managed by the community. That is, the +This project is led by a benevolent dictator (ConsenSys) and managed by the community. That is, the community actively contributes to the day-to-day maintenance of the project, but the general strategic line is drawn by the benevolent dictator. In case of disagreement, they have the last word. It is the benevolent dictator’s job to resolve disputes within the community and to ensure that the diff --git a/README.md b/README.md index 089d6263e..6fd25f139 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # EthSigner -A transaction signing application to be used with a web3 provider. All questions, queries and other discussion can be found on [Discord]. +A transaction signing application to be used with a web3 provider. All questions, queries and other discussion can be found on [Discord]. ## Issues @@ -11,6 +11,9 @@ See our [contribution guidelines](CONTRIBUTING.md) for more detail on searching ## Users * [User documentation](https://docs.ethsigner.pegasys.tech/) +## Chat +* [Discord] + ## Developers * [Contribution Guidelines](CONTRIBUTING.md) * [Coding Conventions](CODING-CONVENTIONS.md) diff --git a/acceptance-tests/build.gradle b/acceptance-tests/build.gradle index 5c0f4cd1d..35eb395be 100644 --- a/acceptance-tests/build.gradle +++ b/acceptance-tests/build.gradle @@ -29,6 +29,8 @@ dependencies { testImplementation project(':ethsigner:app') testImplementation (group: 'tech.pegasys.signers.internal', name: 'signing-secp256k1-impl', classifier: 'test-fixtures') testImplementation (group: 'tech.pegasys.signers.internal', name: 'keystorage-hashicorp', classifier: 'test-fixtures') + testImplementation (group: 'tech.pegasys.signers.internal', name: 'keystorage-hsm', classifier: 'test-fixtures') + testImplementation (group: 'tech.pegasys.signers.internal', name: 'keystorage-cavium', classifier: 'test-fixtures') testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'io.vertx:vertx-core' diff --git a/build.gradle b/build.gradle index 072b4ac57..e30154a4e 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,8 @@ if (!JavaVersion.current().java11Compatible) { " Detected version ${JavaVersion.current()}") } +def dockerRepo = "adharatech" + def bintrayUser = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') def bintrayKey = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_KEY') def bintrayPackage = bintray.pkg { @@ -119,8 +121,16 @@ allprojects { repositories { jcenter() + mavenLocal() mavenCentral() - maven { url "https://consensys.bintray.com/pegasys-repo" } + //maven { url "https://consensys.bintray.com/pegasys-repo" } + maven { + url "https://adhara.jfrog.io/artifactory/maven" + credentials { + username = System.getenv('ARTIFACTORY_USER') + password = System.getenv('ARTIFACTORY_PASSWORD') + } + } } dependencies { errorprone("com.google.errorprone:error_prone_core") } @@ -210,6 +220,7 @@ allprojects { jvmArgs = [ '-Xmx4g', '-XX:-UseGCOverheadLimit', + "-Djava.library.path=/opt/cloudhsm/lib", // Mockito and jackson-databind do some strange reflection during tests. // This suppresses an illegal access warning. '--add-opens', @@ -341,6 +352,7 @@ subprojects { dependencies { testImplementation sourceSets.testSupport.output integrationTestImplementation sourceSets.testSupport.output + compile fileTree(dir: "${rootDir}/libs", include: "*.jar") } task integrationTest(type: Test, dependsOn: ["compileTestJava"]) { @@ -390,6 +402,7 @@ applicationDefaultJvmArgs = [ "-Dlog4j.shutdownHookEnabled=false", // netty specific module warnings "-Dio.netty.tryReflectionSetAccessible=true", + "-Djava.library.path=/opt/cloudhsm/lib", "--add-exports", "java.base/jdk.internal.misc=ALL-UNNAMED", "--add-opens", @@ -425,6 +438,7 @@ startScripts { } dependencies { + compile fileTree(dir: "${rootDir}/libs", include: "*.jar") compile project(':ethsigner:app') errorprone 'com.google.errorprone:error_prone_core' } @@ -478,29 +492,36 @@ tasks.register("dockerDistUntar") { } task distDocker(type: Exec) { + def tagSuffix = project.hasProperty("dockerfile")? "-" + dockerfile : "" + def dockerfile = project.hasProperty("dockerfile")? dockerfile + ".Dockerfile" : "Dockerfile" dependsOn dockerDistUntar def dockerBuildVersion = project.hasProperty('release.releaseVersion') ? project.property('release.releaseVersion') : "${rootProject.version}" - def imageName = "pegasyseng/" + repositoryName - def image = project.hasProperty('release.releaseVersion') ? "${imageName}:" + project.property('release.releaseVersion') : "${imageName}:${project.version}" + def imageName = dockerRepo + "/" + repositoryName + def image = project.hasProperty('release.releaseVersion') ? "${imageName}:" + project.property('release.releaseVersion') + "${tagSuffix}": "${imageName}:${project.version}${tagSuffix}" def dockerBuildDir = "build/docker-" + repositoryName + "/" workingDir "${dockerBuildDir}" doFirst { copy { - from file("${projectDir}/docker/Dockerfile") + from file("${projectDir}/docker/" + dockerfile) + into(workingDir) + } + copy { + from file("${projectDir}/docker/cloudhsm-entrypoint.sh") into(workingDir) } } executable "sh" - args "-c", "docker build --build-arg BUILD_DATE=${buildTime()} --build-arg VERSION=${dockerBuildVersion} --build-arg VCS_REF=${getCheckedOutGitCommitHash()} -t ${image} ." + args "-c", "docker build -f ${dockerfile} --build-arg BUILD_DATE=${buildTime()} --build-arg VERSION=${dockerBuildVersion} --build-arg VCS_REF=${getCheckedOutGitCommitHash()} -t ${image} ." } task testDocker(type: Exec) { dependsOn distDocker + def tagSuffix = project.hasProperty("dockerfile")? "-" + dockerfile : "" def dockerReportsDir = "docker/reports/" - def imageName = "pegasyseng/" + repositoryName - def image = project.hasProperty('release.releaseVersion') ? "${imageName}:" + project.property('release.releaseVersion') : "${imageName}:${project.version}" + def imageName = dockerRepo + "/" + repositoryName + def image = project.hasProperty('release.releaseVersion') ? "${imageName}:" + project.property('release.releaseVersion') + "${tagSuffix}": "${imageName}:${project.version}${tagSuffix}" workingDir "docker" doFirst { @@ -513,18 +534,19 @@ task testDocker(type: Exec) { task dockerUpload(type: Exec) { dependsOn distDocker - def imageName = "pegasyseng/" + repositoryName - def image = project.hasProperty('release.releaseVersion') ? "${imageName}:" + project.property('release.releaseVersion') : "${imageName}:${project.version}" + def tagSuffix = project.hasProperty("dockerfile")? "-" + dockerfile : "" + def imageName = dockerRepo + "/" + repositoryName + def image = project.hasProperty('release.releaseVersion') ? "${imageName}:" + project.property('release.releaseVersion') + "${tagSuffix}": "${imageName}:${project.version}${tagSuffix}" def cmd = "docker push '${image}'" def additionalTags = [] if (project.hasProperty('branch') && project.property('branch') == 'master') { - additionalTags.add('develop') + additionalTags.add("develop${tagSuffix}") } if (!(version ==~ /.*-SNAPSHOT/)) { - additionalTags.add('latest') - additionalTags.add(version.split(/\./)[0..1].join('.')) + additionalTags.add("latest${tagSuffix}") + additionalTags.add(version.split(/\./)[0..1].join('.') + "${tagSuffix}") } additionalTags.each { tag -> cmd += " && docker tag '${image}' '${imageName}:${tag.trim()}' && docker push '${imageName}:${tag.trim()}'" } diff --git a/community/community-membership.md b/community/community-membership.md index 93607ac86..c4f2f8169 100644 --- a/community/community-membership.md +++ b/community/community-membership.md @@ -11,11 +11,11 @@ this project. | Everyone | none | anybody with a belly button | Member | everyone who contributes - code or otherwise | EthSigner GitHub org member | Approver | approve accepting contributions | write permissions on master -| Project Manager | management of the project | PegaSys -| Project Sponsor | contribute developer resources | PegaSys -| Open Source Circle | OSS support | PegaSys -| Project Evangelist | promote the project | PegaSys -| Benevolent Dictator | decision tie-breaker | PegaSys +| Project Manager | management of the project | ConsenSys +| Project Sponsor | contribute developer resources | ConsenSys +| Open Source Circle | OSS support | ConsenSys +| Project Evangelist | promote the project | ConsenSys +| Benevolent Dictator | decision tie-breaker | ConsenSys ## Everyone Any person from the public is able to access the code. The standard permissions grant the ability to view the code, view open bugs, access the wiki, download binaries, view CI results and comment on pull requests. @@ -46,7 +46,6 @@ issues and PRs assigned to them. - Authoring or reviewing PRs on GitHub - Filing or commenting on issues on GitHub - Contributing to community discussions (e.g. meetings, Slack, email discussion forums, Stack Overflow) -- Subscribed to [ethsigner-dev@pegasys.tech] - Joined [EthSigner Discord] - Have read the [contributor guide] - Signed ICLA, as described in [CLA.md] @@ -88,7 +87,7 @@ Code approvers are members that have signed an ICLA and have been granted additi ## Project Manager The Project Manager role provides the user with control over management aspects of the project. -**Defined by:** PegaSys +**Defined by:** ConsenSys ### Requirements - Includes all of the requirements of a Member user @@ -105,7 +104,7 @@ The Project Manager role provides the user with control over management aspects ## Project Sponsor The Project Sponsor role provides a user with the ability to contribute additional developer resources to the project. Project Sponsors must sign the ICLA. -**Defined by:** PegaSys +**Defined by:** ConsenSys ### Requirements - Signed ICLA, as described in [CLA.md] @@ -117,7 +116,7 @@ The Project Sponsor role provides a user with the ability to contribute addition ## Open Source Circle The Open Source Circle is a group that provides open source software support to projects. -**Defined by:** PegaSys +**Defined by:** ConsenSys ### Requirements - Includes all of the requirements of a Member user @@ -132,7 +131,7 @@ The Open Source Circle is a group that provides open source software support to ## Project Evangelist The Project Evangelist role is for those who wish to promote the project to the outside world, but not actively contribute to it. -**Defined by:** PegaSys +**Defined by:** ConsenSys ### Requirements - Includes all of the requirements of a Member user @@ -158,8 +157,8 @@ The benevolent dictator, or project lead, is self-appointed. However, because th ## Attribution This document is adapted from the following sources: -- Kubernetes community-membership.md, available at [kub community membership]. -- OSSWatch Benevolent Dictator Governance Model, available at [oss watch benevolent dictator]. +- Kubernetes community-membership.md, available at [kub community membership]. +- OSSWatch Benevolent Dictator Governance Model, available at [oss watch benevolent dictator]. [CLA.md]: /CLA.md [oss watch benevolent dictator]: http://oss-watch.ac.uk/resources/benevolentdictatorgovernancemodel @@ -168,6 +167,5 @@ This document is adapted from the following sources: [contributor guide]: /CONTRIBUTING.md [New contributors]: /CONTRIBUTING.md [two-factor authentication]: https://help.github.com/articles/about-two-factor-authentication -[ethsigner-dev@pegasys.tech]: mailto:ethsigner-dev@pegasys.tech [EthSigner Discord]: https://discord.gg/5U9Jwp7 [EthSigner Documentation]: /docs diff --git a/docker/cloudhsm-entrypoint.sh b/docker/cloudhsm-entrypoint.sh new file mode 100644 index 000000000..fc026a086 --- /dev/null +++ b/docker/cloudhsm-entrypoint.sh @@ -0,0 +1,32 @@ +#! /bin/bash + +# Set HSM IP as an environmental variable +# ENV HSM_IP + +if [[ -z "$HSM_IP" ]]; then + #statements + echo -e "\n* HSM_IP env variable not set: skipping CloudHSM connection \n" +else + # Configure cloudhms-client + # COPY customerCA.crt /opt/cloudhsm/etc/ + /opt/cloudhsm/bin/configure -a $HSM_IP + + # start cloudhsm client + echo -n "* Starting CloudHSM client ... " + /opt/cloudhsm/bin/cloudhsm_client /opt/cloudhsm/etc/cloudhsm_client.cfg &> /tmp/cloudhsm_client_start.log & + + # wait for startup + while true + do + if grep 'libevmulti_init: Ready !' /tmp/cloudhsm_client_start.log &> /dev/null + then + echo "[OK]" + break + fi + sleep 0.5 + done + echo -e "\n* CloudHSM client started successfully ... \n" +fi + +# start application +/opt/ethsigner/bin/ethsigner $@ diff --git a/docker/cloudhsm.Dockerfile b/docker/cloudhsm.Dockerfile new file mode 100644 index 000000000..7b26983d2 --- /dev/null +++ b/docker/cloudhsm.Dockerfile @@ -0,0 +1,38 @@ +# Use the amazon linux image +FROM amazonlinux:2 + +# Install CloudHSM client +RUN yum install -y https://s3.amazonaws.com/cloudhsmv2-software/CloudHsmClient/EL7/cloudhsm-client-latest.el7.x86_64.rpm + +# Install CloudHSM Java library +RUN yum install -y https://s3.amazonaws.com/cloudhsmv2-software/CloudHsmClient/EL7/cloudhsm-client-jce-latest.el7.x86_64.rpm + +# Install Java, Maven, wget, unzip and ncurses-compat-libs +RUN yum install -y java wget unzip ncurses-compat-libs +RUN yum install -y which + +COPY cloudhsm-entrypoint.sh /opt/ethsigner/bin/cloudhsm-entrypoint.sh +RUN chmod +x /opt/ethsigner/bin/cloudhsm-entrypoint.sh + +COPY ethsigner /opt/ethsigner/ +WORKDIR /opt/ethsigner + +# Expose services ports +# 8545 HTTP JSON-RPC +EXPOSE 8545 + +ENTRYPOINT ["/opt/ethsigner/bin/cloudhsm-entrypoint.sh"] + +# Build-time metadata as defined at http://label-schema.org +ARG BUILD_DATE +ARG VCS_REF +ARG VERSION +LABEL org.label-schema.build-date=$BUILD_DATE \ + org.label-schema.name="Ethsigner" \ + org.label-schema.description="Ethereum transaction signing application" \ + org.label-schema.url="https://docs.ethsigner.pegasys.tech/" \ + org.label-schema.vcs-ref=$VCS_REF \ + org.label-schema.vcs-url="https://github.com/PegaSysEng/ethsigner.git" \ + org.label-schema.vendor="Pegasys" \ + org.label-schema.version=$VERSION \ + org.label-schema.schema-version="1.0" diff --git a/ethsigner/app/src/main/java/tech/pegasys/ethsigner/EthSignerApp.java b/ethsigner/app/src/main/java/tech/pegasys/ethsigner/EthSignerApp.java index b4177b559..8492650b0 100644 --- a/ethsigner/app/src/main/java/tech/pegasys/ethsigner/EthSignerApp.java +++ b/ethsigner/app/src/main/java/tech/pegasys/ethsigner/EthSignerApp.java @@ -15,7 +15,9 @@ import static java.nio.charset.StandardCharsets.UTF_8; import tech.pegasys.ethsigner.subcommands.AzureSubCommand; +import tech.pegasys.ethsigner.subcommands.CaviumSubCommand; import tech.pegasys.ethsigner.subcommands.FileBasedSubCommand; +import tech.pegasys.ethsigner.subcommands.HSMSubCommand; import tech.pegasys.ethsigner.subcommands.HashicorpSubCommand; import tech.pegasys.ethsigner.subcommands.MultiKeySubCommand; import tech.pegasys.ethsigner.subcommands.RawSubCommand; @@ -35,6 +37,8 @@ public static void main(final String... args) { new HashicorpSubCommand(), new FileBasedSubCommand(), new AzureSubCommand(), + new HSMSubCommand(), + new CaviumSubCommand(), new MultiKeySubCommand(), new RawSubCommand()); diff --git a/ethsigner/commandline/build.gradle b/ethsigner/commandline/build.gradle index 13becc29e..0e396446c 100644 --- a/ethsigner/commandline/build.gradle +++ b/ethsigner/commandline/build.gradle @@ -21,6 +21,8 @@ dependencies { implementation 'org.apache.logging.log4j:log4j-api' implementation 'org.apache.logging.log4j:log4j-core' implementation 'org.apache.tuweni:tuweni-toml' + implementation 'org.hyperledger.besu:plugin-api' + implementation 'org.hyperledger.besu.internal:metrics-core' runtimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl' diff --git a/ethsigner/commandline/src/main/java/tech/pegasys/ethsigner/EthSignerBaseCommand.java b/ethsigner/commandline/src/main/java/tech/pegasys/ethsigner/EthSignerBaseCommand.java index d1fb9562c..3765afd60 100644 --- a/ethsigner/commandline/src/main/java/tech/pegasys/ethsigner/EthSignerBaseCommand.java +++ b/ethsigner/commandline/src/main/java/tech/pegasys/ethsigner/EthSignerBaseCommand.java @@ -16,31 +16,40 @@ import static tech.pegasys.ethsigner.DefaultCommandValues.LONG_FORMAT_HELP; import static tech.pegasys.ethsigner.DefaultCommandValues.PATH_FORMAT_HELP; import static tech.pegasys.ethsigner.DefaultCommandValues.PORT_FORMAT_HELP; +import static tech.pegasys.ethsigner.core.metrics.EthSignerMetricCategory.DEFAULT_METRIC_CATEGORIES; import static tech.pegasys.ethsigner.util.RequiredOptionsUtil.checkIfRequiredOptionsAreInitialized; import tech.pegasys.ethsigner.annotations.RequiredOption; +import tech.pegasys.ethsigner.config.AllowListHostsProperty; import tech.pegasys.ethsigner.config.ConfigFileOption; import tech.pegasys.ethsigner.config.InvalidCommandLineOptionsException; import tech.pegasys.ethsigner.config.PicoCliTlsServerOptions; import tech.pegasys.ethsigner.config.tls.client.PicoCliClientTlsOptions; +import tech.pegasys.ethsigner.convertor.MetricCategoryConverter; import tech.pegasys.ethsigner.core.CorsAllowedOriginsProperty; import tech.pegasys.ethsigner.core.config.Config; import tech.pegasys.ethsigner.core.config.TlsOptions; import tech.pegasys.ethsigner.core.config.tls.client.ClientTlsOptions; +import tech.pegasys.ethsigner.core.metrics.EthSignerMetricCategory; import tech.pegasys.ethsigner.core.signing.ChainIdProvider; import tech.pegasys.ethsigner.core.signing.ConfigurationChainId; import tech.pegasys.ethsigner.util.PicoCliClientTlsOptionValidator; import tech.pegasys.ethsigner.util.PicoCliTlsServerOptionsValidator; +import java.net.InetAddress; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.time.Duration; import java.util.Collection; +import java.util.List; import java.util.Optional; +import java.util.Set; import com.google.common.base.MoreObjects; import org.apache.logging.log4j.Level; +import org.hyperledger.besu.metrics.StandardMetricCategory; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; import picocli.CommandLine.Command; import picocli.CommandLine.HelpCommand; import picocli.CommandLine.Mixin; @@ -172,6 +181,44 @@ public void setDownstreamHttpPath(final String path) { @Mixin private PicoCliClientTlsOptions clientTlsOptions; + @Option( + names = {"--metrics-enabled"}, + description = "Set to start the metrics exporter (default: ${DEFAULT-VALUE})") + private final Boolean metricsEnabled = false; + + @SuppressWarnings({"FieldCanBeFinal", "FieldMayBeFinal"}) // PicoCLI requires non-final Strings. + @Option( + names = {"--metrics-host"}, + paramLabel = HOST_FORMAT_HELP, + description = "Host for the metrics exporter to listen on (default: ${DEFAULT-VALUE})", + arity = "1") + private String metricsHost = InetAddress.getLoopbackAddress().getHostAddress(); + + @Option( + names = {"--metrics-port"}, + paramLabel = PORT_FORMAT_HELP, + description = "Port for the metrics exporter to listen on (default: ${DEFAULT-VALUE})", + arity = "1") + private final Integer metricsPort = 8546; + + @Option( + names = {"--metrics-category", "--metrics-categories"}, + paramLabel = "", + split = ",", + arity = "1..*", + description = + "Comma separated list of categories to track metrics for (default: ${DEFAULT-VALUE}),", + converter = Web3signerMetricCategoryConverter.class) + private final Set metricCategories = DEFAULT_METRIC_CATEGORIES; + + @Option( + names = {"--metrics-host-allowlist"}, + paramLabel = "[,...]... or * or all", + description = + "Comma separated list of hostnames to allow for metrics access, or * to accept any host (default: ${DEFAULT-VALUE})", + defaultValue = "localhost,127.0.0.1") + private final AllowListHostsProperty metricsHostAllowList = new AllowListHostsProperty(); + @Override public Level getLogLevel() { return logLevel; @@ -234,6 +281,31 @@ public Collection getCorsAllowedOrigins() { return rpcHttpCorsAllowedOrigins; } + @Override + public Boolean isMetricsEnabled() { + return metricsEnabled; + } + + @Override + public Integer getMetricsPort() { + return metricsPort; + } + + @Override + public String getMetricsHost() { + return metricsHost; + } + + @Override + public Set getMetricCategories() { + return metricCategories; + } + + @Override + public List getMetricsHostAllowList() { + return metricsHostAllowList; + } + @Override public void run() { // validation is performed to simulate similar behavior as with ArgGroups. @@ -282,4 +354,12 @@ void validateArgs() { throw new InvalidCommandLineOptionsException(errorMessage.trim()); } } + + public static class Web3signerMetricCategoryConverter extends MetricCategoryConverter { + + public Web3signerMetricCategoryConverter() { + addCategories(EthSignerMetricCategory.class); + addCategories(StandardMetricCategory.class); + } + } } diff --git a/ethsigner/commandline/src/main/java/tech/pegasys/ethsigner/config/AllowListHostsProperty.java b/ethsigner/commandline/src/main/java/tech/pegasys/ethsigner/config/AllowListHostsProperty.java new file mode 100644 index 000000000..7e9759427 --- /dev/null +++ b/ethsigner/commandline/src/main/java/tech/pegasys/ethsigner/config/AllowListHostsProperty.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.ethsigner.config; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import javax.annotation.Nonnull; + +import com.google.common.base.Splitter; +import com.google.common.base.Strings; + +public class AllowListHostsProperty extends AbstractList { + + private final List hostnamesAllowlist = new ArrayList<>(); + + public AllowListHostsProperty() {} + + @Override + @Nonnull + public Iterator iterator() { + if (hostnamesAllowlist.size() == 1 && hostnamesAllowlist.get(0).equals("none")) { + return Collections.emptyIterator(); + } else { + return hostnamesAllowlist.iterator(); + } + } + + @Override + public int size() { + return hostnamesAllowlist.size(); + } + + @Override + public boolean add(final String string) { + return addAll(Collections.singleton(string)); + } + + @Override + public String get(final int index) { + return hostnamesAllowlist.get(index); + } + + @Override + public boolean addAll(final Collection collection) { + final int initialSize = hostnamesAllowlist.size(); + for (final String string : collection) { + if (Strings.isNullOrEmpty(string)) { + throw new IllegalArgumentException("Hostname cannot be empty string or null string."); + } + for (final String s : Splitter.onPattern("\\s*,+\\s*").split(string)) { + if ("all".equals(s)) { + hostnamesAllowlist.add("*"); + } else { + hostnamesAllowlist.add(s); + } + } + } + + if (hostnamesAllowlist.contains("none")) { + if (hostnamesAllowlist.size() > 1) { + throw new IllegalArgumentException("Value 'none' can't be used with other hostnames"); + } + } else if (hostnamesAllowlist.contains("*")) { + if (hostnamesAllowlist.size() > 1) { + throw new IllegalArgumentException( + "Values '*' or 'all' can't be used with other hostnames"); + } + } + + return hostnamesAllowlist.size() != initialSize; + } +} diff --git a/ethsigner/commandline/src/main/java/tech/pegasys/ethsigner/convertor/MetricCategoryConverter.java b/ethsigner/commandline/src/main/java/tech/pegasys/ethsigner/convertor/MetricCategoryConverter.java new file mode 100644 index 000000000..623c808ee --- /dev/null +++ b/ethsigner/commandline/src/main/java/tech/pegasys/ethsigner/convertor/MetricCategoryConverter.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.ethsigner.convertor; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; +import picocli.CommandLine; + +public class MetricCategoryConverter implements CommandLine.ITypeConverter { + + private final Map metricCategories = new HashMap<>(); + + @Override + public MetricCategory convert(final String value) { + final MetricCategory category = metricCategories.get(value); + if (category == null) { + throw new IllegalArgumentException("Unknown category: " + value); + } + return category; + } + + public & MetricCategory> void addCategories(final Class categoryEnum) { + EnumSet.allOf(categoryEnum) + .forEach(category -> metricCategories.put(category.name(), category)); + } +} diff --git a/ethsigner/core/build.gradle b/ethsigner/core/build.gradle index 81f640393..aa486b881 100644 --- a/ethsigner/core/build.gradle +++ b/ethsigner/core/build.gradle @@ -37,6 +37,8 @@ dependencies { implementation 'io.vertx:vertx-web' implementation 'io.vertx:vertx-web-client' implementation 'org.apache.tuweni:tuweni-net' + implementation 'org.hyperledger.besu.internal:metrics-core' + implementation 'org.hyperledger.besu:plugin-api' runtimeOnly 'org.apache.logging.log4j:log4j-core' runtimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl' @@ -52,8 +54,6 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' - - integrationTestImplementation 'tech.pegasys.signers.internal:signing-secp256k1-impl' integrationTestImplementation 'org.junit.jupiter:junit-jupiter-api' integrationTestImplementation 'org.junit.jupiter:junit-jupiter-params' diff --git a/ethsigner/core/src/integration-test/java/tech/pegasys/ethsigner/jsonrpcproxy/IntegrationTestBase.java b/ethsigner/core/src/integration-test/java/tech/pegasys/ethsigner/jsonrpcproxy/IntegrationTestBase.java index a3e265746..bbae84f8c 100644 --- a/ethsigner/core/src/integration-test/java/tech/pegasys/ethsigner/jsonrpcproxy/IntegrationTestBase.java +++ b/ethsigner/core/src/integration-test/java/tech/pegasys/ethsigner/jsonrpcproxy/IntegrationTestBase.java @@ -15,6 +15,7 @@ import static io.restassured.RestAssured.given; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockserver.integration.ClientAndServer.startClientAndServer; @@ -27,6 +28,7 @@ import tech.pegasys.ethsigner.core.AddressIndexedSignerProvider; import tech.pegasys.ethsigner.core.Runner; import tech.pegasys.ethsigner.core.jsonrpc.JsonDecoder; +import tech.pegasys.ethsigner.core.metrics.MetricsEndpoint; import tech.pegasys.ethsigner.core.requesthandler.sendtransaction.DownstreamPathCalculator; import tech.pegasys.ethsigner.jsonrpcproxy.model.request.EthNodeRequest; import tech.pegasys.ethsigner.jsonrpcproxy.model.request.EthRequestFactory; @@ -147,7 +149,8 @@ static void setupEthSigner(final long chainId, final String downstreamHttpReques jsonDecoder, dataPath, vertx, - singletonList("sample.com")); + singletonList("sample.com"), + new MetricsEndpoint(false, 0, "", emptySet(), emptyList())); runner.start(); final Path portsFile = dataPath.resolve(PORTS_FILENAME); diff --git a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/AddressIndexedSignerProvider.java b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/AddressIndexedSignerProvider.java index 56b618b75..d0f5e76fc 100644 --- a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/AddressIndexedSignerProvider.java +++ b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/AddressIndexedSignerProvider.java @@ -74,4 +74,8 @@ public Set availableAddresses() { public Set availablePublicKeys() { return signerProvider.availablePublicKeys(); } + + public void shutdown() { + signerProvider.shutdown(); + } } diff --git a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/EthSigner.java b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/EthSigner.java index a77f63bf5..f2fd4e2f5 100644 --- a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/EthSigner.java +++ b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/EthSigner.java @@ -16,6 +16,7 @@ import tech.pegasys.ethsigner.core.config.Config; import tech.pegasys.ethsigner.core.config.TlsOptions; import tech.pegasys.ethsigner.core.jsonrpc.JsonDecoder; +import tech.pegasys.ethsigner.core.metrics.MetricsEndpoint; import tech.pegasys.ethsigner.core.requesthandler.sendtransaction.DownstreamPathCalculator; import tech.pegasys.ethsigner.core.util.FileUtil; import tech.pegasys.signers.secp256k1.api.SignerProvider; @@ -71,6 +72,14 @@ public void run() { .setReuseAddress(true) .setReusePort(true); + final MetricsEndpoint metricsEndpoint = + new MetricsEndpoint( + config.isMetricsEnabled(), + config.getMetricsPort(), + config.getMetricsHost(), + config.getMetricCategories(), + config.getMetricsHostAllowList()); + final Vertx vertx = Vertx.vertx(); try { final Runner runner = @@ -84,13 +93,23 @@ public void run() { jsonDecoder, config.getDataPath(), vertx, - config.getCorsAllowedOrigins()); + config.getCorsAllowedOrigins(), + metricsEndpoint); runner.start(); } catch (final Throwable t) { vertx.close(); throw new InitializationException("Failed to create http service.", t); } + Runtime.getRuntime().addShutdownHook(new Shutdown()); + } + + class Shutdown extends Thread { + @Override + public void run() { + signerProvider.shutdown(); + System.out.println("Shutting down EthSigner"); + } } private HttpServerOptions applyConfigTlsSettingsTo(final HttpServerOptions input) { diff --git a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/Runner.java b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/Runner.java index c7d21321c..99baf807e 100644 --- a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/Runner.java +++ b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/Runner.java @@ -19,10 +19,12 @@ import tech.pegasys.ethsigner.core.http.RequestMapper; import tech.pegasys.ethsigner.core.http.UpcheckHandler; import tech.pegasys.ethsigner.core.jsonrpc.JsonDecoder; +import tech.pegasys.ethsigner.core.metrics.MetricsEndpoint; import tech.pegasys.ethsigner.core.requesthandler.VertxRequestTransmitter; import tech.pegasys.ethsigner.core.requesthandler.VertxRequestTransmitterFactory; import tech.pegasys.ethsigner.core.requesthandler.internalresponse.EthAccountsResultProvider; import tech.pegasys.ethsigner.core.requesthandler.internalresponse.EthSignResultProvider; +import tech.pegasys.ethsigner.core.requesthandler.internalresponse.EthSignTransactionResultProvider; import tech.pegasys.ethsigner.core.requesthandler.internalresponse.InternalResponseHandler; import tech.pegasys.ethsigner.core.requesthandler.passthrough.PassThroughHandler; import tech.pegasys.ethsigner.core.requesthandler.sendtransaction.DownstreamPathCalculator; @@ -34,6 +36,7 @@ import java.nio.file.Path; import java.time.Duration; import java.util.Collection; +import java.util.Optional; import java.util.Properties; import java.util.StringJoiner; import java.util.concurrent.CompletableFuture; @@ -73,6 +76,7 @@ public class Runner { private final Vertx vertx; private final Collection allowedCorsOrigins; private final HttpServerOptions serverOptions; + private final MetricsEndpoint metricsEndpoint; public Runner( final long chainId, @@ -84,7 +88,8 @@ public Runner( final JsonDecoder jsonDecoder, final Path dataPath, final Vertx vertx, - final Collection allowedCorsOrigins) { + final Collection allowedCorsOrigins, + final MetricsEndpoint metricsEndpoint) { this.chainId = chainId; this.signerProvider = signerProvider; this.clientOptions = clientOptions; @@ -95,13 +100,15 @@ public Runner( this.vertx = vertx; this.allowedCorsOrigins = allowedCorsOrigins; this.serverOptions = serverOptions; + this.metricsEndpoint = metricsEndpoint; } public void start() throws ExecutionException, InterruptedException { + metricsEndpoint.start(vertx); final HttpServer httpServer = createServerAndWait(vertx, router()); LOG.info("Server is up, and listening on {}", httpServer.actualPort()); if (dataPath != null) { - writePortsToFile(httpServer); + writePortsToFile(httpServer, metricsEndpoint.getPort()); } } @@ -132,7 +139,7 @@ private Router router() { .handler(BodyHandler.create()) .handler(ResponseContentTypeHandler.create()) .failureHandler(new JsonRpcErrorHandler(new HttpResponseFactory())) - .blockingHandler(new JsonRpcHandler(responseFactory, requestMapper, jsonDecoder)); + .blockingHandler(new JsonRpcHandler(responseFactory, requestMapper, jsonDecoder), false); // Handler for UpCheck endpoint router @@ -167,16 +174,21 @@ private RequestMapper createRequestMapper( requestMapper.addHandler( "eth_sign", new InternalResponseHandler<>(responseFactory, new EthSignResultProvider(signerProvider))); - + requestMapper.addHandler( + "eth_signTransaction", + new InternalResponseHandler<>( + responseFactory, + new EthSignTransactionResultProvider(chainId, signerProvider, jsonDecoder))); return requestMapper; } - private void writePortsToFile(final HttpServer server) { + private void writePortsToFile(final HttpServer server, final Optional metricsPort) { final File portsFile = new File(dataPath.toFile(), "ethsigner.ports"); portsFile.deleteOnExit(); final Properties properties = new Properties(); properties.setProperty("http-jsonrpc", String.valueOf(server.actualPort())); + metricsPort.ifPresent(port -> properties.setProperty("metrics-port", String.valueOf(port))); LOG.info( "Writing ethsigner.ports file: {}, with contents: {}", diff --git a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/config/Config.java b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/config/Config.java index 17fdc86ff..2a88c91c7 100644 --- a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/config/Config.java +++ b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/config/Config.java @@ -18,9 +18,12 @@ import java.nio.file.Path; import java.time.Duration; import java.util.Collection; +import java.util.List; import java.util.Optional; +import java.util.Set; import org.apache.logging.log4j.Level; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; public interface Config { @@ -47,4 +50,14 @@ public interface Config { Optional getClientTlsOptions(); Collection getCorsAllowedOrigins(); + + Boolean isMetricsEnabled(); + + Integer getMetricsPort(); + + String getMetricsHost(); + + Set getMetricCategories(); + + List getMetricsHostAllowList(); } diff --git a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/metrics/EthSignerMetricCategory.java b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/metrics/EthSignerMetricCategory.java new file mode 100644 index 000000000..039df5d0f --- /dev/null +++ b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/metrics/EthSignerMetricCategory.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.ethsigner.core.metrics; + +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import org.hyperledger.besu.metrics.StandardMetricCategory; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +public enum EthSignerMetricCategory implements MetricCategory { + HTTP("http"), + SIGNING("signing"); + + private final String name; + + public static final Set DEFAULT_METRIC_CATEGORIES; + + static { + DEFAULT_METRIC_CATEGORIES = + ImmutableSet.builder() + .addAll(EnumSet.allOf(EthSignerMetricCategory.class)) + .addAll(EnumSet.allOf(StandardMetricCategory.class)) + .build(); + } + + EthSignerMetricCategory(final String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public Optional getApplicationPrefix() { + return Optional.empty(); + } +} diff --git a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/metrics/MetricsEndpoint.java b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/metrics/MetricsEndpoint.java new file mode 100644 index 000000000..1708ae46f --- /dev/null +++ b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/metrics/MetricsEndpoint.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.ethsigner.core.metrics; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import io.vertx.core.Vertx; +import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; +import org.hyperledger.besu.metrics.prometheus.MetricsService; +import org.hyperledger.besu.metrics.prometheus.PrometheusMetricsSystem; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +public class MetricsEndpoint { + private final MetricsSystem metricsSystem; + private final MetricsConfiguration metricsConfig; + private Optional metricsService = Optional.empty(); + + public MetricsEndpoint( + final Boolean metricsEnabled, + final Integer metricsPort, + final String metricsNetworkInterface, + final Set metricCategories, + final List metricsHostAllowList) { + final MetricsConfiguration metricsConfig = + createMetricsConfiguration( + metricsEnabled, + metricsPort, + metricsNetworkInterface, + metricCategories, + metricsHostAllowList); + this.metricsSystem = PrometheusMetricsSystem.init(metricsConfig); + this.metricsConfig = metricsConfig; + } + + public void start(final Vertx vertx) { + if (metricsConfig.isEnabled()) { + metricsService = Optional.of(MetricsService.create(vertx, metricsConfig, metricsSystem)); + } else { + metricsService = Optional.empty(); + } + metricsService.ifPresent(MetricsService::start); + } + + public void stop() { + metricsService.ifPresent(MetricsService::stop); + } + + public Optional getPort() { + return metricsService.flatMap(MetricsService::getPort); + } + + public MetricsSystem getMetricsSystem() { + return metricsSystem; + } + + private MetricsConfiguration createMetricsConfiguration( + final Boolean metricsEnabled, + final Integer metricsPort, + final String metricsNetworkInterface, + final Set metricCategories, + final List metricsHostAllowList) { + return MetricsConfiguration.builder() + .enabled(metricsEnabled) + .port(metricsPort) + .host(metricsNetworkInterface) + .metricCategories(metricCategories) + .hostsWhitelist(metricsHostAllowList) + .build(); + } +} diff --git a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/requesthandler/internalresponse/EthSignTransactionResultProvider.java b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/requesthandler/internalresponse/EthSignTransactionResultProvider.java new file mode 100644 index 000000000..394623387 --- /dev/null +++ b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/requesthandler/internalresponse/EthSignTransactionResultProvider.java @@ -0,0 +1,116 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.ethsigner.core.requesthandler.internalresponse; + +import static tech.pegasys.ethsigner.core.jsonrpc.response.JsonRpcError.INVALID_PARAMS; +import static tech.pegasys.ethsigner.core.jsonrpc.response.JsonRpcError.SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT; + +import tech.pegasys.ethsigner.core.AddressIndexedSignerProvider; +import tech.pegasys.ethsigner.core.jsonrpc.EthSendTransactionJsonParameters; +import tech.pegasys.ethsigner.core.jsonrpc.JsonDecoder; +import tech.pegasys.ethsigner.core.jsonrpc.JsonRpcRequest; +import tech.pegasys.ethsigner.core.jsonrpc.exception.JsonRpcException; +import tech.pegasys.ethsigner.core.requesthandler.ResultProvider; +import tech.pegasys.ethsigner.core.requesthandler.sendtransaction.transaction.EthTransaction; +import tech.pegasys.ethsigner.core.requesthandler.sendtransaction.transaction.Transaction; +import tech.pegasys.ethsigner.core.signing.TransactionSerializer; +import tech.pegasys.signers.secp256k1.api.Signer; + +import java.util.List; +import java.util.Optional; + +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.JsonObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class EthSignTransactionResultProvider implements ResultProvider { + + private static final Logger LOG = LogManager.getLogger(); + + private final long chainId; + private final AddressIndexedSignerProvider signerProvider; + private final JsonDecoder decoder; + + public EthSignTransactionResultProvider( + final long chainId, + final AddressIndexedSignerProvider signerProvider, + final JsonDecoder decoder) { + this.chainId = chainId; + this.signerProvider = signerProvider; + this.decoder = decoder; + } + + @Override + public String createResponseResult(final JsonRpcRequest request) { + LOG.debug("Transforming request {}, {}", request.getId(), request.getMethod()); + final Transaction transaction; + try { + transaction = createTransaction(request); + + } catch (final NumberFormatException e) { + LOG.debug("Parsing values failed for request: {}", request.getParams(), e); + throw new JsonRpcException(INVALID_PARAMS); + } catch (final IllegalArgumentException | DecodeException e) { + LOG.debug("JSON Deserialization failed for request: {}", request.getParams(), e); + throw new JsonRpcException(INVALID_PARAMS); + } + + if (!transaction.isNonceUserSpecified()) { + LOG.debug("Nonce not present in request {}", request.getId()); + throw new JsonRpcException(INVALID_PARAMS); + } + + LOG.debug("Obtaining signer for {}", transaction.sender()); + final Optional Signer = signerProvider.getSigner(transaction.sender()); + if (Signer.isEmpty()) { + LOG.info("From address ({}) does not match any available account", transaction.sender()); + throw new JsonRpcException(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT); + } + + final TransactionSerializer transactionSerializer = + new TransactionSerializer(Signer.get(), chainId); + return transactionSerializer.serialize(transaction); + } + + private Transaction createTransaction(final JsonRpcRequest request) { + final EthSendTransactionJsonParameters params = + fromRpcRequestToJsonParam(EthSendTransactionJsonParameters.class, request); + return new EthTransaction(params, null, request.getId()); + } + + public T fromRpcRequestToJsonParam(final Class type, final JsonRpcRequest request) { + final Object object; + final Object params = request.getParams(); + if (params instanceof List) { + @SuppressWarnings("unchecked") + final List paramList = (List) params; + if (paramList.size() != 1) { + throw new IllegalArgumentException( + type.getSimpleName() + + " json Rpc requires one parameter, request contained " + + paramList.size()); + } + object = paramList.get(0); + } else { + object = params; + } + if (object == null) { + throw new IllegalArgumentException( + type.getSimpleName() + + " json Rpc requires a valid parameter, request contained a null object"); + } + final JsonObject receivedParams = JsonObject.mapFrom(object); + return decoder.decodeValue(receivedParams.toBuffer(), type); + } +} diff --git a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/requesthandler/sendtransaction/SendTransactionHandler.java b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/requesthandler/sendtransaction/SendTransactionHandler.java index 41ef32cd7..495112933 100644 --- a/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/requesthandler/sendtransaction/SendTransactionHandler.java +++ b/ethsigner/core/src/main/java/tech/pegasys/ethsigner/core/requesthandler/sendtransaction/SendTransactionHandler.java @@ -71,6 +71,7 @@ public void handle(final RoutingContext context, final JsonRpcRequest request) { return; } + LOG.debug("Obtaining signer for {}", transaction.sender()); final Optional signer = signerProvider.getSigner(transaction.sender()); if (signer.isEmpty()) { diff --git a/ethsigner/core/src/test/java/tech/pegasys/ethsigner/core/jsonrpcproxy/EthSignTransactionResultProviderTest.java b/ethsigner/core/src/test/java/tech/pegasys/ethsigner/core/jsonrpcproxy/EthSignTransactionResultProviderTest.java new file mode 100644 index 000000000..ffbca4c55 --- /dev/null +++ b/ethsigner/core/src/test/java/tech/pegasys/ethsigner/core/jsonrpcproxy/EthSignTransactionResultProviderTest.java @@ -0,0 +1,219 @@ +/* + * Copyright 2020 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.ethsigner.core.jsonrpcproxy; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static tech.pegasys.ethsigner.core.jsonrpc.response.JsonRpcError.INVALID_PARAMS; +import static tech.pegasys.ethsigner.core.jsonrpc.response.JsonRpcError.SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT; + +import tech.pegasys.ethsigner.core.AddressIndexedSignerProvider; +import tech.pegasys.ethsigner.core.jsonrpc.JsonDecoder; +import tech.pegasys.ethsigner.core.jsonrpc.JsonRpcRequest; +import tech.pegasys.ethsigner.core.jsonrpc.JsonRpcRequestId; +import tech.pegasys.ethsigner.core.jsonrpc.exception.JsonRpcException; +import tech.pegasys.ethsigner.core.requesthandler.internalresponse.EthSignTransactionResultProvider; +import tech.pegasys.signers.secp256k1.EthPublicKeyUtils; +import tech.pegasys.signers.secp256k1.api.Signature; +import tech.pegasys.signers.secp256k1.api.Signer; + +import java.math.BigInteger; +import java.security.interfaces.ECPublicKey; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.json.JsonObject; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.NullSource; +import org.web3j.crypto.Credentials; +import org.web3j.crypto.Keys; +import org.web3j.crypto.Sign; + +public class EthSignTransactionResultProviderTest { + + private static JsonDecoder jsonDecoder; + private static long chainId; + + @BeforeAll + static void beforeAll() { + final ObjectMapper jsonObjectMapper = new ObjectMapper(); + jsonObjectMapper.configure(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES, true); + jsonObjectMapper.configure(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES, true); + jsonDecoder = new JsonDecoder(jsonObjectMapper); + chainId = 44844; + } + + @ParameterizedTest + @ArgumentsSource(InvalidParamsProvider.class) + @NullSource + public void ifParamIsInvalidExceptionIsThrownWithInvalidParams(final Object params) { + final AddressIndexedSignerProvider mockSignerProvider = + mock(AddressIndexedSignerProvider.class); + final EthSignTransactionResultProvider resultProvider = + new EthSignTransactionResultProvider(chainId, mockSignerProvider, jsonDecoder); + + final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_signTransaction"); + request.setId(new JsonRpcRequestId(1)); + request.setParams(params); + + final Throwable thrown = catchThrowable(() -> resultProvider.createResponseResult(request)); + assertThat(thrown).isInstanceOf(JsonRpcException.class); + final JsonRpcException rpcException = (JsonRpcException) thrown; + assertThat(rpcException.getJsonRpcError()).isEqualTo(INVALID_PARAMS); + } + + @Test + public void ifAddressIsNotUnlockedExceptionIsThrownWithSigningNotUnlocked() { + final AddressIndexedSignerProvider mockSignerProvider = + mock(AddressIndexedSignerProvider.class); + final EthSignTransactionResultProvider resultProvider = + new EthSignTransactionResultProvider(chainId, mockSignerProvider, jsonDecoder); + + final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_signTransaction"); + request.setId(new JsonRpcRequestId(1)); + request.setParams(List.of(getTxParameters())); + final Throwable thrown = catchThrowable(() -> resultProvider.createResponseResult(request)); + assertThat(thrown).isInstanceOf(JsonRpcException.class); + final JsonRpcException rpcException = (JsonRpcException) thrown; + assertThat(rpcException.getJsonRpcError()).isEqualTo(SIGNING_FROM_IS_NOT_AN_UNLOCKED_ACCOUNT); + } + + @Test + public void signatureHasTheExpectedFormat() { + final Credentials cs = + Credentials.create("0x1618fc3e47aec7e70451256e033b9edb67f4c469258d8e2fbb105552f141ae41"); + final ECPublicKey key = EthPublicKeyUtils.createPublicKey(cs.getEcKeyPair().getPublicKey()); + + final Signer mockSigner = mock(Signer.class); + doReturn(key).when(mockSigner).getPublicKey(); + final BigInteger v = BigInteger.ONE; + final BigInteger r = BigInteger.TWO; + final BigInteger s = BigInteger.TEN; + doReturn(new Signature(v, r, s)).when(mockSigner).sign(any(byte[].class)); + final AddressIndexedSignerProvider mockSignerProvider = + mock(AddressIndexedSignerProvider.class); + doReturn(Optional.of(mockSigner)).when(mockSignerProvider).getSigner(anyString()); + final EthSignTransactionResultProvider resultProvider = + new EthSignTransactionResultProvider(chainId, mockSignerProvider, jsonDecoder); + + final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_signTransaction"); + final int id = 1; + request.setId(new JsonRpcRequestId(id)); + request.setParams(List.of(getTxParameters())); + + final Object result = resultProvider.createResponseResult(request); + assertThat(result).isInstanceOf(String.class); + final String signedTx = (String) result; + assertThat(signedTx).hasSize(72); + } + + @Test + public void nonceNotProvidedExceptionIsThrownWithInvalidParams() { + final AddressIndexedSignerProvider mockSignerProvider = + mock(AddressIndexedSignerProvider.class); + final EthSignTransactionResultProvider resultProvider = + new EthSignTransactionResultProvider(chainId, mockSignerProvider, jsonDecoder); + + final JsonObject params = getTxParameters(); + params.remove("nonce"); + final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_signTransaction"); + final int id = 1; + request.setId(new JsonRpcRequestId(id)); + request.setParams(params); + final Throwable thrown = catchThrowable(() -> resultProvider.createResponseResult(request)); + assertThat(thrown).isInstanceOf(JsonRpcException.class); + final JsonRpcException rpcException = (JsonRpcException) thrown; + assertThat(rpcException.getJsonRpcError()).isEqualTo(INVALID_PARAMS); + } + + @Test + public void returnsExpectedSignature() { + final Credentials cs = + Credentials.create("0x1618fc3e47aec7e70451256e033b9edb67f4c469258d8e2fbb105552f141ae41"); + final ECPublicKey key = EthPublicKeyUtils.createPublicKey(cs.getEcKeyPair().getPublicKey()); + final String addr = Keys.getAddress(EthPublicKeyUtils.toHexString(key)); + + final Signer mockSigner = mock(Signer.class); + doReturn(key).when(mockSigner).getPublicKey(); + + doAnswer( + answer -> { + byte[] data = answer.getArgument(0, byte[].class); + final Sign.SignatureData signature = + Sign.signPrefixedMessage(data, cs.getEcKeyPair()); + return new Signature( + new BigInteger(signature.getV()), + new BigInteger(1, signature.getR()), + new BigInteger(1, signature.getS())); + }) + .when(mockSigner) + .sign(any(byte[].class)); + final AddressIndexedSignerProvider mockSignerProvider = + mock(AddressIndexedSignerProvider.class); + doReturn(Optional.of(mockSigner)).when(mockSignerProvider).getSigner(anyString()); + final EthSignTransactionResultProvider resultProvider = + new EthSignTransactionResultProvider(chainId, mockSignerProvider, jsonDecoder); + + final JsonObject params = getTxParameters(); + params.put("from", addr); + final JsonRpcRequest request = new JsonRpcRequest("2.0", "eth_signTransaction"); + final int id = 1; + request.setId(new JsonRpcRequestId(id)); + request.setParams(params); + + final Object result = resultProvider.createResponseResult(request); + assertThat(result).isInstanceOf(String.class); + final String encodedTransaction = (String) result; + assertThat(encodedTransaction) + .isEqualTo( + "0xf862468082760094627306090abab3a6e1400e9345bc60c78a8bef57010083015e7ca0c1de8a14a6bb3882fd97d5ebc3ed6db2f15cbdf9cbd9e89027973276c9d5f6d6a068214ca6ca701eaa8e74e819f838478865c267869e362c02018a11a150422efe"); + } + + private static JsonObject getTxParameters() { + final JsonObject jsonObject = new JsonObject(); + jsonObject.put("from", "0xf17f52151ebef6c7334fad080c5704d77216b732"); + jsonObject.put("to", "0x627306090abaB3A6e1400e9345bC60c78a8BEf57"); + jsonObject.put("gasPrice", "0x0"); + jsonObject.put("gas", "0x7600"); + jsonObject.put("nonce", "0x46"); + jsonObject.put("value", "0x1"); + jsonObject.put("data", "0x0"); + return jsonObject; + } + + private static class InvalidParamsProvider implements ArgumentsProvider { + @Override + public Stream provideArguments(final ExtensionContext context) { + return Stream.of( + Arguments.of(Collections.emptyList()), + Arguments.of(Collections.singleton(2)), + Arguments.of(List.of(1, 2, 3)), + Arguments.of(new Object())); + } + } +} diff --git a/ethsigner/subcommands/build.gradle b/ethsigner/subcommands/build.gradle index 84fa1eae6..d451e554a 100644 --- a/ethsigner/subcommands/build.gradle +++ b/ethsigner/subcommands/build.gradle @@ -19,6 +19,8 @@ dependencies { implementation 'tech.pegasys.signers.internal:signing-secp256k1-api' implementation 'tech.pegasys.signers.internal:signing-secp256k1-impl' implementation 'tech.pegasys.signers.internal:keystorage-hashicorp' + implementation 'tech.pegasys.signers.internal:keystorage-hsm' + implementation 'tech.pegasys.signers.internal:keystorage-cavium' implementation 'info.picocli:picocli' implementation 'com.google.guava:guava' diff --git a/ethsigner/subcommands/src/main/java/tech/pegasys/ethsigner/subcommands/CaviumSubCommand.java b/ethsigner/subcommands/src/main/java/tech/pegasys/ethsigner/subcommands/CaviumSubCommand.java new file mode 100644 index 000000000..85c4c35c3 --- /dev/null +++ b/ethsigner/subcommands/src/main/java/tech/pegasys/ethsigner/subcommands/CaviumSubCommand.java @@ -0,0 +1,94 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.ethsigner.subcommands; + +import tech.pegasys.ethsigner.SignerSubCommand; +import tech.pegasys.signers.cavium.CaviumConfig; +import tech.pegasys.signers.cavium.CaviumKeyStoreProvider; +import tech.pegasys.signers.secp256k1.api.Signer; +import tech.pegasys.signers.secp256k1.api.SignerProvider; +import tech.pegasys.signers.secp256k1.api.SingleSignerProvider; +import tech.pegasys.signers.secp256k1.cavium.CaviumKeyStoreSignerFactory; +import tech.pegasys.signers.secp256k1.common.SignerInitializationException; + +import java.nio.file.Path; + +import com.google.common.base.MoreObjects; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Spec; + +/** HSM-based authentication related sub-command */ +@Command( + name = CaviumSubCommand.COMMAND_NAME, + description = "Sign transactions with a key stored in an HSM.", + mixinStandardHelpOptions = true) +public class CaviumSubCommand extends SignerSubCommand { + + // private static final String READ_PIN_FILE_ERROR = "Error when reading the pin from file."; + public static final String COMMAND_NAME = "cavium-signer"; + + public CaviumSubCommand() {} + + @SuppressWarnings("unused") // Picocli injects reference to command spec + @Spec + private CommandLine.Model.CommandSpec spec; + + @Option( + names = {"-l", "--library"}, + description = "The HSM PKCS11 library used to sign transactions.", + paramLabel = "", + required = true) + private Path libraryPath; + + @Option( + names = {"-p", "--slot-pin"}, + description = "The crypto user pin of the HSM slot used to sign transactions.", + paramLabel = "", + required = true) + private String slotPin; + + @Option( + names = {"-a", "--eth-address"}, + description = "Ethereum address of account to sign with.", + paramLabel = "", + required = true) + private String ethAddress; + + private Signer createSigner() throws SignerInitializationException { + final CaviumKeyStoreProvider provider = + new CaviumKeyStoreProvider( + new CaviumConfig(libraryPath != null ? libraryPath.toString() : null, slotPin)); + final CaviumKeyStoreSignerFactory factory = new CaviumKeyStoreSignerFactory(provider); + return factory.createSigner(ethAddress); + } + + @Override + public SignerProvider createSignerFactory() throws SignerInitializationException { + return new SingleSignerProvider(createSigner()); + } + + @Override + public String getCommandName() { + return COMMAND_NAME; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("library", libraryPath) + .add("address", ethAddress) + .toString(); + } +} diff --git a/ethsigner/subcommands/src/main/java/tech/pegasys/ethsigner/subcommands/HSMSubCommand.java b/ethsigner/subcommands/src/main/java/tech/pegasys/ethsigner/subcommands/HSMSubCommand.java new file mode 100644 index 000000000..fd2f830fd --- /dev/null +++ b/ethsigner/subcommands/src/main/java/tech/pegasys/ethsigner/subcommands/HSMSubCommand.java @@ -0,0 +1,102 @@ +/* + * Copyright 2018 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.ethsigner.subcommands; + +import tech.pegasys.ethsigner.SignerSubCommand; +import tech.pegasys.signers.hsm.HSMConfig; +import tech.pegasys.signers.hsm.HSMWalletProvider; +import tech.pegasys.signers.secp256k1.api.Signer; +import tech.pegasys.signers.secp256k1.api.SignerProvider; +import tech.pegasys.signers.secp256k1.api.SingleSignerProvider; +import tech.pegasys.signers.secp256k1.common.SignerInitializationException; +import tech.pegasys.signers.secp256k1.hsm.HSMSignerFactory; + +import java.nio.file.Path; + +import com.google.common.base.MoreObjects; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Spec; + +/** HSM-based authentication related sub-command */ +@Command( + name = HSMSubCommand.COMMAND_NAME, + description = "Sign transactions with a key stored in an HSM.", + mixinStandardHelpOptions = true) +public class HSMSubCommand extends SignerSubCommand { + + // private static final String READ_PIN_FILE_ERROR = "Error when reading the pin from file."; + public static final String COMMAND_NAME = "hsm-signer"; + + public HSMSubCommand() {} + + @SuppressWarnings("unused") // Picocli injects reference to command spec + @Spec + private CommandLine.Model.CommandSpec spec; + + @Option( + names = {"-l", "--library"}, + description = "The HSM PKCS11 library used to sign transactions.", + paramLabel = "", + required = true) + private Path libraryPath; + + @Option( + names = {"-s", "--slot-label"}, + description = "The HSM slot used to sign transactions.", + paramLabel = "", + required = true) + private String slotLabel; + + @Option( + names = {"-p", "--slot-pin"}, + description = "The crypto user pin of the HSM slot used to sign transactions.", + paramLabel = "", + required = true) + private String slotPin; + + @Option( + names = {"-a", "--eth-address"}, + description = "Ethereum address of account to sign with.", + paramLabel = "", + required = true) + private String ethAddress; + + private Signer createSigner() throws SignerInitializationException { + final HSMConfig config = + new HSMConfig(libraryPath != null ? libraryPath.toString() : null, slotLabel, slotPin); + final HSMWalletProvider provider = new HSMWalletProvider(config); + final HSMSignerFactory factory = new HSMSignerFactory(provider); + return factory.createSigner(ethAddress); + } + + @Override + public SignerProvider createSignerFactory() throws SignerInitializationException { + return new SingleSignerProvider(createSigner()); + } + + @Override + public String getCommandName() { + return COMMAND_NAME; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("library", libraryPath) + .add("slot", slotLabel) + .add("address", ethAddress) + .toString(); + } +} diff --git a/ethsigner/subcommands/src/main/java/tech/pegasys/ethsigner/subcommands/MultiKeySubCommand.java b/ethsigner/subcommands/src/main/java/tech/pegasys/ethsigner/subcommands/MultiKeySubCommand.java index 6937f0ac5..1fed235d3 100644 --- a/ethsigner/subcommands/src/main/java/tech/pegasys/ethsigner/subcommands/MultiKeySubCommand.java +++ b/ethsigner/subcommands/src/main/java/tech/pegasys/ethsigner/subcommands/MultiKeySubCommand.java @@ -60,6 +60,14 @@ public MultiKeySubCommand() {} arity = "1") private Path directoryPath; + @Option( + names = {"-c", "--config"}, + description = "The path to a config file to initialize signer providers", + paramLabel = PATH_FORMAT_HELP, + arity = "1", + required = false) + private Path configPath; + @Override protected void validateArgs() throws InitializationException { checkIfRequiredOptionsAreInitialized(this); @@ -68,7 +76,7 @@ protected void validateArgs() throws InitializationException { @Override public SignerProvider createSignerFactory() throws SignerInitializationException { - return MultiKeySignerProvider.create(directoryPath, new EthSignerFileSelector()); + return MultiKeySignerProvider.create(directoryPath, configPath, new EthSignerFileSelector()); } @Override diff --git a/ethsigner/subcommands/src/test/java/tech/pegasys/ethsigner/subcommands/CaviumSubCommandTest.java b/ethsigner/subcommands/src/test/java/tech/pegasys/ethsigner/subcommands/CaviumSubCommandTest.java new file mode 100644 index 000000000..9604b9f74 --- /dev/null +++ b/ethsigner/subcommands/src/test/java/tech/pegasys/ethsigner/subcommands/CaviumSubCommandTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.ethsigner.subcommands; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.logging.log4j.Level; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +public class CaviumSubCommandTest { + + private static final String LIBRARY = "/this/is/the/path/to/the/library/library.so"; + private static final String PIN = "pin"; + private static final String ADDRESS = "0x"; + + private CaviumSubCommand config; + + private boolean parseCommand(final String cmdLine) { + config = new CaviumSubCommand(); + final CommandLine commandLine = new CommandLine(config); + commandLine.setCaseInsensitiveEnumValuesAllowed(true); + commandLine.registerConverter(Level.class, Level::valueOf); + + try { + commandLine.parse(cmdLine.split(" ")); + } catch (final CommandLine.ParameterException e) { + return false; + } + return true; + } + + private String validCommandLine() { + return "--library=" + LIBRARY + " --slot-pin=" + PIN + " --eth-address=" + ADDRESS; + } + + private String removeFieldFrom(final String input, final String fieldname) { + return input.replaceAll("--" + fieldname + "=.*?(\\s|$)", ""); + } + + @Test + public void fullyPopulatedCommandLineParsesIntoVariables() { + final boolean result = parseCommand(validCommandLine()); + + assertThat(result).isTrue(); + assertThat(config.toString()).contains(LIBRARY); + assertThat(config.toString()).contains(ADDRESS); + } + + @Test + public void missingRequiredParamShowsAppropriateError() { + missingParameterShowsError("library"); + missingParameterShowsError("slot-pin"); + missingParameterShowsError("eth-address"); + } + + private void missingParameterShowsError(final String paramToRemove) { + final String cmdLine = removeFieldFrom(validCommandLine(), paramToRemove); + final boolean result = parseCommand(cmdLine); + assertThat(result).isFalse(); + } +} diff --git a/ethsigner/subcommands/src/test/java/tech/pegasys/ethsigner/subcommands/HSMSubCommandTest.java b/ethsigner/subcommands/src/test/java/tech/pegasys/ethsigner/subcommands/HSMSubCommandTest.java new file mode 100644 index 000000000..090e7eb32 --- /dev/null +++ b/ethsigner/subcommands/src/test/java/tech/pegasys/ethsigner/subcommands/HSMSubCommandTest.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.ethsigner.subcommands; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.apache.logging.log4j.Level; +import org.junit.jupiter.api.Test; +import picocli.CommandLine; + +public class HSMSubCommandTest { + + private static final String library = "/this/is/the/path/to/the/library/library.so"; + private static final String slot = "slot"; + private static final String pin = "pin"; + private static final String address = "0x"; + + private HSMSubCommand config; + + private boolean parseCommand(final String cmdLine) { + config = new HSMSubCommand(); + final CommandLine commandLine = new CommandLine(config); + commandLine.setCaseInsensitiveEnumValuesAllowed(true); + commandLine.registerConverter(Level.class, Level::valueOf); + + try { + commandLine.parse(cmdLine.split(" ")); + } catch (final CommandLine.ParameterException e) { + return false; + } + return true; + } + + private String validCommandLine() { + return "--library=" + + library + + " --slot-label=" + + slot + + " --slot-pin=" + + pin + + " --eth-address=" + + address; + } + + private String removeFieldFrom(final String input, final String fieldname) { + return input.replaceAll("--" + fieldname + "=.*?(\\s|$)", ""); + } + + @Test + public void fullyPopulatedCommandLineParsesIntoVariables() { + final boolean result = parseCommand(validCommandLine()); + + assertThat(result).isTrue(); + assertThat(config.toString()).contains(library); + assertThat(config.toString()).contains(slot); + assertThat(config.toString()).contains(address); + } + + @Test + public void missingRequiredParamShowsAppropriateError() { + missingParameterShowsError("library"); + missingParameterShowsError("slot-label"); + missingParameterShowsError("slot-pin"); + missingParameterShowsError("eth-address"); + } + + private void missingParameterShowsError(final String paramToRemove) { + final String cmdLine = removeFieldFrom(validCommandLine(), paramToRemove); + final boolean result = parseCommand(cmdLine); + assertThat(result).isFalse(); + } +} diff --git a/gradle.properties b/gradle.properties index bc83ab5dd..72d49c5f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ org.gradle.jvmargs=-Xmx1g -version=0.7.2-SNAPSHOT +version=0.7.2-ADHARA-SNAPSHOT besuVersion=1.5.0 besuDistroUrl=https://bintray.com/hyperledger-org/besu-repo/download_file?file_path=besu-${besuVersion}.tar.gz hashicorpVaultVersion=1.4.3 diff --git a/gradle/check-licenses.gradle b/gradle/check-licenses.gradle index a49d76184..1b97d7c95 100644 --- a/gradle/check-licenses.gradle +++ b/gradle/check-licenses.gradle @@ -51,7 +51,8 @@ ext.acceptedLicenses = [ 'Common Development and Distribution License 1.0', 'CDDL', 'Unicode/ICU License', - 'CC0' + 'CC0', + 'IAIK of Graz University of Technology License' ]*.toLowerCase() /** @@ -159,7 +160,8 @@ downloadLicenses { (group('javax.mail')): cddl, (group('net.jcip')): apache, (group('net.java.dev.jna')): apache, - (group('com.sun.mail')): cddl + (group('com.sun.mail')): cddl, + ('cloudhsm-3.0.1.jar'): apache ] } diff --git a/gradle/versions.gradle b/gradle/versions.gradle index ec1982bed..1be9eb0c9 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -13,6 +13,8 @@ dependencyManagement { dependencies { + dependency 'com.github.docker-java:docker-java:3.2.0' + dependency 'com.google.errorprone:error_prone_annotation:2.3.4' dependency 'com.google.errorprone:error_prone_check_api:2.3.4' dependency 'com.google.errorprone:error_prone_core:2.3.4' @@ -68,12 +70,17 @@ dependencyManagement { dependency 'org.web3j:core:4.5.14' dependency 'org.web3j:crypto:4.5.14' - dependencySet(group: 'tech.pegasys.signers.internal', version: '1.0.4') { + dependencySet(group: 'tech.pegasys.signers.internal', version: '1.0.7-ADHARA-SNAPSHOT') { entry 'keystorage-hashicorp' + entry 'keystorage-hsm' + entry 'keystorage-cavium' entry 'signing-secp256k1-api' entry 'signing-secp256k1-impl' } dependency 'org.zeroturnaround:zt-exec:1.11' + + dependency 'org.hyperledger.besu:plugin-api:1.4.0' + dependency 'org.hyperledger.besu.internal:metrics-core:1.4.0' } } diff --git a/libs/cloudhsm-3.0.1.jar b/libs/cloudhsm-3.0.1.jar new file mode 100644 index 000000000..5bf682bb1 Binary files /dev/null and b/libs/cloudhsm-3.0.1.jar differ